Commit Graph

72 Commits

Author SHA1 Message Date
Martin Vogel 1acd87912f test(java): pin enum constants surviving alongside enum methods
Java enum methods already resolve on main: find_class_member_body
descends into enum_body_declarations (Java-gated) while find_class_body
keeps returning enum_body, which is what extract_enum_members needs to
reach the constants — they are siblings of enum_body_declarations, not
children.

That distinction was unguarded. java_enum_dedup_preserves_calls_issue1234
asserted the methods and the absence of duplicate Function defs, but
never asserted the constants, so collapsing the two lookups would have
passed the suite while silently dropping every enum constant from the
graph.

PR #984 proposed exactly that collapse — redirecting the shared
find_class_body — which is what surfaced the gap. Verified binding:
pointing extract_enum_members at find_class_member_body reddens the new
Variable assertions and nothing else.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: sahil-mangla <manglasahil2017@gmail.com>
2026-08-20 18:42:26 +02:00
Martin Vogel 5e9c63304e Merge pull request #1763 from DeusData/feat/swift-protocol-methods
fix(swift): index protocol method requirements as Method nodes
2026-08-20 15:19:53 +02:00
Martin Vogel 5cabeb4b10 fix(swift): index protocol method requirements as Method nodes
A Swift protocol requirement — a bodyless `func generate() -> String`
inside a protocol — was absent from the graph entirely. Swift codebases
are heavily protocol-driven, so the requirement is very often the
declaration a reader is looking for, and "who declares generate()" had no
answer.

Three edits deliver it: protocol_body joins the class-body types so a
protocol's members are walked at all; protocol_function_declaration joins
swift_func_types, because extract_class_methods gates on that set and
would otherwise walk the requirement and discard it; and both name
resolvers accept the node, which has the same simple_identifier shape as
function_declaration.

Distilled from #613 by xbsjason. That PR also carried an enum/struct half
whose two assertions fail on CI: tree-sitter-swift has no
struct_declaration or enum_declaration node type (it models both as
class_declaration), so the corresponding swift_class_types entries are
inert and a bare enum is still labeled Class. That is a real pre-existing
modelling gap, left untouched here and now documented at the dead entries
rather than silently deleted.

Also dropped from the original: a find_first_descendant_by_kind rescue in
extract_class_methods. Removing it changes no test outcome once the
function-type entry is present, so it was carrying nothing.

Advances #43.

Co-authored-by: xbsjason <xbsjason@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-20 13:39:49 +02:00
Martin Vogel 43ecc0988c feat(dbt): extract dbt model lineage from Jinja-templated SQL
A dbt model is an ordinary .sql file whose dependencies are written
{{ ref('other_model') }} or {{ source('group','table') }}, never as
literal table names. The SQL grammar cannot read those -- FROM {{ ref(
'x') }} is a parse error to it -- so the dependency structure of an
entire dbt project was invisible to the graph.

