Commit Graph
626 Commits
Author SHA1 Message Date
Martin Vogel 867e5f883d Merge origin/main into the smart-CI branch
Ten files changed on both sides; every conflict was a union of independent
additions:

- workflows (_lint.yml, _test.yml): keep the branch's lane `if:` and main's
  pinned ubuntu-24.04 runner with its reason.
- internal/cbm/cbm.h: main's callable-identity and module-doc fields plus the
  branch's configured-test fields.
- Makefile.cbm: both new test files; prod_lsp_all.o keeps main's
  .build-config dependency and the branch's coverage flags.
- scripts/test.sh: main's setup-scripts contract becomes Step 0y3, ahead of
  the branch's 0z series and the contracts-only exit.
- src/foundation/subprocess.c: main's cbm_posix_close_fds_from (it closes the
  error descriptor too).
- src/mcp/mcp.c: main's repository default base for detect_changes, with the
  early returns released through the branch's detect_release.
- src/pipeline/lsp_resolve.h, tests/test_main.c: both includes, suites and
  start-up steps (main's git environment reset first).
- scripts/memory-core-baseline.txt: recounted on the merged tree and lowered
  (mcp.c 773, config_toml_edit.c 73).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-05 18:53:37 +02:00
Martin Vogel c5abb64943 feat(test-impact): detect_changes scope:"tests" end to end
One request pins the change (merge base .. HEAD), builds the graph and the
runnable test registry of exactly the HEAD snapshot, seeds the graph from
the change, walks what depends on it and answers which tests to run
(cbm.test_impact). Missing or doubtful evidence never narrows: it becomes a
run-all answer that names why.

Snapshot (classified ledger -> frozen graph -> registry):
- one shared language classifier for discovery and pinned blobs (raw bytes
  on every platform); the classified ledger feeds the frozen build, which
  indexes exactly the listed files and records partial parses as per-file
  PARSE_GAP instead of refusing the graph;
- the pinned tree skips symlinks and submodule links (counted), as
  discovery never indexes them;
- the test model scopes uncertainty to the suites of its file (they run
  whole), reads line splices where they occur, treats a mid-file
  `#ifndef X` + `#define X` as an ordinary condition, and no longer reads a
  plain identifier named like a macro as an invocation. On this repository
  the registry is now narrowable and matches a text census exactly
  (8,365 cases, 141 runner suites).

Seeding and reach (port of the measured replay, rule B):
- hunk -> overlapping definitions, comment-only hunks dropped, pure
  deletions -> enclosing definition, no-definition hunks -> whole file,
  deleted names found by text, header changes propagated through the
  include closure and the headers' macros (pastes included), file-level
  escalation for unclassifiable header lines;
- inbound fixpoint walk over CALLS, ASYNC_CALLS, USAGE, CALL_REFERENCE,
  READS (+ routes); cases, suites and the runner's entry points are sinks;
  a reached runner main runs everything (RUNNER_REACHED);
- a parse gap whose text names a reached definition reaches the
  definition around it (or its file);
- macro-registered tests run their suites whole; cases and suites map to
  graph nodes by their start line.
The line patterns reproduce the reference regexes exactly on 1,383,850
lines of this repository and 37,167,926 kernel lines; on 24 replayed
changes the engine selects at least what the reference selected (13
identical, the rest only conservative supersets on the same graph).

Also:
- configured test declarations that cannot be mapped degrade their file
  (no configured roles, a test_declarations diagnostic) instead of
  aborting the whole index;
- the selector keeps other runners' suites out of the selection, and the
  result codec judges the model by narrowability like the selector;
- a change to .codebase-memory.json runs everything (built-in rule);
- .codebase-memory.json with the c-cbm preset and this repository's rules.

Gate fixes (suites, lint-ci, Windows cross-compile):
- the request runs on its own thread with a default-size stack (daemon
  tool calls run on 256 KiB threads, where the in-process build died);
- a request-supplied config_path is no longer followed by an unlink of an
  uninitialized path buffer;
- the inventory's frame-local reader no longer stays reachable from the
  caller's source after the filter call;
- FILE_ID_INFO needs _WIN32_WINNT >= 0x0602; the pinned-tree header pins
  it before any system header (toolchains defaulting lower failed);
- memory-core ratchet: the win_utf8 converters gain core-backed
  `_in(cls, ...)` forms over one implementation (the legacy names keep
  the C library for unmigrated callers), the Git facts module uses them,
  and the Windows spawn path releases everything through one failure
  exit; pipeline, extraction and config allocations that this work
  touched moved to the core in matched pairs; baselines tightened;
- the branch's C files formatted with the project's clang-format.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-05 02:15:08 +02:00
Martin Vogel 2803d6a5fe Merge pull request #2255 from DeusData/distill/1245-route-literal-exts
fix(routes): a document or log path is not an HTTP route literal
2026-10-04 21:01:09 +02:00
Martin Vogel 982621dd08 Merge pull request #2459 from DeusData/fix/doc-comment-capture
fix(extract): capture complete doc comments across languages
2026-10-04 16:30:34 +02:00
Martin Vogel 369cf768c2 Merge pull request #2523 from DeusData/fix/issue-514-constructor-fields-v2
fix(typescript): resolve constructor properties and new-initialized fields
2026-10-04 16:24:22 +02:00
Martin Vogel 02abf31016 Merge pull request #2509 from DeusData/fix/issue-1186-psr4-imports-v2
fix(pipeline): resolve PSR-4 imports to exact class files
2026-10-04 16:23:52 +02:00
Martin Vogel dffec08c40 Merge pull request #2498 from DeusData/fix/issue-1277-py-crossfile-fields-v2
fix(python): retain typed fields across file boundaries
2026-10-04 16:23:46 +02:00
Martin Vogel 79363adbc7 Merge pull request #2495 from DeusData/fix/issue-598-graphql-route-name
fix(pipeline): key GraphQL routes by operation name
2026-10-04 16:23:32 +02:00
Martin Vogel a5d2ea0e18 Merge pull request #2466 from DeusData/fix/issue-1153-instance-receivers
fix(cpp): resolve Class::member calls across files (#1153)
2026-10-04 16:22:51 +02:00
Martin Vogel 971afc069d Merge pull request #2463 from DeusData/fix/http-axios-capitalized-import
fix(pipeline): classify capitalized `Axios` calls as HTTP clients
2026-10-04 16:22:26 +02:00
Martin Vogel 01fd6cb5e6 Merge pull request #2342 from DeusData/feat/callable-identity-plumbing
feat(identity): callable identity plumbing, no language enabled (#2061)
2026-10-04 16:21:48 +02:00
Martin Vogel a245618a64 wip(ci): checkpoint hybrid test-impact implementation
Preserve the current selector, frozen-input inventory, result codec,
coverage collection, runner filtering, and isolated Windows venue work.
Component-level regression proofs are recorded in the PR handover.

The live classified provider, public execution integration, artifact
admission, fresh oracle/shadow evidence, and full platform gates remain
incomplete. This checkpoint does not enable or claim verified narrowing.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-03 23:55:18 +02:00
Martin Vogel e37274f088 wip(graph): checkpoint staged member-resolution changes
Preserve the original staged graph and LSP work separately from the
smart-CI continuation. This is an unfinished checkpoint; final
integration and cross-platform verification remain outstanding.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-03 23:53:16 +02:00
Martin Vogel a4a65ed0ea fix(typescript): resolve constructor properties and new-initialized fields
Reuse scoped constructor typing for direct new field initializers and
collect annotated constructor parameter properties as class fields.

Refs #514

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-03 20:16:31 +02:00
Martin Vogel bf2daa3603 fix(lsp): detach field arrays before overlay refinement
Copy inherited field-type pointer arrays into the writable registry arena
before Python refines an existing field. Keep shared type objects intact
and avoid retaining per-file scratch types in the sealed fallback.

Add deterministic shared-state and later-overlay coverage, retaining the
sequential Python field regression and adding a forced-parallel variant.

Refs #1277

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-03 18:25:03 +02:00
Martin Vogel 830f0ccfae fix(pipeline): resolve PSR-4 imports to exact class files
Preserve import kinds and resolve class imports through Composer prefixes
to the existing target class file, including autoload-dev mappings.

Refs #1186

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-03 17:56:11 +02:00
Martin Vogel 519c765e1f fix(python): retain typed fields across file boundaries
Carry declared instance-field types through extraction and result
compaction into the shared Python type registry so chained calls through
imported classes resolve their receiver types.

Refs #1277

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-03 01:37:27 +02:00
Martin Vogel 193b0a4d06 fix(graph): an untyped C member access still binds an unambiguous member name
The previous commit bound a C member access only through the type of its
object. On this repository that removed 9% of the USAGE edges onto Fields; on
the Linux kernel it removed 44% (2,131,043 -> 1,184,323), and 359,236 of the
edges it dropped were onto a member name that exactly one Field in the whole
tree carries. Those were never a guess. The kernel has far more member
accesses the C LSP cannot type: designated initializers, structs reached
through transitively included headers, structs duplicated per architecture.

The defect in bare-name member resolution was the arbitrary pick among
SEVERAL same-named members, not binding by name as such. The rule is now:
  1. the object is typed: bind the Field its type names;
  2. the object is not typed: bind the member name only when exactly one
     Field carries it (cbm_registry_unique_field_qn). Symbols of other labels
     are not candidates for a member;
  3. otherwise bind nothing.
It applies at all six sites: USAGE and READS/WRITES (sequential and
parallel) and a member call that resolved onto a Field.

In C++ mode the C LSP now also publishes the object's type when that type
declares no such field (`text.size()` on a std::string). The row names no
node, so the resolver sees "typed, no project Field meant" and the name
fallback does not apply. C keeps the fallback there, because a C aggregate's
nested anonymous members are not all registered on it.

Linux kernel, old binary vs this commit (same machine, same run):
  wall 239 s -> 240 s; indexer CPU 1235 s -> 1249 s; peak RSS 15.8 -> 15.4 GB
  USAGE -> Field   2,131,043 -> 1,571,103
    kept 1,248,385; newly typed to the object's struct 322,718
    no longer bound 834,502, of which onto a unique member name 1,523
  WRITES -> Field    948,187 -> 782,846
  CALLS onto a Field  92,440 -> 1,778
  nodes: Class +1,658, Enum +782, Field +12,519, Variable +5,646
Redis (from scratch): Class 913 -> 1092, Field 3854 -> 4563, CALLS onto a
Field 455 -> 24.

The 834,502 edges no longer bound are member names several structs share,
on objects the LSP could not type: before, each was attached to one of the
same-named Fields by registry preference. Typing designated initializers and
following transitive includes would bind them correctly; neither is in this
change.

Tests: unique_field_qn_needs_exactly_one_field_of_that_name; the pipeline
member-access tests gain the untyped-object cases (a shared name binds
nothing, a unique one binds).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-02 02:21:12 +02:00
Martin Vogel 426676b60b fix(graph): typedef'd C aggregates get nodes; member access binds by object type
Two defects that fed each other, fixed together because the first alone would
let the new Field nodes attract bare-name calls.

1. An anonymous aggregate under a typedef had no nodes.
`typedef struct { ... } X;` produced no Class node and none of its members a
Field node: the struct_specifier carries no name of its own, and
extract_class_def gave up on it. That is the dominant C spelling of a type
(37 of the 38 structs in internal/cbm/cbm.h). The enclosing type_definition
now names the aggregate through its declarator; only a plain type_identifier
declarator counts, since a pointer, array or function typedef names another
type. Applies to struct, union and enum specifiers in C and C++ (`.h` is
classified C++).

2. A C member access was resolved by its bare member name.
The member half of `a.b` / `a->b` reached the short-name registry as `b`,
which returned whichever Field (or function, or variable) was called `b`: on
this repository 9,150 USAGE and 2,810 WRITES edges onto Fields from C
sources, 3,976 of the USAGE edges onto a name several structs share.

The C LSP already types the object of a field_expression. It now publishes
the owner as a FIELD_REFERENCE row, one per (enclosing function, object type,
field): the pipeline edge is function -> Field, so a row per occurrence would
only add a record per member access. The usage and read/write resolvers
(sequential and parallel) join those rows by function and member name; a C
member name with no typed owner binds nothing.

Four things had to hold for the owner to be found:
- the cross-file registry folded Field defs into their owning type for Go
  only, so every C/C++ struct from another file (every header) registered
  with zero fields. The fold now covers C and C++;
- c_register_lsp_defs kept the first 63 fields of a type. It is sized from
  the list now: a cap there silently dropped edges onto later fields;
- a struct is registered under the header that declares it while the use
  site knows its short name. The owner type resolves as written, then
  module-qualified, then by short name from this module, a directly included
  header, or as the single candidate. Two same-named structs with no
  evidence between them resolve to nothing;
- the parallel resolver ran the cross-file pass only for files with
  unqualified call sites. A C member access or member call that still has no
  owner now requests it too.

Calls landing on a Field follow the same rule (cbm_call_onto_field_policy):
dropped across languages (the #1928 reference rule; a Bash `command` or a
Python `sorted()` is not a C struct member), dropped for a bare C call
(`socket(...)`, a function-pointer parameter), and bound by owner for a C or
C++ member call. Bare C++ calls and every other language are unchanged.

Measured on this repository, old binary vs new:
  C/H Class nodes                                         155 -> 1000
  C/H Field nodes                                        1047 -> 5478
  enumerators of formerly anonymous enums                +911 Variable nodes
  USAGE onto the new types, enums and constants          +23k edges
  USAGE -> Field from .c, owner type named in the file  58.1% -> 97.6%
  USAGE onto a non-Field, name only used as a member      146 -> 0
  WRITES -> Field from .c                                2981 -> 6534
  CALLS                                       159 removed, 3 added
  index wall time unchanged within noise; peak footprint +2.8%
  full macOS suite: 8309 passed, 0 failed, 10 skipped

Tests: extract_c_anonymous_typedef_aggregate_is_named_by_its_typedef,
pipeline_c_member_access_binds_field_by_object_type (+ _parallel),
call_onto_field_follows_language_and_shape,
c_member_access_binds_by_owner_only. Each was RED on the unchanged tree; the
pipeline tests are RED again with the call policy neutralized.

Not in this change: a function-pointer member declared inline
(`void (*f)(int);`) has no Field node; a `p->f()` call is not resolved (the
registry splits on `.` and `::` only); inline functions in `.h` files keep
the bare-name member binding; designated initializers are not typed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-02 00:53:42 +02:00
Martin VogelandAndrew Hundt 38d422f030 fix(routes): a document or log path is not an HTTP route literal
Distilled from #1245 by Andrew Hundt (a slice of 0370d52f).

detect_url_in_args in the parallel resolver runs for every resolved call
with no callee gating. A first argument like "/new/file.txt" clears
normalize_url_arg (it has a second slash and no whitespace) and then
reaches cbm_service_pattern_is_http_route_literal, whose filesystem
extension list knew about config, key and socket files but not about
documents or logs. So

    open_file("/new/file.txt")
    read_file("/docs/guide.md")
    write_file("/data/app.log")

each minted a Route node and an HTTP_CALLS edge -- the graph reported a
web endpoint where the program opens a file. Five extensions join the
list: .log .md .pdf .rst .txt, alphabetical order preserved.

Two things the tests are careful about, because the obvious fixture
would not have been RED. A bare Python builtin open(...) never resolves
to an indexed target, so it never reaches the URL detector at all; the
bug shows only for calls that resolve to a project symbol, which is what
the fixture defines. And /var/... is already rejected by the filesystem
ROOT list, so the fixture paths start with segments that list does not
know, leaving the extension guard as the only thing that can reject them.

RED before the fix:
  infrascan_http_route_literal_guard_rejects_document_extensions
      FAIL tests/test_infrascan.c:44:
      ASSERT(!(cbm_service_pattern_is_http_route_literal("/new/file.txt", "open")))
  pipeline_arg_url_rejects_document_file_paths
      FAIL tests/test_pipeline.c:6652:
      count_nodes_named(s, project, "/new/file.txt") == 1, expected 0 == 0

The pipeline test keeps a positive control above that line -- a genuine
requests.get("/api/items") still yields its Route node and HTTP_CALLS
edge -- so the guard cannot pass by rejecting everything.

GREEN after: infrascan 4 passed; pipeline 282 passed; extraction
edge_types_probe route_canon 420 passed.

Co-authored-by: Andrew Hundt <ATHundt@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-01 23:45:24 +02:00
Martin Vogel 922531a130 fix(csharp): refresh grammar for collection expressions in conditionals (#1748)
The vendored tree-sitter-c-sharp (88366631d598) predated upstream's C# 12
collection-expression support (tree-sitter-c-sharp #402, first released in
v0.23.4). A `[...]` literal could only be parsed as an
element_binding_expression, so `c ? [] : x`, wrapped `? []` ternaries and
both-branch collection conditionals had no valid parse. Error recovery then
ate the surrounding statement, the file was reported as parse_partial, and
nearby declarations could fall out of the graph.

Refresh the generated grammar bundle byte-for-byte from upstream tag v0.23.5
(cac6d5fb595f5811a076336682d5d595ac1c9e85). The ABI stays at 15, the MIT
LICENSE is byte-identical, and scanner.c is content-identical. The only new
node types are collection_expression, collection_element,
expression_element and spread_element. No node type is removed or renamed,
and the C# extractor and LSP depend on none of the old or new names.
MANIFEST.md and the vendored checksums are updated.

Add parse_coverage regressions for the reported variants plus a `? null`
control. A second test checks that calls inside a collection expression
(element and spread) are still extracted. A malformed-source guard checks
that broken C# is still flagged.

On dotnet/runtime (60108ba6), 4 files are no longer parse_partial and no
file becomes newly partial. Utf8JsonWriterTests.cs gets its class back, and
216 orphaned Functions are re-attached as Methods. Two files that were
already partial for unrelated unsupported syntax (the preview `closed`
modifier, `async` as an identifier) recover differently and lose 46 and
10 definitions.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-01 20:13:24 +02:00
Martin Vogel dab9187ca5 fix(extract): capture complete doc comments across languages
extract_docstring() took only the single comment node directly before a
definition. Multi-line `//` and `///` docs therefore kept just their last
line (in C# usually `/// </summary>`); docs above attributes, exports and
wrapper declarations were lost; fields, variables and modules never got a
doc; Perl POD was never stored; and every doc was cut at 500 bytes.

Capture the full documentation comment instead:
- contiguous same-style comment runs; Go comment groups with go/ast
  directive lines (`//go:`, `//line`, `//export`, ...) stripped
- docs above Rust attributes, JS/TS export statements and wrapper
  declarations (arrow and function expressions, class fields,
  assignments, ambient declarations) and C# top-level statements
- docs for fields, variables, constants and enum members, found with a
  memoised linear walk
- Go package comments and Rust `//!` docs on the File node
- Perl POD sections that name a sub (`=head`/`=item <name>`)
- Rust impl methods, Kotlin KDoc folded into package_header or sibling
  gaps, C `typedef struct/enum/union` declarations
- a doc comment wins over an intervening plain `//`; runs detached by a
  blank line and runs made only of divider lines are not docs
- no 500-byte cut; the properties buffer grows with the docstring, so a
  long doc no longer pushes other properties out

Measured on nine real-world corpora (Go, C#, Java, TypeScript, Rust, C,
PHP, Perl, Kotlin): full-doc capture Go 35% -> 99.999%, C# 10% -> 99.8%,
Java 74% -> 99.99%, Rust 7% -> 99.9%, C 67% -> 99.2%, Kotlin 70% ->
99.96%, TypeScript sources 42% -> 100%, named-sub Perl POD 0% -> 100%.
Node sets and all non-semantic edges are unchanged; index time stays
within run-to-run noise.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-01 19:48:10 +02:00
Martin Vogel 0bed0d372d Merge pull request #2327 from DeusData/fix/issue-1736
fix(grammar): accept a lone '&' in JSX strings and text (#1736)
2026-10-01 19:19:16 +02:00
Martin Vogel f67243ff0e Merge pull request #2320 from DeusData/fix/issue-1428
fix(extract): form Route nodes from NestJS @Controller/@Get decorators (#1428)
2026-10-01 19:19:04 +02:00
Martin Vogel 11867dfa74 feat(identity): callable identity plumbing, no language enabled (#2061)
Overloads that share a base QN (Java/C#/C++/Kotlin/Swift/Scala/ObjC) merge
into one graph node today, and that node collects the CALLS of every
overload. The agreed cure is one signature-qualified callable identity for
all overloading languages:

    callable_qn := base_qn [<tparams>] (params) [cvref]

This change lands the plumbing with every language still at
CBM_CALLABLE_ID_NONE, so the graph is byte-identical; each language is
switched on later in its own change (with its index-format bump).

- internal/cbm/callable_sig.{c,h}: the per-language mode side table
  (NONE|TYPED|LABELED_TYPED|LABELED|ARITY; kept out of CBMLangSpec like
  cbm_string_dispatch_suffixes, since ~160 positional rows would otherwise
  all need the member under -Wmissing-field-initializers), the shared
  builder cbm_callable_sig[_mode] for the def and the call-scope QN, and
  the inverse cbm_qn_callable_base_len[_named]. Suffix contract: no '.',
  '::' or '->'; whitespace-normalized; comments/annotations/attributes
  dropped; qualified types keep their last segment; '->' spelled '=>';
  variadic '[]' (Java) / '~'; C++ top-level cv dropped, arrays decay,
  cvref kept; <= 200 bytes, longer ones keep whole leading entries and end
  in '#<fnv1a64 of the full suffix>)'. The builder is iterative (cursor
  walk, bounded declarator loop) and self-checks against the inverse.
- The inverse is anchored to the node's bare name wherever the name is
  known: real QNs already end in ')' without being callables (a Java field
  whose name is its declarator `X = f()`, make `$(VAR)` targets), and only
  the anchor makes the inverse exact on them.
- Leaf splitters strip a suffix first (no-op today): registry by-name key
  (indexed by the bare name for a suffixed callable), cbm_lsp_bare_segment,
  cbm_pipeline_qn_class_method_tail, pxc_last_component, pxc_qn_leaf,
  go_index_visit_node, collect_prod_defs, sg_qn_prefix_len.
- New unused fields: CBMDefinition.qn_sig_off, CBMResolvedCall.callee_sig
  (relocated by result compaction), CBMRegisteredFunc.sig, CBMLSPDef.sig.
  The lsp_surface codec is unchanged (no format bump in this change).
- get_code_snippet and trace_path gain base-QN tiers
  (cbm_store_find_nodes_by_qn_base) that find nothing until suffixes exist.
- store: cbm_store_find_nodes_by_qn_suffix built its LIKE pattern in a
  512-byte buffer (a long suffix was truncated into a pattern that matched
  nothing) and did not escape '_'/'%' (`my_func` also matched `myXfunc`).
  The pattern is now heap-built, escaped, and matched with ESCAPE '\'.

Proof: full sorted node+edge+lsp_surface dumps are byte-identical before
and after on 7 corpora (this repo, elasticsearch libs, Exposed, dotnet
System.Linq, System.Collections, coreclr/gc, a C corpus).

Refs #2061

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-01 00:10:39 +02:00
Martin Vogel 3984795769 fix(ts-lsp): type imported class receivers for cross-file method calls (#1354)
A method call on an instance of a class defined in another file produced
no CALLS edge, while the identical code in the class's own file resolved
via lsp_ts_method. The type-aware tier typed the receiver wrongly in two
places, so the method lookup in the shared TS registry missed and the
call fell to method_not_in_registry:

1. `new C()` always qualified a bare constructor name against the
   CURRENT module. `const t = new ImportedClass(); t.m()` and
   `new ImportedClass().m()` were typed as a phantom
   `<this module>.ImportedClass`. The constructor type now follows TS
   scoping: a class declared in this module, else the class an
   unambiguous import binding names, else the old spelling.

2. ts_import_symbol_qn (and the copy of its heuristic in
   resolve_type_with_imports) treated an import value ending in
   ".<name>" as an already-qualified symbol QN. For the dominant TS
   convention of a file named after its class (`Foo.ts` exports
   `class Foo`, module `x.Foo`, class `x.Foo.Foo`), the receiver was
   typed as the MODULE, for `new`, annotations and parameters alike.
   The registry now decides the ambiguous case: a registered
   `module.name` wins, else the value is the symbol QN as before.

Both are resolved through the existing shared cross-file registry; no
name-based fallback is reintroduced. Cost is one or two hashed lookups
on the finalized overlay / sealed shared registry, per `new` expression
or per ambiguous import spelling during the walk; nothing is looked up
during registration and nothing is written to the shared registry.

typeorm (3,593 .ts files, shallow f279fd13): lsp_ts_method 2,551 ->
3,130, all 579 new edges cross-file (cross-file lsp_ts_method 696 ->
1,275), 0 edges lost, other strategies unchanged, index time
unchanged. 20 sampled new edges checked against source: 20 true
positives.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-30 23:06:27 +02:00
Martin Vogel 0bee4c1b79 Merge pull request #2406 from DeusData/fix/spill-test-disk-determinism
test(spill): make spill test verdicts independent of host free disk
2026-09-30 22:21:17 +02:00
Martin Vogel 6d19350e37 Merge pull request #2358 from DeusData/fix/issue-2311
docs(pipeline): CBM_EXTRACT_BUDGET is a CPU-time parse budget, not bytes (#2311)
2026-09-30 22:20:51 +02:00
Martin Vogel 1ff6299976 Merge pull request #2354 from DeusData/fix/issue-2235
fix(extract): read the url of a JS/TS request-config object (#2235)
2026-09-30 22:20:23 +02:00
Martin Vogel 78795ad76a Merge pull request #2339 from DeusData/fix/issue-2053
fix(rust): never let a std-typed receiver bind a same-named project method (#2053)
2026-09-30 22:19:17 +02:00
Martin Vogel 5910c47a97 Merge pull request #2332 from DeusData/fix/issue-1916
fix(pipeline): compose axios.create baseURL into HTTP_CALLS (#1916)
2026-09-30 22:19:12 +02:00
Martin Vogel 6bce86383e Merge pull request #2330 from DeusData/fix/issue-1783
fix(sqlite-writer): never end a B-tree level with a zero-cell interior page (#1783)
2026-09-30 22:18:48 +02:00
Martin Vogel 09819ba144 Merge pull request #2328 from DeusData/fix/issue-1743
fix(lsp): bound structured-binding field reads and TS method heritage walk (#1743)
2026-09-30 22:18:43 +02:00
Martin Vogel f5ecb5f739 Merge pull request #2321 from DeusData/fix/issue-1457
fix(dart): index extension declarations and their members (#1457)
2026-09-30 22:18:27 +02:00
Martin Vogel 88d4f29d18 fix(extract): form Route nodes from NestJS @Controller/@Get decorators (#1428)
NestJS controllers produced zero Route nodes. Two gaps in the decorator
route walk (extract_route_from_decorators):

- It only inspected decorator children of kind `call` (Python). A
  TypeScript decorator's call is a `call_expression`, so @Get(':id') was
  never examined.
- The path helpers only accept literals that start with "/". Nest writes
  segments without one (@Controller('users'), @Get(':id')), and the
  class-level prefix was composed only for Java/Kotlin (Spring/JAX-RS).

TS/TSX class methods now read the Nest verb decorators (@Get, @Post, @Put,
@Delete, @Patch, @Head, @Options, @All -> ANY) and compose them with the
class's @Controller / @JsonController prefix. The prefix can be a string,
an array (first entry), or the options object's `path`. Paths are rooted
at "/". A verb decorator outside a controller class forms no route. A
non-literal path argument (a constant) also forms none, so no route is
invented. Route and HANDLES are reused as they are: no new label.

Not covered: the app-level setGlobalPrefix() and enableVersioning() in
main.ts are cross-file and are not applied.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-29 03:14:39 +02:00
Martin Vogel 1b79558975 fix(grammar): accept a lone '&' in JSX strings and text (#1736)
A plain '&' inside JSX -- a URL query in an attribute
(href="https://fonts.googleapis.com/css2?family=Inter&display=swap"),
or text such as <p>Tom & Jerry</p> -- produced a tree-sitter ERROR node
in .js, .jsx and .tsx files. The file was flagged parse_partial in the
coverage view, and the error recovery turned words from the URL
(create-next-app -> create/next/app) into identifiers that resolved to
unrelated symbols as false USAGE edges.

Root cause is upstream tree-sitter-javascript, which the tsx dialect
builds on: unescaped_{double,single}_jsx_string_fragment is
/([^"&]|&[^#A-Za-z])+/, so '&' is only accepted before a space/digit
or as a complete html_character_reference (&amp; / &#38;). jsx_text is
scanned by the external scanner, which stops at every '&', and nothing
in _jsx_child accepted a '&' that is not a reference. So '&' + letter in
a string, and any lone '&' in JSX text, had no valid token.

Fix: javascript and tsx become self-maintained forks of the pins we
already vendored (javascript @ 58404d8cf191; typescript tsx @
75b3874edb2d on its javascript base @ 3a837b6f3658), following the
arkts model. One patch (tools/tree-sitter-javascript/patches/
jsx-lone-ampersand.patch) is applied to both javascript bases:
- a one-character '&' token at lexical precedence -1, aliased to the
  existing string_fragment (in strings) and jsx_text (in text); the
  lower precedence keeps &amp; / &#38; lexing as html_character_reference
- the string-fragment regexes exclude a '&' directly before the closing
  quote, so "a&" no longer runs past the end of the string
Node types are unchanged (same type/named set). The external scanners
stay upstream bytes. Parsers regenerated with tree-sitter-cli 0.25.10
(ABI 15; tsx moves from ABI 14, and its tree_sitter/ headers now match
arkts). Without the patch both fork dirs regenerate upstream exactly.
The upstream corpus passes unchanged: javascript 116/116; tsx gives the
same results as unpatched upstream.

Proof on vercel/next.js examples/ (canary 725705bd, 1,722 js/jsx/tsx
files): parse_partial 92 -> 47 (tsx 33 -> 0, jsx 2 -> 0, js 12 -> 2; the
two left are Babel do-expressions and Flow types), no newly flagged
file, code-graph nodes 14,739 -> 14,739, edges 24,773 -> 24,657: the
115 USAGE edges removed were all false cross-example edges from URL
fragments, plus a 4 SIMILAR_TO / +3 SEMANTICALLY_RELATED reshuffle
from the corrected ASTs.

Registered like arkts: MANIFEST.md fork rows + notes (ABI tally
recounted), audit FORKS map, THIRD_PARTY.md, vendored checksums.
LICENSEs stay upstream's MIT byte-identical.

Tests: parse_coverage gains a .js/.jsx/.tsx matrix (double/single-quoted
attribute, '&' before the quote, '&' + letter/space/digit in text, plus
&amp;/&#38; controls) and a guard that broken JSX is still flagged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-29 03:02:51 +02:00
Martin Vogel cdd8c895d0 fix(cpp): resolve Class::member calls across files (#1153)
A method called on the object a static factory returns got no CALLS
edge when the class lives in another file:
`SecdManager::getInstance()->m()`, `Registry::instance().m()`, and
`auto w = Widget::create(); w->m()` with std::unique_ptr or
std::shared_ptr. The same code in a single file already resolved.

Why: the cross-file C++ LSP resolved the scope of `Class::member` only
as the bare QN or under the caller's own module. A class declared in a
header and defined out of line in a .cpp is registered under that
file's module (`<proj>.app.src.SecdManager.SecdManager`), so the static
call missed and fell to a name heuristic (unique_name 0.38,
qualified_suffix). Its return type stayed unknown, and every receiver
built from it dropped the method call. A name heuristic on the same
site could also bind a same-named function elsewhere: luabridge's
`UserdataSharedHelper<C, false>::push(...)` was bound to the free
function `luabridge::push`.

Fix: once every existing lookup of `Class::member` has missed, resolve
the scope to exactly one registered class (bare, caller module, then a
type whose QN ends in `.<scope>` at a segment boundary) and look the
member up on that class: direct, aliases, bases, never re-anchored onto
a same-named class elsewhere. Two classes with that name name no single
target, so the call stays unresolved. The same resolution types the
call, so the declared return type (pointer, reference, unique_ptr,
shared_ptr) reaches chained and auto receivers. C/C++ LSP only; no
call that resolved before changes.

Proof, qBittorrent e8f7c18b (628 C/C++ files), same machine, fresh
index with a binary built from origin/main and one with this change:
- CALLS 17,401 -> 17,889: +489 edges (487 lsp_type_dispatch, 2
  lsp_scoped) and -1 edge, the suffix_match `luabridge::push` false
  positive above.
- 295 existing edges now come from the LSP with the same target (268
  qualified_suffix, 27 unique_name -> lsp_scoped).
- All 489 added edges checked against their source line: 155 are
  `Class::f()->m()` or `Class::m()` on that line, 334 go through a
  receiver initialised from `Class::instance()` in the same function
  (Preferences, SettingsStorage, UIThemeManager, Logger). None binds a
  different class.
- `X::instance()->m()` sites whose enclosing function has the edge:
  4/461 -> 304/461. Namespaced scopes (`RSS::Session::instance()`) stay
  unresolved: forward declarations mint extra Class nodes, so the
  scope does not name one class.

Tests (pipeline suite; each runs the sequential and the parallel path):
one per shape (pointer, reference, unique_ptr, shared_ptr), plus
controls: a class with the same method names is never bound, and an
undefined receiver type and a class name two libraries declare both
stay unresolved. The fixture asserts that every node it names exists,
so a missing node cannot pass as a missing edge. RED 3/3 on main,
green with the fix, RED again on revert; dropping the uniqueness rule
turns the ambiguity control RED.

The Python half of #1153, a method call on a module-level instance
(`manager = StackManager(); manager.status()`), is fixed by the held
Python LSP change 53653c15, not here.

Refs #1153

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-29 01:11:14 +02:00
Martin Vogel 80eb92a701 Merge pull request #2252 from DeusData/distill/1245-scope-lookup
perf(lsp): collapse repeated Python scope-chain lookups
2026-09-29 00:32:27 +02:00
Martin Vogel 17718126ea Merge pull request #2375 from DeusData/fix/complexity-walker-no-silent-cap
fix(extract): AST walker stacks grow instead of silently capping content
2026-09-29 00:31:19 +02:00
Martin Vogel d82e1e6014 Merge pull request #2326 from DeusData/fix/issue-1735-sql-cost
fix(sql,coverage): make large SQL dumps cheap to parse (#1735)
2026-09-28 19:44:27 +02:00
Martin Vogel d17f97f431 Merge pull request #2352 from DeusData/fix/issue-2176
fix(rescript): stop binary .res resources hanging the indexer (#2176)
2026-09-28 19:43:46 +02:00
Martin Vogel d3059efebd fix(pipeline): classify capitalized Axios calls as HTTP clients
With `import Axios from "axios"`, a common convention in TypeScript
projects, every `Axios.get/post/put/delete(url)` client call was indexed as
a route registration. The result was a `CALLS {via: route_registration}`
edge to a Route node instead of an HTTP_CALLS edge. The client's request
paths therefore posed as server routes, and no HTTP call site existed to
match a handler. microsoft/Game-Control-Puzzle-Event-Administration-Tools
has all 69 of its client calls in this shape.

Root cause: service-pattern matching is case-sensitive by design, so that
"requests" cannot match "Requests" or "myrequests". The HTTP library
table knew only the lowercase id `axios`. `Axios.get` therefore matched no
library, on either resolver. Resolution came back empty, because an
external package is never indexed. The call then fell through to the
callee-suffix fallback, which treats any `<x>.get/post/...(path)` as a route
registration.

Fix: add `Axios` next to `axios` in the HTTP library table. The table
already pairs case variants of one library (`Dio`/`dio`,
`Guzzle`/`guzzle`, `OkHttp`/`okhttp3`). The existing identifier-boundary
rule applies unchanged:
- `Axios.get` and `AxiosInstance.get` (axios's own instance type) match;
- a lowercase continuation such as `Axiosphere` does not.
Express/Koa/Fastify registrations such as `router.get(path, handler)` are
untouched.

Proof (release binary, macOS arm64,
microsoft/Game-Control-Puzzle-Event-Administration-Tools @4fe23196,
before -> after):
- route_registration CALLS 69 -> 0; all 69 were `Axios.*` calls in
  client/.
- All 69 reappear as typed HTTP_CALLS from the same caller to the same
  Route QN (`__route__PUT__/api/admin/users/{}`), and no typed HTTP_CALLS
  edge is lost.
- HTTP_CALLS 4 -> 142. Besides the 69 typed calls, 69 are the method-less
  arg_url twins that fix/http-typed-call-no-url-twin removes, and 4 are the
  existing `isOnPage` arg_url edges.
- CALLS 868 -> 799.
- SEMANTICALLY_RELATED 83 -> 174, deterministic across two runs of each
  binary. The semantic pass tokenizes CALLS-neighbour names, and the fake
  registration edges had fed each service function's URL path into its
  vector. Without them, the 69 `Axios` wrapper functions read as related.
Five sampled calls were checked against the client source, including
admin/users/service.ts:44 `Axios.put(`/api/admin/users/${participantId}/
${eventInstanceId}`)` -> `__route__PUT__/api/admin/users/{}/{}`.

On the pending ASP.NET route stack (fix/aspnet-route-case-inrepo, 8e734ee5),
together with the arg_url twin fix, the reclassified calls reach their C#
handlers:
- typed HTTP_CALLS 0 -> 69;
- DATA_FLOWS 0 -> 66, and HANDLES 94 -> 106 (+12 case_fold);
- sampled links match their controller source, for example
  `Axios.put(`/api/admin/users/${participantId}`)` ->
  `[Route("api/admin/users")] [HttpPut("{participantId}")] UpdateUserInfo`;
- the 3 calls left unlinked carry a query string in the URL literal.

Tests: RED 3/3 before the fix, green after, RED again on revert.
- pipeline_capitalized_axios_import_is_http_client (parallel resolver) and
  its _sequential twin: `import Axios from "axios"` with Axios.get, .post
  and .delete gives 0 route registrations and exactly one typed
  HTTP_CALLS per call, while an Express `router.get` registration in the
  same project stays a registration.
- infrascan service-pattern tables: `Axios.get`, `Axios.post` and
  `AxiosInstance.get` classify as HTTP, and `geo.Axiosphere.map` stays
  NONE.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 19:33:28 +02:00
Andrew Hundt df8dd1011b internal/cbm/lsp/scope.*: inline complete binding lookup
Previous behavior: cbm_scope_lookup(), cbm_scope_contains(), and cbm_scope_lookup_callable() called an out-of-line complete-binding scan after the Python lookup consolidation.

What changed:
- internal/cbm/lsp/scope.h defines cbm_scope_lookup_binding() as the one static-inline scope-chain scan.
- internal/cbm/lsp/scope.c keeps the type, presence, and callable wrappers on that shared implementation without another function boundary.

Why: Python still reads type and callable identity in one traversal, while existing language resolvers retain the direct hot path used before the consolidation.

Verification:
- CBM_ONLY_SUITE=scope build/c/test-runner: 33 passed
- CBM_ONLY_SUITE=cs_lsp build/c/test-runner: 62 passed
- CBM_ONLY_SUITE=py_lsp build/c/test-runner: 125 passed
- neutral 11-pair extraction matrix: output parity; C# latency -2.97% vs first parent and -1.64% vs upstream; retired instructions +0.29% vs first parent and -1.01% vs upstream; Python scale latency -4.25% and instructions -2.81% vs first parent

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
(cherry picked from commit 150c88e1c8)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 00:13:39 +02:00
Andrew Hundt f339aa625f perf(lsp): collapse repeated Python scope-chain lookups
Add cbm_scope_lookup_binding so callers can read lexical presence, type, and callable identity in one traversal. Route Python expression, call, and callable-reference resolution through that complete binding and use the existing checked bind APIs for allocation-failure detection.

The neutral 11-pair production matrix kept exact output parity. Python scale latency was 4.33% below merge parent 07255cfe and 61.01% below upstream parent 0c64de93; incremental instructions were 3.09% and 54.07% lower. Focused ASan/UBSan scope and Python suites passed 33 and 125 tests.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
(cherry picked from commit 4d3a103047)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 00:13:39 +02:00
Martin Vogel 64cf275ffa Merge pull request #2367 from isc-tdyar/fix/iris-cls-dispatch
fix(python): resolve iris.cls("Pkg.X").Method() to the named class
2026-09-28 00:08:12 +02:00
Martin Vogel a1b7c05685 test(spill): make spill test verdicts independent of host free disk
extraction::extract_spill_round_trip_keeps_every_field and
parallel::parallel_spill_mode_builds_the_same_graph failed on any machine
with less than 10 GB free: cbm_result_spill_open refuses to spill onto a
nearly full disk (mem.spill.refused reason=low_disk), so the store never
opened. Three pipeline tests took the same refused-store path there and
passed without spilling at all (backpressure_futile_nap_disengages,
over_budget_after_futility_fails_whole_and_preserves_db,
spill_resolves_namespace_imports_like_memory). A verdict must be a pure
function of code, test, platform and seed, not of how full the disk is.

Add a free-space pin to result_spill that exists only in test-seam
builds (CBM_ENABLE_TEST_SEAMS). Production still asks the disk, and its
10 GB floor and half-of-free cap are unchanged. The test runner pins
64 GiB at the top of main, before worker re-exec dispatch, so every
suite and re-exec'd index worker sees the same disk. A new test,
extraction::extract_spill_refuses_below_the_free_disk_floor, pins the
guard itself: one byte under the floor refuses, the floor opens.

Verified with cbm_fs_free_bytes temporarily forced to 9 GB: both tests
RED 3/3 before, green 3/3 after, RED again with the runner pin removed;
the refusal test goes RED with the production guard disabled.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 00:02:48 +02:00
Martin Vogel 7707c9d791 fix(extract): AST walker stacks grow instead of silently capping content
Three iterative AST walkers kept their pending nodes in fixed-size stacks
and silently stopped pushing children once full:

  - cbm_compute_complexity and count_branching_iter (helpers.c,
    BRANCHING_STACK_CAP 4096) -> cyclomatic, cognitive, loop and access
    metrics
  - extract_body_ident_tokens (extract_defs.c, BT_STACK 512) -> the
    body-token sample that feeds semantic search / SEMANTICALLY_RELATED

Children are pushed last-to-first, so a full stack dropped a wide body's
FIRST children. The metrics undercounted, and the token sampler read a
mid-body window instead of the first identifiers. That let a work cap decide
graph content.

Real input: tree-sitter-llvm's generated ts_lex (a 4654-case switch)
reported cyclomatic 11133 / cognitive 29303. It really has 8776 if + 4654
case + 1 default + 1 switch = 13432 branches, which is what it reports now
(cognitive 35639). On the Linux kernel no function reaches the old 4096 cap,
so complexity is byte-identical for every function. Three functions with
bodies over 512 pending nodes now sample their tokens from the start of the
body (for example rimob_panel_on), which moves 11 SEMANTICALLY_RELATED edges.
Index time is unchanged within noise (main 214.4/207.9 s, fix 197.6/192.2 s).

Fix: cbm_walk_stack_reserve()/cbm_walk_stack_release() in helpers.c. Each
walker keeps a small on-stack first chunk (512 frames) and spills to the heap
through the memory core (cbm_alloc/cbm_realloc/cbm_free, class EXTRACT) only
for deep or wide trees. Traversal order is unchanged. If the stack cannot
grow, the failure is logged ("extract.walk_stack_alloc_failed") and the
walker returns a documented sentinel instead of a truncated result:
CBM_WALK_METRIC_UNAVAILABLE (-1) for the metrics, or no token sample.

The body-token sampler's caps of 128 identifiers and a 2048-byte buffer are
its sampling contract and stay. The 512-entry stack was never part of that
contract, because it broke the "first identifiers in source order" property.

Tests (extraction suite): a C function with 5000 top-level if/while
statements must report exact cyclomatic/cognitive/loop counts through both
the extractor and cbm_count_branching, and a 600-call body must sample from
f0 on. All three are RED on main (4095 / 4095 / sample starting at f91) and
GREEN with the fix.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-27 23:40:24 +02:00
Martin Vogel f4654a7c77 Merge pull request #2373 from DeusData/fix/defs-walk-no-cap
fix(extract): definitions walk stack grows without a ceiling
2026-09-27 23:39:31 +02:00
Thomas Dyar 075f40e9d4 fix(python): make iris.cls() callee primary, drop the extra bare call
When the receiver is iris.cls("<literal>"), the class-qualified name now
replaces the primary call's callee instead of being pushed as a second
call. Keeping the bare name let it weak-match an unrelated same-named
method.

Tests assert exactly one Run call per iris.cls() site, and the pipeline
fixture adds two unrelated Run definitions that must receive no edges.

Refs #1260

Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
2026-09-26 17:08:14 -04:00
Martin Vogel 77ab4bea87 fix(extract): definitions walk stack grows without a ceiling
walk_defs keeps its pending frames in a growable stack that was bounded by
an 8M-frame ceiling (env CBM_WALK_DEFS_MAX). At the ceiling wd_push logged
"extract.walk_defs_capped" once and then stopped pushing. Children are
pushed last-to-first, so a file wider than the ceiling lost its FIRST
definitions: a work cap decided graph content.

Fix: no ceiling. The stack doubles on demand, heap frames now come from the
memory core (cbm_realloc/cbm_free, class EXTRACT) instead of safe_realloc and
raw free, and the doubling is overflow-guarded (int capacity and size_t
byte count). The only thing that can stop the walk is an allocation failure,
and that is loud: logged at error level as "extract.walk_stack_alloc_failed"
(walker=walk_defs, pending, path) -- the same event the complexity walkers
use -- and the file's result is marked has_error with
"definitions walk: stack allocation failed", which the definitions passes
already report as a per-file extract error. The arena-backed path
(ctx->scratch) gets the same failure handling. Traversal order is unchanged.

CBM_WALK_DEFS_MAX is removed. It was read only in wd_stack_max(); it was not
documented anywhere (docs, --help, config, tests), so nothing else changes.

extract_defs.c loses its raw free() call, so the memory-core baseline for
the file drops from 5 to 4.

Test (extraction suite): walk_defs_wide_file_extracts_every_definition
extracts a C file with 1000 top-level functions with CBM_WALK_DEFS_MAX=256
set, and asserts every wd0..wd999 definition is present. RED on main
(extract.walk_defs_capped limit=256; wd0 missing), GREEN with the fix, RED
again with the production change reverted. The extraction (379), complexity
(5), ast_profile (10) and semantic (35) suites pass.

Real input: Linux kernel, index-only, fresh cache and runtime dirs, same
machine. main and fix both produce 8,530,336 nodes and 15,764,128 edges,
SEMANTICALLY_RELATED 607 each, and the full node set (label, QN, lines) and
edge set (type, source QN, target QN) hash identically -- no kernel file
approaches the old ceiling. Index wall time 210.0 s (main) vs 194.4 s (fix),
within run-to-run noise.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-26 20:59:00 +02:00