extract_dbt.c runs as a sub-extractor inside the normal indexing
pipeline. Per qualifying file it emits one Model definition (named by
the file stem, dbt's own model identity) plus one usage per ref()/
source() call; pass_usages resolves those into model -> relation
lineage edges like any other reference.

Model is a relation label alongside Table and View, which is what makes
the rest work without new plumbing: registry seeding, the central
relation veto, the incremental surface hash, search ranking and the
architecture queries all pick it up from cbm_label_is_relation. Sharing
one label class also means a model's source('raw','customers') resolves
onto a Table declared in a plain DDL migration in the same repository,
so dbt lineage and SQL lineage form one graph rather than two, while
the veto keeps model names out of every non-lineage consumer.

The pass is self-gating: SQL files only, and only those carrying a real
ref()/source() call. The dbt builtins are themselves the evidence, which
is cheaper than a dbt_project.yml lookup and more precise -- templated
SQL that is not dbt (an Airflow {{ ds }} parameter) produces no Model
node and no usages even inside a dbt repository.

{% macro %} definitions are deliberately excluded: the vendored
tree-sitter-jinja2 grammar has no node types for {% %} statements, so
they can only be recovered by a hand-written scanner that cannot see
comments or find {% endmacro %} for a correct span. Filed as follow-up
rather than approximated.

Tests cover the lineage, the last-string-argument semantics of both
builtins, the gate (against a plain-SQL control extraction), plain DDL
staying untouched, and an end-to-end pipeline case asserting model ->
model across files, model -> Table onto plain DDL, and cross-language
isolation. Disabling the pass reddens three of them; removing Model from
the relation set breaks lineage outright.

Implements the lineage half of #575.

Co-authored-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-20 10:23:48 +02:00
Martin Vogel 7ba84e3754 feat(sql): first-class Table/View nodes with FROM/JOIN lineage
CREATE TABLE / CREATE VIEW / CREATE MATERIALIZED VIEW now produce Table
and View nodes (previously generic Variable), CREATE PROCEDURE produces
a Function, and schema-qualified DDL names (schema.table) are named by
the table identifier instead of the schema. A view's FROM/JOIN relations
are emitted as usages and resolve into view -> table USAGE lineage
edges.

Relations join the cross-file name registry so lineage can resolve, with
two structural safeguards:

- Registry membership is defined once by cbm_label_is_registry_symbol
  (helpers.c); the full, parallel and incremental seed sites all call
  it, ending the KEEP-IN-SYNC copies the old label lists required.
- The default cbm_registry_resolve vetoes relation-labeled results:
  common table names (users, orders, config) collide with code
  identifiers in every language, and no CALLS/USAGE/READS/WRITES/THROWS/
  handler/decorator consumer may bind them. The SQL lineage path opts in
  through the new cbm_registry_resolve_lineage.

Table/View also join the registry-only per-file LSP surface labels so a
table rename invalidates dependent SQL files on incremental (no stale
lineage edges), rank with the type tier in BM25 search, and appear in
the architecture boundary/package/cluster queries via the pinned
CBM_SQL_RELATION_LABELS fragment.

Tests: extraction trio (labels, lineage usages, schema-qualified names),
grammar golden + probe updates, relation-label contract pin, and two
pipeline tests: cross-language isolation (binding: fails without the
veto) and incremental table-rename stale-lineage.

Closes #574.

Co-authored-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-20 00:12:28 +02:00
Martin Vogel 33a3f0322a Merge pull request #1308 from Yyunozor/fix/issue-1294-is-test-tests-dir
fix(extract): converge Function/Method is_test with the tests/ path filter
2026-08-18 00:02:28 +02:00
Martin Vogel a3ae7513a4 Merge pull request #1400 from AmirF194/fix/1249-binary-concat-route-url
fix(extract): resolve Go route paths built via string concatenation
2026-08-15 08:57:12 +02:00
Martin Vogel 5eae0a4371 test(extract): bind the Java interface/enum method dedup fix (#1234)
Regression coverage for #1234: Java interface and enum methods were
emitted as both a Method node and a duplicate top-level Function node.
The production fix landed via 87a0e3f (language-agnostic class-body
routing); this adds the missing test coverage so it cannot silently
regress:

- java_interface_no_duplicate_function_issue1234: interface methods
  produce Method nodes only, zero Function nodes
- java_enum_dedup_preserves_calls_issue1234: enum methods dedup'd while
  cross-class CALLS edges survive
- cp_interface_method_no_dup_function: convergence probe asserting the
  surviving Method still carries its CALLS edge end-to-end

Distilled from PR #1327.

Co-Authored-By: Harshita Joshi <j.harshitaa06@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 16:37:06 +02:00
Amir Fathi 478ce25ce3 fix(extract): resolve Go route paths built via string concatenation
r.HandleFunc(base+"/login", handler) fell through extract_positional_url
with no branch for binary_expression, so first_string_arg stayed NULL. A
real BFF with 47 such registrations (all base+"literal") produced only 9
Route nodes. Recover the literal suffix from the right-hand operand of a
"+" concatenation; a right side that is not itself a literal is left
unresolved rather than guessed.

Fixes #1249

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
2026-08-01 06:17:02 +00:00
Martin Vogel 029416fda6 Merge remote-tracking branch 'origin/main' into qa/call-reference-rebase
# Conflicts:
#	Makefile.cbm
#	internal/cbm/extract_defs.c
#	src/foundation/compat_fs.c
#	src/foundation/compat_fs.h
#	src/pipeline/pipeline.c
#	src/pipeline/pipeline_incremental.c
#	tests/test_main.c
#	tests/test_pipeline.c
#	tests/test_store_checkpoint.c
2026-08-01 01:29:20 +02:00
Yyunozor cc7b4a8852 fix(extract): converge Function/Method is_test with the tests/ path filter
Function/Method nodes only got is_test=true via a Rust #[test] attribute
check; the per-file test-file flag (cbm_is_test_file) was set on the Module
node but never propagated to the definitions inside it, so e.g. a C function
in tests/helpers/fixtures.c stayed is_test=false — invisible to store.c's
is_test!=1 filters — even though trace_path's own independent path check
already treated the file as a test. Propagate is_test_file into
extract_func_def and push_method_def, and teach cbm_is_test_file the same
tests/ (and test/, spec/, __tests__/) directory convention cbm_is_test_path
already uses, so a non-test_-named file anywhere under tests/ is caught too.

Separately, trace_call_path's own include_tests filter (mcp.c is_test_file)
matched a nested ".../tests/..." path but not a project-root-relative one,
so tests/repro/foo.c leaked into results with the default
include_tests=false; add the missing tests/ (and test/, spec/, __tests__/)
prefix check.

Closes #1294.

Signed-off-by: Yyunozor <yyunozor@icloud.com>
2026-07-31 16:49:17 +02:00
Martin Vogel 8f06867d78 Merge pull request #1305 from sini/feat/nix-attrpath-and-variables
fix(extract): model Nix attrpaths (leaf name, scoped QN) and mint Variables for module-level bindings
2026-07-31 08:17:56 +02:00
Martin Vogel facd32b230 Merge pull request #1008 from CharlesQueiroz/fix/template-string-urls
fix: flatten JS/TS template-literal URLs to {} placeholders
2026-07-31 05:33:38 +02:00
Jason Bowman 7f9feb6b63 test(nix): cover Variable extraction, scope bound, and the Function/Variable split
nix_module_level_bindings_mint_variables
      a `let` binding and an attrset binding both mint Variables, and the QN
      carries the attrpath (services.nginx.enable).
  nix_nested_bindings_are_not_module_level
      a binding two attrsets deep mints nothing, and neither `deep` nor `nested`
      becomes a Variable since both are scopes.
  nix_lambda_binding_is_function_not_variable
      a lambda-valued binding is a Function and NOT a Variable; a scalar binding
      is the reverse.

Every absence assertion is paired with a positive one on the same predicate in
the same result — `topLevel` in the nesting test, `val` in the split test — so
none of them can pass by extracting nothing at all. Several false "clean" results
in this area came from predicates that could not have matched.

Signed-off-by: Jason Bowman <jason@json64.dev>
2026-07-30 17:45:25 -07:00
Jason Bowman b5209af7a3 test(nix): cover attrpath qualification, quoting, and interpolation
Four extraction cases and one pipeline case, each asserting a distinct claim so a
regression names what it broke:

  nix_attrset_scope_disambiguates_leaf_names
      two attrsets each holding `dup` produce two definitions with distinct QNs.
      Unqualified they shared one QN and the second was discarded.
  nix_dotted_attrpath_qualifies_like_nested
      `wrap.deep.fn = …` and `wrap = { deep = { fn = …; }; }` produce the SAME
      QN. This is the equality that makes attrpath qualification a correctness
      fix rather than a preference — the two are the same expression.
  nix_quoted_attr_name_strips_quotes
      `"kebab-case"` is named kebab-case, and asserts the quoted form is absent
      rather than merely that the bare form is present.
  nix_interpolated_attr_mints_no_def
      `"${dynamic}" = …` mints nothing, and the sibling binding still does — so
      the test cannot pass by extracting nothing at all.

  pipeline_nix_scoped_binding_calls_resolve  (tests/test_pipeline.c)
      a call inside a scoped binding reaches its target through the store, for
      both routes into a qualified name: an enclosing attrset and a dotted
      attrpath.

The pipeline case has to live at that level. Definition QNs and call-scope QNs
come from two separate functions, and if they disagree the edge is dropped at
write with no error — every extraction-level assertion still passes while the
graph quietly loses edges. Only the store shows it.

Adds has_def_qn alongside the existing has_def. find_def_by_name returns the
first match by NAME and so cannot tell two same-named definitions in different
scopes apart, which is exactly what these tests exist to separate.

Extraction and pipeline suites: 491 passed, 0 failed.

Signed-off-by: Jason Bowman <jason@json64.dev>
2026-07-30 17:45:25 -07:00
Martin Vogel 4573a51d8f Merge pull request #1329 from harshitaajoshi/fix/cpp-gtest-name-collision-1266
fix(extract): derive unique names for GoogleTest macros to prevent same-file node collisions (#1266)
2026-07-31 02:39:32 +02:00
Martin Vogel 475a698d69 Merge pull request #1097 from JhohanBustamante/fix/decorator-comment-drop
fix(extract): keep decorators separated by a comment
2026-07-31 02:39:25 +02:00
Martin Vogel 8313b9abd8 Merge pull request #1304 from sini/spike/nix-function-header-defs
fix(extract): Nix definitions are dropped in any file whose root expression is a function
2026-07-31 02:08:50 +02:00
Martin Vogel ae52db3147 test: make three Windows nondeterminisms deterministic
All three cost real release cycles, and none is fixed by widening a budget.

daemon_application_cancels_physical_job_only_after_final_session waited for the
SUBSCRIBER COUNT to reach 2, then cancelled both sessions and asserted the
physical job had started exactly once. The job starts asynchronously after
subscription, so both cancels could land first and leave starts == 0 --
arguably the correct outcome. It now waits for the state the assertions
actually require. Verified 47/47 on Windows, the only platform it ever failed.

The parallel harness refused outright when the suite leader had already exited,
because taskkill /T cannot walk a tree from a dead PID. But the leader can exit
between the timeout decision and that call, so the harness itself lost a race:
a natural exit at the wrong moment failed the whole wave. It now proves cleanup
the only way still available -- nothing parented to that PID -- and its contract
asserts the PROPERTY rather than the phrase "tree cleanup" it used to grep for.
That string pin is what broke when the guard was reworded while behaving
correctly; the contract now checks rc==2 AND that the descendant really did
survive, which would also catch a guard that claims to fail closed while
leaking.

extract_wide_flat_file_is_linear took ONE sample per size, so the ratio carried
the noise of both. On a loaded Windows VM linear code measured 51x against a 40x
bound (184ms -> 9387ms). Best-of-N instead: timing noise only ever adds time, so
the minimum is the cheapest good estimate of the noise-free cost. The bound is
deliberately unchanged -- it sits where linear (~20x) and quadratic (~128x) are
each >=2x away, so raising it would move the test toward the very signal it
exists to catch. Now measures 19.1x on Windows, 21.4x on macOS.

Also: the smoke's `cli` helper redirected stderr to a file and discarded it, so
any of the 10 bare `VAR=$(cli ...)` assignments could kill the run under
`set -euo pipefail` printing NOTHING. One such abort cost a full Windows cycle
just to locate and still could not be attributed. It now surfaces the command
and its stderr. Neutral wording on purpose: one call site expects a non-zero
exit and must not read as a failure.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-29 17:05:10 +02:00
Harshita Joshi a6f32d0fba fix(extract): derive unique names for GoogleTest macros to prevent same-file node collisions (#1266)
Signed-off-by: Harshita Joshi <j.harshitaa06@gmail.com>
2026-07-28 23:25:36 -05:00
Jason Bowman c7cb8c4e33 test(nix): cover the root shapes, and assert defs rather than absence of error
`nix_function` was the only Nix extraction test, and it could not fail for the
bug the previous commit fixes. Three separate reasons:

  - its fixture is function-headed — the exact broken shape;
  - its binding, `hello = pkgs.writeShellScriptBin "hello" ''...''`, is an
    application, not a function, so no Function def is expected from it either
    way;
  - it asserts only ASSERT_NOT_NULL and ASSERT_FALSE(has_error), omitting the
    has_def check every adjacent language test makes.

So the suite reported green while definitions were being dropped for the
dominant file shape in the ecosystem.

This keeps `nix_function` as-is — it usefully pins the "parses, yields no def"
case — and adds one test per root shape, each asserting its own definitions so a
regression names the shape it broke:

  nix_defs_in_let_rooted_file               let ... in
  nix_defs_in_attrset_rooted_file           { ... }
  nix_defs_in_nested_let                    let inside let
  nix_defs_survive_function_header_let      { prelude }: let ... in
  nix_defs_survive_function_header_attrset  { prelude }: { ... }
  nix_defs_survive_curried_header           final: prev: { ... }
  nix_curried_lambda_mints_one_def          guards the other direction

The curried-header case is the nixpkgs overlay signature, the most common
multi-arm header in the ecosystem: two nested function_expressions sit between
the file root and the body. The last test exists because the fix descends past
the header — it pins that `iota = a: b: a + b` mints exactly one Function, so a
change that starts minting the inner arm as a second def is caught.

Verified as a negative control — with the previous commit's one-line change
reverted, the three function-header tests fail on their own assertions (gamma,
eta, iota) while the pre-existing shapes stay green. With it applied the
extraction suite is 260 passed, 0 failed.

Signed-off-by: Jason Bowman <jason@json64.dev>
2026-07-27 17:14:39 -07:00
Martin Vogel 87a0e3f74f fix: preserve semantic graph relationships
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-18 15:52:50 +02:00
Charles Queiroz a029b3a63b fix: flatten JS/TS template-literal URLs to {} placeholders (#1006)
Client-side URL extraction only recognized static string literals; any
template literal was silently skipped, so parameterized endpoints never
produced HTTP_CALLS edges or Route nodes and cross-repo route matching
missed them (the server side already normalizes path params to {}).

New cbm_template_string_text() flattens a template_string node: string
fragments verbatim, each ${...} substitution becomes {}. Wired into:
- extract_positional_url / extract_string_value (call-arg URLs)
- handle_string_refs (URL-shaped refs from const/return positions)
- handle_string_constants (module-level const lookups)

`/api/v1/things/${id}` now yields __route__ANY__/api/v1/things/{} and
the enclosing function gets the HTTP_CALLS edge, joining the canonical
placeholder shape of server-side routes.

Signed-off-by: Charles Queiroz <fcqueiroz@liquibase.com>
2026-07-16 14:30:55 -03:00
Martin Vogel a75f4b6a3b fix(extract): don't flag in-body function-like macro calls as parse gaps
DCO / dco (push) Has been cancelled
A function-like macro invocation whose argument is a type token — e.g.
ALLOC(int, n) — makes tree-sitter's C/C++ grammar emit an ERROR node (it parses
the type in expression position), which cbm_collect_error_regions recorded as a
parse_partial coverage gap. But the macro is #defined in the same file and the
call sits inside an already-extracted function body, so nothing is actually
missing from the graph — it's a benign call the grammar can't parse without the
preprocessor (#1071, systematic across allocation-macro-style codebases).

Subtract an error region only when it both (a) contains an invocation of a
file-defined function-like macro and (b) is fully enclosed by an extracted
Function/Method body. Condition (b) keeps a TOP-LEVEL macro invocation that
expands to a definition still flagged (#949: the generated def isn't in the
original span), and fails safe — a benign top-level call stays flagged rather
than a real gap being hidden.

Tests: a type-arg macro call inside a function no longer reports parse_partial;
a real in-body syntax error still does; #949/#946 preserved.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-16 18:19:54 +02:00
KolisCode 012be57815 fix(extract): keep decorators separated by a comment
Comments are NAMED tree-sitter nodes, so the prev-sibling walk in
extract_decorators() stopped at one — silently dropping every decorator ABOVE
an interleaved comment:

    @Post('login')                 <-- dropped
    @HttpCode(HttpStatus.OK)       <-- dropped
    // throttled per IP and account
    @Throttle({ ... })             <-- kept
    async login(...)

The route then vanished from decorator/route queries, so documenting a
decorator made the endpoint disappear from the graph. Real-world impact: on a
NestJS backend, 1 of 95 HTTP endpoints was missing — the one whose throttle
policy carried an explanatory comment.

Comments are now transparent to the walk, the same way anonymous tokens
(e.g. TS `export`) already were. Reuses the existing is_comment_node() helper.

Signed-off-by: KolisCode <jhohantma@gmail.com>
2026-07-15 21:01:29 -05:00
Martin Vogel e2d41d9cd4 Merge pull request #1094 from harshitaajoshi/fix/utf8-docstring-truncation-1017
fix(extract): snap docstring truncation to UTF-8 codepoint boundary (#1017)
2026-07-16 02:16:11 +02:00
Martin Vogel 2080d82100 Merge pull request #1060 from isc-tdyar/fix/semantic-body-tokens
fix(extract): populate body_tokens before MinHash gate; add OS/TS identifier node types
2026-07-16 01:50:34 +02:00
Martin Vogel ef1482245f Merge pull request #949 from blankanswer/agent/fix-cpp-preproc-signature-gap
fix(cpp): remap recovered definitions to original lines
2026-07-16 00:05:49 +02:00
Harshita Joshi 4ce04f2d03 Merge branch 'main' into fix/utf8-docstring-truncation-1017 2026-07-15 16:28:54 -05:00
Harshita Joshi 3ebcf48a46 fix(extract): snap docstring truncation to UTF-8 codepoint boundary (#1017)
Signed-off-by: Harshita Joshi <j.harshitaa06@gmail.com>
2026-07-15 16:27:34 -05:00
Martin Vogel 34867d9b34 Merge pull request #1007 from CharlesQueiroz/fix/jaxrs-path-composition
fix: compose JAX-RS method-level @Path with class-level prefix
2026-07-15 22:21:38 +02:00
Thomas Dyar b9797ecd44 fix(extract): populate body_tokens before MinHash gate; add OS/TS identifier node types
Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
2026-07-14 20:53:58 -04:00
Martin Vogel feadbe1955 Merge pull request #467 from isc-tdyar/pr/objectscript-language-support
feat(objectscript): InterSystems IRIS ObjectScript language support
2026-07-15 01:33:19 +02:00
Charles Queiroz 041eb99c11 fix: compose JAX-RS method-level @Path with class-level prefix (#1005)
JAX-RS splits a route across two annotations: the verb comes from a bare
@GET/@POST/... and the path from a sibling @Path. The annotation scan
returned on the first mapping annotation, so the @GET matched, defaulted
the path to "/" and the sibling @Path was never read. Class-level @Path
was never recognized as a prefix either, because a lone @Path carries no
verb and the scan reported no route.

scan_route_annotations() now collects the whole annotation set (mapping
verb + optional inline path, and a separate JAX-RS @Path), then:
- method-level: verb required; path = inline mapping path, else @Path,
  else "/"
- class-level prefix: inline mapping path, else @Path
Also recognizes bare @HEAD/@OPTIONS verbs.

Spring behavior is unchanged (mapping annotations keep their inline
path); covered by the existing test suite plus a new regression test.

Signed-off-by: Charles Queiroz <fcqueiroz@liquibase.com>
2026-07-13 23:17:08 -03:00
Blank_Answer 99d39192cd fix(cpp): remap preprocessed recovery lines
Signed-off-by: Blank_Answer <97771966+blankanswer@users.noreply.github.com>
2026-07-13 05:21:54 +00:00
Martin Vogel db330bf3a0 Merge pull request #1050 from DeusData/fix/961-ifdef-split-defs
fix(extract): recover C/C++ defs lost to #ifdef-split brace ERROR regions
2026-07-12 16:41:22 +02:00
Martin Vogel 65a176e4d8 fix(extract): recover C/C++ defs lost to #ifdef-split brace ERROR regions
DCO / dco (push) Has been cancelled
A function whose body braces are split across #ifdef/#else branches
(one open brace per branch, a single shared close) parses with an
ERROR region on the raw source - both branches are present at once, so
the braces are unbalanced - and the defs walk silently dropped it
while its callers stayed as unresolved names (cbm_path_within_root,
handle_process_kill).

The preprocessed second pass (simplecpp) already exists for macro-
hidden CALLS; it evaluates the conditionals, so one branch is chosen
and the expanded tree parses clean, with same-file token lines aligned
to the original. Recover definitions from it, adopting only defs that
(a) intersect a raw-tree ERROR region, (b) have their name visible on
the corresponding original source line (rejects header-inlined defs
whose physical expanded lines alias unrelated raw lines when
compile_commands include paths resolve), and (c) are absent by
qualified name from the raw pass.

Verified on the real instances: handle_process_kill (580-647) and
cbm_path_within_root (5070-5085) both index with correct line ranges.

Closes #961

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-12 15:09:07 +02:00
Martin Vogel 2f3be252a3 test(extract): guard delegation-receiver recursion false positives
DCO / dco (push) Has been cancelled
The #599 receiver-aware narrowing (87091ed) shipped without coverage
for the delegation/store bucket reported in #876: same-named calls
whose receiver is a call result (_get_store().get inside get), a
module singleton (_default.check inside check), or a loop-local cursor
(cur.execute inside execute). All three are clean on current main;
this guard pins the bucket so it cannot regress. Refs #876

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-12 13:55:45 +02:00
Thomas Dyar 4d6f2dd34a feat(objectscript): add InterSystems IRIS ObjectScript language support
Add ObjectScript (InterSystems IRIS / Caché) as a supported language,
covering the UDL class format (.cls), MAC/INT routines (.mac/.int/.rtn),
include/macro files (.inc), and IRIS Studio Export XML.

Definition extraction (extract_defs.c): Class, Method, ClassMethod,
Property, Parameter, Index, Trigger (with body text), XData, Storage,
and Query members as graph nodes; base classes from the Extends clause.

Call dispatch resolution (extract_calls.c) — four ObjectScript patterns
that are structurally invisible to text search:
  1. ##class(Pkg.Class).Method()    explicit cross-class call
  2. ..Method()                     relative-dot self-call (the dominant
                                    intra-class form; large impact on
                                    CALLS completeness)
  3. $$$Macro                       macro expansion via a per-project
                                    table built from .inc files
  4. type inference from %New/%OpenId + declared return types

Ensemble production topology (pass_ensemble_routing.c): EnsembleItem
nodes per production component and ROUTES_TO edges resolved from
ProductionDefinition XData, plus WorkMgr .Queue("##class(X).method")
dispatch — all parsed statically at index time, no live IRIS required.

Language detection (language.c): .mac/.int/.rtn map to ObjectScript
routine directly; .cls (shared with Apex) and .inc (shared with BitBake)
are disambiguated by content, defaulting to the existing language on any
doubt so neither Apex nor BitBake detection regresses.

The two new per-project tables (macros, return types) are threaded
through a new internal cbm_extract_file_ex() so the public
cbm_extract_file() signature is unchanged.

The tree-sitter grammars for ObjectScript UDL and routine are vendored
in internal/cbm/vendored/grammars/objectscript_{udl,routine}/ from
https://github.com/intersystems/tree-sitter-objectscript (MIT, ABI 15).

Refs #462

Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
2026-07-11 19:09:43 -04:00
Martin Vogel 975dc76709 fix(test): constant def count in the wide-flat scaling fixture
DCO / dco (push) Has been cancelled
The guard's sparse defs scaled WITH the input (one per 2000 lines), so
the fixture inherited a slice of the separate per-def sibling-scan cost
(O(defs x siblings) — its own tracked finding): an n^2/2000 term that
is negligible on clang-macOS but dominant enough on windows-CLANG64
ASan to push LINEAR walk code to a 43x measured ratio, over the 40x
bound. Ten defs at fixed positions regardless of size keep the
anti-vacuous breadth check while the sibling-scan term stays 10 x n =
linear. Measured after the change: fixed code 22x, pre-merge quadratic
walk 339x — the discriminator now has ~2x headroom on the green side
and ~8x on the red side across all three toolchains measured.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-07 15:55:24 +02:00
Martin Vogel f380a4514f fix(test): scaling-ratio form for the wide-flat linearity guard
DCO / dco (push) Has been cancelled
An absolute wall-clock bound conflates machine speed with complexity
class: the gcc-13-ARM ASan CI leg runs this extraction ~200x slower
than clang at measured-perfectly-linear complexity (27.1/54.2/108.3/
214.8s for 50k/100k/200k/400k siblings) and flunked a 30s bound on
LINEAR code — the leg has always been the slow one (71min in the last
fully green run, unchanged since).

Assert the complexity class instead: extract the same comment-sibling
fixture at 20k and 400k lines and bound the growth ratio at 40x.
Calibrated from measurement, not models — the ASan builds carry a large
linear per-line baseline that dilutes small-growth ratios (at 8x growth
the known-quadratic pre-merge walk measured 22.4x and slipped under a
24x bound), while at 20x growth linear code measures ~20x on both
toolchains and the quadratic walk measures 64x. Verified RED (64.4x,
fails) against the pre-merge walk_defs and GREEN (19x) on the fix; the
test now prints both timings unconditionally for future triage.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-07 14:51:15 +02:00
Martin Vogel 7b8b7038f7 fix: TypeScript-at-scale indexing cascade and parallel-only crash recovery
DCO / dco (push) Has been cancelled
An 81k-file TypeScript corpus previously never finished indexing. Three
stacked defects, found by stack-sampling the live grind and reading the
supervisor's quarantine records:

1) walk_defs pushed children via index-based ts_node_child(i), which is
   O(i) per call in tree-sitter — O(n^2) per wide node. A 3.5 MB file
   with ~580k flat comment siblings needed ~1.7e11 iterator steps and
   hung extraction past the supervisor's quiet-timeout. All six descent
   sites (including the default loop) now collect children linearly with
   a TSTreeCursor (wd_collect_children); small nodes keep the direct
   index path. Guard: extract_wide_flat_file_is_linear (400k comment
   siblings — the monster's exact shape — bounded at 30s).

2) The sequential cross-LSP driver handed the FULL def list to a full
   per-file registry build+finalize — O(files x defs); 74% of samples
   sat in build_qn_index. The parallel resolve worker's dispatch
   (module-def filter -> shared prebuilt registries -> filtered
   fallback) is now extracted into cbm_pxc_dispatch_file and drives
   BOTH pipelines — one path, one semantics. The shared registries
   live in a caller-owned arena that outlives the calls pass
   (resolved_calls borrow registry strings; freeing at pass end was a
   use-after-free ASan caught in pass_calls). Rust is exempt from the
   def filter: Cargo-manifest cross-crate resolution needs defs from
   OTHER workspace crates that the own+imports filter drops (this
   starvation predated the unification on the parallel path). Guard:
   pipeline_seq_ts_cross_uses_shared_registry (full builds 40 -> 0,
   cross-file edge preserved).

3) The crash supervisor's recovery re-ran the worker SINGLE-THREADED to
   keep one exact marker. At scale that fell into the sequential crawl,
   was killed as a hang mid-pass, and the stale extraction marker got
   four innocent files quarantined, one 15-minute retry at a time.
   Recovery (and the terminal partial run) now re-run PARALLEL; the
   marker is an append journal ("S <rel>" / "D <rel>", written by
   extraction and both resolve drivers), the suspect set is the open-S
   in-flight set, and a file is quarantined only when it recurs across
   two consecutive failed runs — one per round, oldest open S first;
   disjoint consecutive sets stop the loop rather than blame an
   innocent. Guard: index_recovery_parallel_quarantines_crasher
   (single-threaded spawn count must stay zero; verified RED against
   the old loop).

Also: parse_ts_type_text gains a dynamic work budget proportional to
the input (1M + 64x source bytes, CBM_TS_TYPE_BUDGET override, warn
once then degrade to unknown) replacing unbounded recursion cost.

Measured on microsoft/TypeScript (81,398 files): infinite -> 33s cold
full index, 295k nodes / 779k edges, zero dangling edges, zero files
skipped — the monster file itself now indexes cleanly. Full 9-language
sweep green with no quality flags; kernel unchanged at 390s / 8.5M
nodes / 0 dangling.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-07 10:34:06 +02:00
Greg Tiller 7b7e05652b fix(extract-rust): mark #[test]/#[tokio::test] fns is_test so they are filtered
Inline Rust test fns (#[test], #[tokio::test], #[async_std::test],
#[actix_rt::test], #[test_case::case]) inside a regular .rs file were indexed
as ordinary Functions (is_test=false) and leaked past the store.c is_test!=1
filter into graph/agent context — test detection was file-path-based only
(cbm_is_test_file: *_test.rs / test_*). Add rust_def_is_test(decorators)
(mirroring rust_cfg_qualified_name's scan) and set def.is_test in
extract_func_def's Rust branch.

Closes #855.

Signed-off-by: Greg Tiller <tiller@dal.ca>
2026-07-04 14:22:13 -03:00
Hitoshi Okazaki 60d0855541 fix(extract): flag TS/JS member calls with non-this receivers as method calls
A JS/TS/TSX member call `x.foo()` whose receiver is not `this`/`super` now
sets CBMCall.is_method, mirroring the existing Perl arrow/method flag. This
lets the call-resolution passes recognise a member call whose receiver type
the TS-LSP could not resolve, so they can decline to bind it to an unrelated
project symbol by a weak short-name guess (#592/#606; precedent PR #477 for
Perl).

`this`/`super` receivers are left unflagged — their target is the enclosing
class, where a namespace-proximity weak match is usually right. Bare calls and
new-expressions have no member receiver, so they stay is_method=false; every
non-TS/JS/Perl language is byte-identical (the struct is zero-init).

The pre-#592 test that asserted a JS member call never sets is_method inverts;
it is replaced by TS and JS flagging tests plus a Go test that keeps the
"flag-exempt languages are unaffected" contract, and the cbm.h field comment
is updated to describe both the Perl and TS/JS semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hitoshi Okazaki <okazaki.hitoshi@tecnos.co.jp>
2026-07-04 12:37:17 +09:00
Martin Vogel 87091edd24 fix(extract): receiver-aware self-recursion detection
The recursion detector flagged any call whose short name matched the
enclosing def as self-recursion, so super().save() inside save,
axios.get() inside get, and console.error() inside error were all
false positives feeding is_recursive, recursion_in_loop and
unguarded_recursion.

Add is_self_receiver() and AND it into the short-name comparison: a
qualified callee only counts when its whole receiver chain (everything
before the last '.') names the same object — self/this/cls/@self, or
the enclosing def's own receiver identifier parsed from
CBMDefinition.receiver (Go: s in 'func (s *Store) save()'). Bare names
keep the prior behavior. Matching the full chain keeps self.obj.recur()
(a field's same-named method) out, and the dynamic receiver whitelist
keeps Go s.save() detected.

Distilled from PR #699 with two corrections: the receiver chain is
matched via the last dot instead of the first segment (self.obj.recur
was still a false positive), and the enclosing-receiver whitelist so
Go method self-recursion is not lost.

Closes #599

Co-authored-by: Gen Li <lg320531124@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 20:20:49 +02:00
Martin Vogel 174e56b447 fix(extract): heap-allocate walk_defs frame stack — fix overflow + >4096 truncation (#668)
DCO / dco (push) Has been cancelled
walk_defs used a fixed `walk_defs_frame_t stack[4096]` — a single ~160 KB frame on
the C stack. Two problems:
- On small thread stacks (notably the pre-2026-03 Windows 1 MB main thread) that
  huge frame overflowed the stack during the definitions pass — the 0xC00000FD
  crash a reporter hit indexing a large SQL file (#668). Reproduced locally by
  running extraction under a constrained stack (~208 KB) + ASan.
- The `top < 4096` push guards SILENTLY DROPPED every top-level definition past
  4096, so a file with more than 4096 top-level defs was only partially indexed.

Replace the fixed array with a growable heap stack (wd_stack_t + wd_push): a
256-frame initial buffer that doubles on demand, bounded by a generous,
env-configurable ceiling (CBM_WALK_DEFS_MAX, default 8M frames) that WARNs once
instead of dropping, and returns cleanly on realloc OOM (no NULL deref). A normal
file now uses ~10 KB of heap instead of 160 KB of stack, and files with thousands
of top-level defs are fully extracted. The frame stack is a pure index-addressed
LIFO (frames copied by value on pop), so heap-backing is semantically identical.

Reproduce-first: walk_defs_no_truncation_over_4096_issue668 — 5000 top-level defs
must all be extracted (RED on the old 4096 cap, GREEN after). Full suite 5748/0,
no ASan/UBSan reports on the new alloc.

Root cause was pinned by reproduction (constrained-stack + ASan), refuting earlier
grammar/scanner hypotheses — extraction and the vendored tree-sitter tree ops are
otherwise iterative. The separate error-recovery assertion mode
(symbol<token_count) was not locally reproducible and is addressed by the
forthcoming general crash-containment layer.

Refs #668

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-02 18:39:17 +02:00
Martin Vogel b3b3a48998 fix(fqn): directory-based module QN for Java/Go (de-double) + nested-class enclosing
Java/Go QNs doubled the top-level class/package (proj.Outer.Outer.run; Go module
included the filename). Add cbm_fqn_module_source_lang / cbm_fqn_compute_source_lang
(dir-based module for Java/Go; legacy cbm_fqn_compute UNCHANGED for import paths),
route the single authority cbm.c:569 result->module_qn + the def/calls/pipeline
module derivations through them. Also fix unified compute_class_qn to prefix the
enclosing class for nested classes (was ignored) so def-QN == LSP caller_qn ==
calls-enclosing → lsp_outer_dispatch joins. Reproduce-first tests added; legacy
test_fqn.c/test_registry.c (generic helper) preserved. Per external second-opinion plan.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 21:12:07 +02:00
Martin Vogel aff9bfbbaf fix(test): resolve 18 make-test regressions from the marathon batch
make-test ran for the first time after the lint gate cleared and caught 18 failures:
- REVERT #521 YAML string_ref skip (extract_unified.c): it broke K8s manifest
  resource/infra extraction (7 tests) and didn't fix its own repro. repro_issue521
  returns to RED (1 repro vs 7 regressions); #523/#520 kept.
- REVERT c_lsp.c new_expression ctor->class-node change: existing tests expect the
  synthesized Type.Type ctor QN (3 clsp tests). repro_lsp_c_cpp kConstructor uses an
  explicit ctor + asserts strategy presence only, so it stays green.
- UPDATE tests for INTENTIONAL label changes (production verified correct): struct
  defs now 'Struct' not 'Class' for Rust/Go (test_extraction.c, test_pipeline.c go
  type-classification/grouped/docstring, test_grammar_labels rust golden); Pony member
  funcs 'Method' (golden + probe_pony); PROPERTIES/GraphQL/Prisma now extract
  Variable/Field (goldens + probe_properties).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 16:23:26 +02:00
Martin Vogel 4953fcaf21 test(extract): guard C++ out-of-line method/ctor/dtor enclosing attribution
DCO / dco (push) Has been cancelled
After the C declarator-name walker was de-duplicated into one shared helper
(cbm_resolve_c_declarator_name_node), the C/C++ enclosing-function resolver now
resolves qualified names (Foo::bar) via resolve_qualified_name() and no longer
treats type_identifier as a terminal name. Add reproduce-first coverage so the
#438 fix cannot silently regress on the qualified-declarator path:

- cpp_out_of_line_method_caller_attribution: a call inside `void Foo::bar()` must
  attribute to the method, not the module.
- cpp_out_of_line_ctor_dtor_caller_attribution: calls inside `Foo::Foo()` and
  `Foo::~Foo()` must attribute to the special member, not the module.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-25 21:42:31 +02:00
Kris Kersey 13c3429651 fix(extract): attribute C/C++ CALLS edges to the enclosing function
A CALLS edge whose caller is a C/C++/CUDA/GLSL function was sourced to the
file's Module node instead of the calling Function. "Find callers of X"
returned a file path, outbound trace_path returned empty, and
(:Function)-[:CALLS]->(:Function) queries missed for these languages.

Root cause: the enclosing-function resolvers read only tree-sitter's `name`
field, but a `function_definition` node has none — the name lives in the
declarator chain (pointer/function/parenthesized/array declarators). So
func_node_name() (internal/cbm/helpers.c) and resolve_func_name_node()
(internal/cbm/extract_unified.c) returned NULL, the enclosing scope fell
back to the module QN, and the edge was attributed to the Module node. This
is the C counterpart to #220, which fixed the definition-naming path but not
the enclosing-call path.

Fix: descend the declarator chain to the innermost name node (mirroring
resolve_c_declarator_name in extract_defs.c, including qualified and operator
names) when a function_definition lacks a `name` field. Adds the regression
test c_caller_attribution asserting a C call's enclosing_func_qn is the
function, not the module.

Fixes #438

Signed-off-by: Kris Kersey <kris@kerseyfabrications.com>
2026-06-24 12:42:38 -04:00