mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
12 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e06c2dd13e
|
fix(impact): fail closed on id-less targets and follow ??/||/?: callable values (#3354) (#3373)
* fix(impact): fail closed on id-less targets and follow ??/||/?: callable values (#3354) #3354 reports `impact` returning a byte-identical 1037/CRITICAL/`exact` result for three unrelated targets, with only `target.name` differing. The reporter's `target` had no `id` and no `filePath`, which the traversal path always emits, and a synthetic reproduction of their monorepo (pnpm, Cloudflare worker-configuration.d.ts in five packages, Hono, a Durable Object) resolves every target correctly on main. So the identical result was not reproduced. The repro did surface two real gaps and one hardening point: - `_runImpactBFS` now throws when the target has no node id. Every caller already catches, so impact reports `impactedCount: null, risk: UNKNOWN` instead of a normal-looking blast radius that cannot be about this symbol. - Callable-value flow followed only a single designator on the RHS, so `const sweep = env.__sweep ?? runSweep; await sweep(env)` produced no flow and `scheduled` was missing as a caller of `runSweep`, while the result still claimed `epistemic: exact`. Each branch of `??`, `||`, `or`, and `?:` now flows into the binding (language-neutral: operator and field names, no language checks). Parse cache bumped 104 -> 112 (105-111 are claimed by open PR #3326). - A whitespace-only `target_uid` (strict adapters materialize omitted optional strings) is treated as omitted and falls back to the name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(mcp): treat a non-string uid as omitted instead of throwing (#3354) Review follow-up on #3373. `query.uid?.trim()` called `.trim()` on a client-supplied value, and the MCP envelope is not type-validated, so `context({uid: 42})` threw a TypeError that `context()` does not catch. Before #3373 the same input ended as a structured not_found. A non-string uid now counts as omitted, which matches how normalizeToolParams already treats a non-string `target_uid`. The impact and trace not-found messages print a trimmed uid only when it is a non-blank string, so a strict adapter sending " " sees the name it searched for instead of `' '`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ingestion): expand ??/?:/ternary branches through a provider hook (#3354) Review follow-up on #3373. The shared value-alternatives rule keys on tree-sitter field names, and several grammars spell the same construct differently, so the expansion never fired for them: - Kotlin `elvis_expression` has no fields. - Swift uses `value`/`if_nil` and `if_true`/`if_false`. - Dart uses `first`/`second`, and its conditional has no `condition` field. - Python `a if c else b` has no fields. The `'?:'` operator entry was dead, since no bundled grammar emits it. Add an optional `valueAlternatives` hook to CallableFlowCaptureOptions, consulted before the shared rule, and implement it in the Kotlin, Swift, Dart, Python and Ruby providers. Shared code still names no language. Ruby's statement-bodied `if`/`unless`/`elsif` also carries `condition`/`consequence`/`alternative`, so the shared ternary rule dug an identifier out of an arbitrary statement (`g = h; 0` flowed `h`) and produced a wrong CALLS edge. The Ruby hook now keeps a multi-statement branch as one opaque source, as before #3373. Tests: new provider fixtures for Python, Kotlin, Swift, Dart and Ruby (including a Ruby negative), and TS chain, parenthesized, callable-left and `&&` negative cases. Captures goldens gain one entry each for the new fixtures. SCHEMA_BUMP stays 112 (same unreleased PR). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ingestion): keep long ||/??/?: chains linear in callable-flow capture (#3354) Expanding value-selecting sources into per-branch flows made long chains super-linear and, past a few thousand operands, a stack overflow. A generated `w === "k0" || w === "k1" || ...` keyword table took 6.7 s at 1000 operands and 48 s at 2000, and threw RangeError at 8000. Two causes, both fixed without changing any emitted capture: - valueAlternatives recursed once per operator level and spread the partial results at every level. It is now an explicit stack that writes to one output array. The left-to-right order, the paren unwrapping, and the provider-hook contract (`[node]` means opaque) are unchanged. - Every alternative ran its visibility walks from its own leaf, which can be as deep as the chain is long, all the way to the root. Also, tree-sitter's `parent` re-descends from the root, so each step costs the node's depth. The walks now jump between "anchor" nodes, the only nodes any check can match: region ids and formal owners. The nearest anchor is memoized per node across the file, and parents come from a map recorded by the one DFS the synthesizer already does. After the fix: 0.34 s / 0.23 s / 1.6 s at 1000 / 2000 / 8000 operands. That is within about 1.2x of main without the expansion; what remains is tree-sitter query time. Capture fingerprints are byte-identical before and after the fix for all 16 scope-capture bench languages and for the Python harness. A `typescript-deep-chain` case added to the scope-capture bench guards the scaling: 1.11-1.22 now, 7.5-8.0 before. A unit test pins a 10000-operand chain: it must still yield the seed for a callable operand, the copy for a formal at the deepest leaf, and the invoke. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(kotlin): see through braced if-branches in callable alternatives (#3354) tree-sitter-kotlin wraps a braced branch as `control_structure_body > statements > <expr>`, so for any non-empty block kotlinValueAlternatives saw exactly one named child (`statements`) and pushed the wrapper itself as the branch value. operandSyntax emits nothing for a `statements` node, so `val run = if (c) { ::f } else { ::g }; run()` produced no flow edges, and the "multi-statement block stays opaque" guard could never fire. Descend one level through `statements` and require exactly one non-comment expression there. Empty blocks (`{}` has no named children), multi-statement blocks, and `if` without `else` still return the whole `if` as one opaque source. The doc comment now describes that. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ruby): skip only the multi-statement branch in callable alternatives (#3354) rubyValueAlternatives returned `[node]` (opaque) as soon as one branch held more than one statement. Two things went wrong because of that: - At the top level, `run = if c then g = h; 0 else method(:f) end` dropped the single-statement `else`, so `f` got no flow edge. - In an elsif chain, the outer `if` pushed the `elsif` node as a branch. The shared loop called the hook on it again, got `[elsif]` back, and used the whole elsif subtree as one source. So in `if a then method(:run_a) elsif b then g = h; 0 else method(:run_b) end` the `run_b` edge was lost. The hook now walks the elsif chain itself and skips each multi-statement branch, while every single-statement branch still becomes an alternative. This cannot add a wrong edge: each emitted alternative is a value the conditional really evaluates to, and nothing is taken from the skipped branch. Before, that branch did not contribute a resolvable value either. A whole conditional used as a source becomes a qualified-name seed, which resolves to nothing when it has more than one identifier leaf. With a single identifier leaf, it could even seed the condition variable. When no branch is a single statement, the conditional still stays one opaque source, as before. Ruby captures golden: ruby-callable-alternatives/app.rb goes from 33 to 58 capture groups. 23 of them come from the new fixture functions (checked against the old source). The other 2 are the new branch alternatives, `statement_if -> run_sweep` and `elsif_chain -> run_b`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ingestion): flow the right operand of && / and into callable bindings (#3354) `a && b` / `a and b` yields `a` when it is falsy and `b` otherwise. A falsy value is never a callable, so the right operand is the only one that can be invoked later. The capture left `&&` unexpanded, and the compound source became a qualified seed that resolves to nothing: `const run = x && f; run()` gained no edge to `f` while impact claimed `exact`. Python `x and f or g` reached only `g`. The shared expansion now maps `&&` / `and` to the right branch only. It recurses, so `x and f or g` reaches both `f` and `g`. Where `&&` yields a boolean (Java, C#, Go, Rust, C, C++, PHP, Zig), the destination cannot be invoked, so the flow never meets a call. A before/after CALLS diff over all 83 lang-resolution fixtures that contain `&&` or `and` shows exactly one new edge, logicalAnd -> runAndRight. PHP and Ruby bind low-precedence `and` looser than `=`, so `$g = $x and $y` never reaches this rule. The Python golden digest changes only for the extended python-callable-alternatives fixture. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ingestion): keep operator branches of a value-selecting source opaque (#3354) The fan-out sent every branch of `??` / `||` / `or` / `?:` to emitAssignmentFact on its own, including branches that compute a value. `Handlers.fallback === run || fb` then emitted the comparison as a seed whose qualified text sliced to receiver `Handlers`, member `run`, and resolveBoundMemberCandidates minted a CALLS edge to `Handlers.run`. The same happened for `this.state !== run ?? this.fallback`, `this.state + run || this.fallback`, and Python `self.state != run or self.fallback`. Before the fan-out, the whole compound source was one opaque seed. A branch that is a binary operator expression now contributes nothing. The check uses the field vocabulary valueBranches already reads (a `left`/`right` pair, or an `operator`/`operators`/`op` token after the expression start), not grammar type names. Member accesses that field their `.`/`->` as `operator` (Ruby `call`, C/C++ `field_expression`) also field a member name through the list memberParts uses, now shared as memberNameNode, so they stay designators. Unary `&f`/`*fp` lead with their operator and stay designators. Call results were already dropped by emitAssignmentFact, and lambdas and callable references are unchanged. CALLS-edge diff over 87 lang-resolution fixtures (505 -> 501 edges): only the four false edges above were removed, and none were added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(ingestion): pin the LEFT operand of ?? / or / elvis per provider (#3354) The Python, Kotlin, Swift and Dart "override ?? fn" cases put an unresolvable parameter on the left, so a fan-out that kept only the last operand still passed them. Each fixture now adds a callableLeft case with a real function on the left and a parameter on the right (`run_left or fallback`, `::runLeft ?: fallback`, `runLeft ?? fallback`), and each language asserts `callableLeft → runLeft`. Mutation check: dropping the left branch from the shared `??`/`||`/`or` rule and making the four provider hooks return only their last operand fails all four new tests, while the existing right-operand tests still pass. Capture goldens were regenerated with UPDATE_GOLDEN=1: - python app.py: 35 -> 85 groups. 35 -> 75 is stale drift from |
||
|
|
911151e230
|
fix(resolution): resolve Go pointer-receiver calls, and report the program boundary instead of hedging (#2766) (#2782)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
|
||
|
|
27ab37c432
|
feat(resolution): type receiver chains from AST structure across all 14 languages (#2708) + epistemic lower-bound (#2744) (#2747) | ||
|
|
bc76ba2f25
|
fix(resolution): type inline constructor receivers in every spelling (#2708) (#2737)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* fix(resolution): resolve constructor-expression receivers (#2708) `Service(db).do_work()` emitted no CALLS edge, so the caller was missing from `impact(direction: "upstream")` and `context()` while the two-step spelling of the same call (`s = Service(db)` then `s.do_work()`) resolved. The receiver reaches `resolveCompoundReceiverClass` intact — Case 0 in `receiver-bound-calls` routes it there because the text contains `(`. The free-call branch then only knew one shape: a function whose return-type binding names a class. A class has no return-type binding, so `Service` resolved to nothing and the member call was dropped. Handle the constructor shape: in languages that construct without a `new` keyword (Python, Kotlin, Swift, Scala) a free call naming a class IS a constructor call, so the expression's type is that class. The existing return-type path still runs first and wins, keeping this strictly additive — `new`-keyword languages never reach the new line because their receiver text keeps the keyword (`new Service(db)`), which matches no class binding. Verified on the issue's 4-file repro: `route_inline` now emits `CALLS → Service.do_work` and `impactedCount` goes 1 → 2. Note the issue's second ask — degrading `epistemic` to `lower-bound` when a receiver goes unresolved — is NOT addressed here. `computeEpistemicBoundary` keys only on the target's own heritage edges and runs at query time against the index, while unresolved references live in an in-memory `resolutionOutcomes[]` that is never persisted. That needs unresolved-receiver counts in the index first, so it is left for a follow-up. Tests: new `python-inline-constructor-receiver` fixture plus three integration cases (inline resolves, two-step still resolves, no cross-class fan-out). Two of the three fail without the source change. Full `test/integration/resolvers` suite passes (2928 tests) — the fix is shared across every language, so no-regression coverage matters more than the new cases. Python captures golden regenerated: additions only, no existing digest changed, confirming capture output is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * refactor(resolution): state the construction rule once, cover every spelling (#2708) The first commit fixed `Service(db).do_work()` by special-casing a bare class-name callee inside the free-call branch of the compound receiver resolver. That was the right rule in the wrong place: it covered one surface syntax out of three, and asserted rather than declared which languages it applied to. Probing the same shape across languages showed the bug is wider: | spelling | languages | dropped before? | |------------------------|--------------------|-----------------| | `Service(db).m()` | Python | yes | | `new Service(db).m()` | JS/TS, Java, C# | yes | | `Service.new.m()` | Ruby | yes | | both forms | PHP, Swift, Dart, | no — already | | | Kotlin | resolved | So the rule is stated once — "constructing a class yields an instance of that class" — and the per-language surface syntax is declared through a new `ScopeResolver.constructionSyntax` hook, matching how this file already gates language-varying behaviour (`stripReceiverCastExpressions`, `hoistTypeBindingsToModule`). Shared pipeline code names no language. - `bare: true` — Python - `keyword: 'new'` — JS/TS, Java, C# - `selector: 'new'` — Ruby, including the parenthesis-less `Service.new` spelling that reaches the chain walker rather than the call branch Opt-in is per-language for two reasons. Correctness: `bare` would mistype `stat(&st).field` in C, where a struct and a function may share a name. Evidence: PHP, Swift, Dart and Kotlin resolve this shape already, so they stay unwired instead of carrying a declaration that changes nothing — each verified by diffing analyzer output between builds with and without the change, not assumed. The keyword gate also keeps a bare factory call honest: in a `new` language, `makeOther(db).doWork()` still resolves through the factory's return type and is never read as constructing a same-named class. Tests: TypeScript fixture (inline `new`, a plain `.js` file for the javascript provider, two-step, and the factory guard) and a Ruby fixture (`Service.new` with and without an argument list, plus two-step). With the source change stashed, the inline cases fail and the factory/two-step cases still pass. The Python cases from the first commit are unchanged. No Kotlin fixture: its cases passed without the change, so they would document coverage this commit does not provide. Full `test/integration/resolvers` + `test/unit/scope-resolution`: 4234 passed, 1 skipped. Ruby captures golden regenerated — additions only, no existing digest changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * fix(resolution): only treat a construction selector as construction on the class itself (#2708) The `selector: 'new'` rule fired on any receiver whose type was class-like, which is true both when the receiver IS the class constant (`Factory.new`) and when it is a value of that class (`factory.new`). `isClassLike(...)` cannot tell those apart, so an instance receiver took the construction path too and skipped the member lookup that should have run. That replaced a CORRECT edge with a wrong one. Measured against the base build on a class defining an instance method `new` returning a `Product`: factory = Factory.new; factory.new.run before this PR: Product#run (correct) after this PR: Factory#run (wrong) Track whether resolution currently sits on the class constant or on a value of that class, and apply the selector rule only to the former. The head of a chain is a class constant only when it resolved straight to a class binding rather than through a typeBinding; every hop past it yields a value, so the flag clears. The `obj.method()` branch derives the same fact from whether `objExpr` is a bare name resolving to that class. `Factory.new.run` keeps the behaviour this PR introduced (Factory#run), which is itself a fix over the base build's Product#run. KNOWN LIMITATION, now documented on the contract field and asserted by a test so a future change to it is deliberate: a class-level override (`def self.new` returning another type) is still read as construction. The scope model records no staticness per member, so `def new` and `def self.new` are indistinguishable at this layer; separating them needs the language provider to record staticness first. An earlier attempt to use `TypeRef.source` as a proxy was abandoned after tracing showed Ruby records body-inferred return types as `return-annotation` too, so it does not discriminate. Tests: `ruby-construction-selector` fixture pins all three shapes — class constant, instance receiver, and the documented class-level-override limitation. Ruby resolver suites: 185 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * fix(resolution): resolve generic construction receivers (#2708) `new Box<string>().unwrap()` reached the class lookup as `Box<string>`, which names no class binding, so the member edge was still dropped while the non-generic spelling resolved. `new Foo<T>()` is ordinary in all three keyword-wired languages, so the fix covered a materially narrower slice of real code than intended. Retry the lookup on the base name via `stripTemplateArguments` — the same normalization `resolveClassBindingForName` already applies to typed receivers in the sibling `receiver-bound-calls` pass. The exact-name lookup still runs first, so a class whose name legitimately contains `<` is unaffected. Measured on the probe that first showed the gap: before: | viaGeneric | Class:src/box.ts:Box | (construction edge only) after: | viaGeneric | Method:src/box.ts:Box.get#0 | (member edge resolved) Tests: `viaGenericCtor` added to the typescript-inline-constructor-receiver fixture, asserting both the target file and that the resolved id is `Box`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * fix(resolution): resolve construction in the chain-head position (#2708) `new Service(db).inner.deep()` emitted only the construction edge. The chain walker seeds its starting class from the head segment, which arrives as `new Service(db)` and reduces via `stripCallParens` to `new Service` — no binding and no class of that name, so the walk was never seeded and every segment after it resolved to nothing. Seed the head through the same construction rule the call branch already uses. A constructed value is an instance, so the class-constant flag from the previous commit correctly stays false — `new Factory().new` does not get the selector treatment. The gap was asymmetric across the languages this PR wires: Python's bare form strips to a plain `Service` and was already seeded, so only the keyword languages were affected. Tests: `viaChainHead` added to the typescript-inline-constructor-receiver fixture. Note the fixture annotates `readonly inner: Inner` explicitly — with an unannotated initializer the walk stops at the field, which is field-type inference and a separate concern from head seeding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * fix(resolution): match the construction keyword by token, not by one space (#2708) The keyword form was matched with `startsWith(`${keyword} `)`, so only a single space separated `new` from the type. Any other trivia the source used — a tab, a line break — failed the match and the member-call edge was lost. Match the keyword as a whole token followed by one or more whitespace characters instead. `newService()` still fails the match, which is the point: it is an ordinary call, not a construction, and must keep resolving through its own return type. The keyword is escaped before it enters the pattern. It comes from a language provider rather than from user input, but a keyword containing a regex metacharacter would otherwise build a silently wrong pattern. Tests: tab-separated and newline-separated `new` added to the typescript-inline-constructor-receiver fixture. Note these cases only survive because `gitnexus/test/fixtures/` is listed in the repo-root `.prettierignore` — running prettier from inside `gitnexus/` does not pick that file up and normalizes the tab away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * fix(resolution): resolve qualified construction callees (#2708) `new ns.Service().doWork()` emitted only the construction edge. The call branch splits the callee at its last `.` before construction is considered, so a qualified type name was routed into `obj.method()` resolution as if `ns` were a receiver and `Service` a member. A keyword-marked expression is never a member call, so resolve it as construction before the split. The callee lookup now also handles a dotted name: an unambiguous `qualifiedNames` match first, then the trailing simple name, mirroring how receiver resolution elsewhere in this pass degrades. Measured: before: | viaQualified | Class:src/svc.ts:Service | (construction only) after: | viaQualified | Method:src/svc.ts:Service.doWork#0 | Bare-form qualified construction (Python `models.User(db).save()`) is NOT addressed here: that shape currently emits no edges at all, including no construction edge, so it is a namespace-import resolution gap upstream of this pass rather than a construction-typing one. Tests: `viaQualifiedCtor` added to the typescript-inline-constructor-receiver fixture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * fix(java): drop the unreachable constructionSyntax declaration (#2708) Java was wired `{ keyword: 'new' }`, and the PR described it as one of the languages that needed the fix. Measuring both ways shows it never did: Java resolves `new Svc().doWork()` identically with and without the change, because `java/captures.ts` (#2564) already rewrites an `object_creation_expression` receiver to the constructed type's simple name, so the raw `new Svc()` text never reaches this resolver. The decisive evidence is generics: Java resolves `new Box<User>().doWork()`, which the keyword path could not do before the template-argument fix earlier in this series — the resolution demonstrably comes from the capture rewrite, not from here. Removing the declaration rather than leaving it as defensive configuration: an unreachable per-language opt-in reads as coverage that does not exist, and the contract now records why Java is excluded so the omission is not mistaken for an oversight. Verified after removal: the Java probe still resolves both the inline and two-step spellings, and the Java resolver suites pass (252 passed, 1 skipped). An earlier coordinator measurement in this review claimed Java WAS broken on base; that comparison was invalid (the "without fix" build had not been rebuilt). Corrected here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * refactor(resolution): state the selector rule once and derive its option type (#2708) Two follow-ups from review, no behaviour change (643 resolver tests pass unchanged before and after): The `Class.new` selector rule was written out twice — in the `obj.method()` branch and again in the chain walker — against differently named locals, while the construction helper's own doc comment claimed the rule was stated in exactly one place. Both sites ask the identical question, so they now call one `isConstructionSelectorHop` predicate, and the doc comment says what is actually true. `ResolveCompoundReceiverOptions.constructionSyntax` re-declared the contract's object shape by hand. It was the file's first object-shaped duplicate, and because the value arrives as a non-literal variable, TypeScript's excess property check would not fire: a sub-field added to the contract later would type-check and then be silently ignored here. It is now derived with `ScopeResolver['constructionSyntax']`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * test(resolution): cover the C# construction path and pin the wiring inventory (#2708) Three coverage gaps from review, no behaviour change. C# had no fixture despite being the only keyword-wired language whose behaviour genuinely depends on the construction rule — measured absent on base and present on head. `csharp-inline-constructor-receiver` covers the inline spelling, the two-step spelling, and a static factory that must keep resolving through its return type rather than being read as construction. The TypeScript two-step assertion checked only `toContain('Service')`, and the same fixture defines `LegacyService` — `'LegacyService'.includes('Service')` is true, so the assertion could not distinguish the two targets. It now pins `targetFilePath` the way its sibling assertions already do. Nothing guarded the deliberate opt-in set, so an accidental wiring of a language that already resolves the shape, or a silent loss of one that needs it, would pass the whole suite. `construction-syntax-wiring.test.ts` pins the inventory in both directions: exactly which languages declare `constructionSyntax` and with which spelling, and that java/php/swift/dart/ kotlin stay unwired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * chore(storage): bump INCREMENTAL_SCHEMA_VERSION to 23 for the #2708 edge changes This series changes which CALLS edges are emitted for source whose CONTENT has not changed — inline constructor receivers that previously emitted nothing now resolve, and the Ruby selector fix moves one edge back to the member it always belonged to. That is precisely the class of change the version-history block in this file requires a bump for, and the reuse gate is a strict equality on the persisted stamp. Without it, every existing v22 index passes the gate on the next `analyze` — or is served by the same-commit "already up to date" fast path — and keeps returning the pre-fix graph for unchanged files. `impact(direction: "upstream")` and `context()` would go on omitting the very callers #2708 is about, with no warning, until something unrelated forced a full re-analyze. The fix would have shipped without reaching anyone who already had an index. Precedent is unbroken across the recent resolution PRs: #2723 → v22, #2699 → v21, #2695 → v20, #2563 → v14, each with its own rationale paragraph. This adds v23 in the same form. The pinned assertion in call-summary-schema-version.test.ts moves with it, as that test documents it is designed to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * chore(bench): re-baseline the fixture-corpus fingerprints for #2708 Both bench harnesses fingerprint an entire fixture corpus by directory prefix (`bench/python-scope/measure.mjs:38`, `bench/scope-capture/measure.mjs:76`), so every fixture directory this series adds moves a committed baseline. Neither script writes the baseline itself — running without `--check` only prints, and the file is edited deliberately, which is what its own comment asks for. Regenerated, last in the series so the fixture set was final: bench/python-scope/baseline-fingerprint.txt 36e29abc… -> f120df92… bench/scope-capture/baselines.json ruby 070e4e11… -> fea3edf8… typescript 281e9548… -> cad25be9… csharp e05dc274… -> 05a85bae… CI only ever reported the python drift, because the benchmarks job runs the python step first and aborts there; the cross-language step never ran. Both were verified locally after the update: [measure --check] PASS (capture fingerprint + scaling) [import-target-fingerprint --check] PASS (resolver fingerprint) [scope-capture --check] PASS (15 languages) The `csharp` and `ruby` entries moved because of the fixtures added earlier in this series, not the original ones — a reminder that this baseline moves with any fixture addition, not just the one that first triggered it. Captures goldens regenerated alongside (csharp, ruby); both additive only, no existing digest changed. The python golden did not move: no `python-*` fixture was added after its last regeneration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqnsJK3Cnbu3bjdZbzgMMP * Update tests for passesReuseGate function --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ed8ab1c246
|
fix(scope-resolution): resolve callable reference flows (#2437) (#2522)
Some checks are pending
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* docs(plans): add provider-hook value-refs plan (#2437) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(plans): deepen #2437 plan to USES + property-dispatch design Design revised after prior-art research (Kythe ref vs ref/call, Joern METHOD_REF, Feldthaus field-based call graphs, CodeQL impliedReceiverStep): registration sites emit reference-class USES, invocation is recovered by a field-based property-dispatch pass synthesizing CALLS at member-call sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scope-resolution): model provider-hook value references (#2437) Functions referenced as object-literal property values (provider hooks like emitScopeCaptures: emitCppScopeCaptures) previously produced no edge at all, so impact/context reported a false-safe 0 upstream dependents. Two coordinated halves, per prior art (Kythe ref vs ref/call, Joern METHOD_REF, Feldthaus ICSE'13 field-based call graphs, CodeQL impliedReceiverStep): - Registration -> USES: new ReferenceKind 'value-ref'; TS/JS queries capture pair values and shorthand properties (with @reference.property-key); emitted as a reference-class USES edge, reason 'scope-resolution: value-ref'. Resolution is callable-gated so plain values emit nothing. - Dispatch -> CALLS: new shared pass emitPropertyDispatchCalls synthesizes CALLS (reason 'property-dispatch', confidence 0.7, per-key fan-out cap 32 calibrated on this repo's 16-provider hook tables) from member-call sites to every function registered under the same property key. Deviation from plan: the pass owns value-ref resolution entirely via the post-finalize findCallableBindingInScope walker — the shared registries only see pre-finalize local bindings, so imported hooks (the c-cpp.ts case) were unresolvable through lookupForSite; Reference.propertyKey passthrough dropped as unnecessary. SCHEMA_BUMP 13 -> 14: ParsedFile gains value-ref sites + propertyKey. Verified end-to-end: impact(emitCppScopeCaptures, upstream) now reports 8 impacted / HIGH with extractParsedFile (true dispatch caller) at d=1 via property-dispatch and the c-cpp.ts registration via USES. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(scope-resolution): cover value-ref registration and property dispatch (#2437) Integration: same-file/cross-file/aliased/shorthand registrations emit USES; non-callable and destructuring values emit nothing; dispatch sites gain property-dispatch CALLS (incl. JS twins and per-language partitioning); fan-out-capped keys are dropped entirely; factory-call values unchanged. Unit: capture-shape pins for @reference.value-ref + @reference.property-key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scope-resolution): surface dropped property-dispatch keys in stats (#2437) Review finding: skippedKeys was returned but discarded — a hook table larger than the fan-out cap silently reopened the #2437 gap for those keys. Log dropped keys and fold value-ref USES + dispatch CALLS into referenceEdgesEmitted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(plans): add callable reference-flow implementation plan * fix(scope-resolution): close property-dispatch review gaps * feat(scope-resolution): add callable flow facts * feat(scope-resolution): resolve callable value flow * feat(scope-resolution): resolve callable references across providers * fix: harden callable reference flow resolution * fix(scope-resolution): preserve callable binding semantics * docs(plans): add pr-2522-review-fixes plan Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(storage): bump INCREMENTAL_SCHEMA_VERSION for callable-value-flow edges Callable-value-flow CALLS/USES edges (#2437) can connect two files whose content did not change, but the incremental write set only covers changed files — a top-up against a pre-v7 index would silently omit the new edges for every unchanged file pair, indefinitely. Force the one-time full re-analyze (review finding 1, #2522). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(storage): sanitize callable-flow sites per-site at load, log drops The load-time validator rejected the WHOLE ParsedFile when one site was malformed or over-bound, with no logging — and C++ legitimately emits empty-string parameterTypes entries ('' = unknown, the ReferenceSite.argumentTypes convention) for cv-only/ERROR-recovered types, so real repos fell into a permanent, silent warm-cache-miss reparse loop through the #1983-sensitive main-thread path (review finding 7, #2522). Now: '' entries are valid in type arrays; a malformed/over-bound site drops only itself (counted, warned once per load); only non-array garbage — evidence the serialization itself is untrustworthy — rejects the file. Deviation from plan §6 wording: validator-side tolerance replaces emit-side clamps — smaller diff, same asymmetry closed at the single chokepoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scope-resolution): keep declarations in the union for reassigned callable cells The binding-lookup suppression for fact-constrained cells was wholesale: reassigning a declared function through its own name (greet = other; greet()) deferred the call to the solver, which then refused the lexical lookup that resolves the declaration — an unresolvable RHS yielded zero CALLS for a call that resolved pre-flow (review finding 8, #2522). Suppression now applies only to cells bound by FORMAL facts — its actual purpose (a parameter whose grammar emits no declaration binding must not adopt a same-named outer function). Copy/alias/store/load destinations keep their declaration as an inclusion seed (Andersen-style union). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scope-resolution): count forfeited deferred sites in the budget-bailout warning On work-budget exhaustion the deferred invoke sites end the run with zero CALLS — free-call fallback and reference emission already skipped them — but the warning said 'ordinary graph emission remains untouched', which is false for exactly those sites. The warning context now carries the unresolved deferred-site count and the comment states the real cost (review finding: budget-bailout honesty, #2522). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scope-resolution): surface dropped property-dispatch keys in stats and warn payload The over-cap warning carried only a count; the dropped key NAMES were discarded and RunScopeResolutionStats had no field, so the PR-body claim 'includes them in resolver statistics' was unimplemented (review finding, #2522; reviewer ask on the fan-out cap). The warn payload now names up to 20 dropped keys and the stats carry propertyDispatchSkippedKeys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(scope-resolution): drop producer-less ownerQualifiedName from formal sites No capture emitter anywhere produces @callable-flow.owner-qualified-name — the solver branch consuming it was unreachable in production, yet the field was typed, parsed, validated, and unit-tested with hand-built input (review finding 16, #2522; YAGNI). Re-add with a real producer if C++ qualified member declarators ever need it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(scope-resolution): drop dead callable-flow knobs CallableFlowPassingMode 'callable-object' had no producer and no consumer distinguishing it, and CallableFlowCaptureOptions.extractCallArguments had no language providing it (unlike its live sibling extractCallCallee) — review finding 17, #2522 (YAGNI). The invocation-kind 'callable-object' is a different, live concept and stays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): bind subscripted callable cells to the container, not the index terminalIdentifier iterates children in reverse, so tbl[i] = handler seeded the INDEX variable's cell (polluting a same-named formal) and tbl[i](7) looked up the callee under i in a different scope — no join, no CALLS edge for the classic function-pointer-array dispatch (review finding 12, #2522). Subscript nodes now recurse into their container field only, in both bindingIdentifier and terminalIdentifier, across the fielded grammars (C/C++/JS/TS/Python/Go/Java). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): make cross-function file-scope callable bindings resolvable Two stacked gaps killed the canonical C callback-registration pattern (fp assigned in init(), called in run()) — the exact #2437 false-safe this PR exists to fix (review finding H1, #2522): 1. isVisibleValueBinding only consulted assignment regions and formals, so a call in a function OTHER than the assigning one emitted no invoke fact. A declared callable-typed binding is now a value binding wherever its declaration is visible (visibleCallableSignature). 2. The C scope query had no @declaration.variable pattern for function- pointer declarators — void (*fp)(int); created no scope-tree binding, so the seed (init) and invoke (run) cells canonicalized to different keys and never joined. Both bare and initialized forms now bind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(c): detect variadic parameters via the named variadic_parameter node tree-sitter-c materializes '...' as a named variadic_parameter node; the anonymous-token checks never matched, so variadic function-pointer signatures were emitted with a wrong fixed arity and no '...' sentinel (review finding, #2522). C++ is unaffected ('...' stays an anonymous token there); the token checks remain for such grammars. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): emit invoke facts for field-stored callable member calls The C ops-vtable pattern (o->run = handler; o->run(1)) captured the store but never the call — the member path in emitCallFacts bailed for languages without protocol methods, and the value-binding index recorded the member store under the OBJECT's name ('o'), not the member's ('run') (review finding 11/M3, #2522). Member destinations now also record their terminal member name, and a member call whose name-cell has a visible store emits an indirect invoke — gated on the store so plain accessor calls (map.get) stay inert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): disambiguate (obj->*ptr)() ERROR recovery by token order tree-sitter-cpp groups the recovered '->*' two ways depending on error-recovery cost (identifier lengths): [identifier, ERROR '->*m'] or [ERROR 'obj->*', identifier]. The recovery assumed the first shape, so the second silently swapped receiver/member and dropped the call site — the committed test passed only by name luck (review finding H2, #2522). The identifier's position relative to '->*' inside the ERROR now decides roles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): class members are never file-local in hasFileLocalCallableLinkage The name-keyed file-local set is populated from every static declaration, so an in-class 'static void make();' (external linkage — in-class static means no-instance) and any member sharing a name with a static free function were over-marked, refusing legitimate cross-file declaration/definition joins (review finding 13/M2, #2522). Method and Constructor defs now bypass the name-set, per the hook's own linkage-only contract. Deviation from plan step 13: the regression is a unit-level contract pin rather than an end-to-end join test — C++ merges out-of-line member definitions onto the member node by qualified identity, so the graph shape cannot discriminate the join refusal for members. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): classify parameter passing mode from the declarator chain only A whole-subtree scan for reference_declarator inverted copy vs alias: void reg(void (*cb)(int& out)) marked the by-value pointer cb as 'reference' because of the NESTED parameter's int&, making the solver back-propagate formal targets into every caller's argument cell — alias semantics for a copy (review finding 14/M5, #2522). The chain walk never descends into nested parameter lists; a reference anywhere ON the chain (int& x, void (*&cb)(int)) still aliases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ruby): bare identifiers are calls, not callable references Ruby parses a receiver-less zero-arg method call identically to a variable read, so 'action = process' — which CALLS process and stores its return — seeded action with the callable and minted a wrong CALLS edge from any dispatch through it, confirmed end-to-end (review finding 15/HIGH, #2522). New provider knob bareNamesAreCalls: a bare name that is not a provably local value binding and not an explicit reference form (method(:x), lambda/proc) emits no flow fact, on both the assignment and argument paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(go): pair multi-value := positionally instead of cross-wiring The shared field fallback took the FIRST LHS identifier and the LAST RHS identifier of Go's expression_list pair, cross-wiring 'a, b := f, g' and synthesizing a garbage comma-joined qualified name — the real relationships were silently dropped (review finding 16, #2522). extractAssignment may now return multiple pairs; Go pairs list entries positionally and emits nothing for a length mismatch (multi-return call RHS). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(java): drop get/test from callableProtocolMethods 'get' and 'test' collide with ubiquitous non-functional-interface APIs (Map/List/Optional/Future.get), so every ordinary container access emitted a spurious callable-object invoke fact — high-volume misleading graph facts with a cross-wiring risk on receiver-name reuse (review finding 17, #2522). Supplier.get/Predicate.test dispatch is deliberately traded away until the check can gate on the receiver's declared type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(rust): pin the qualified-name no-degrade guard as a hard invariant Rust's scoped_identifier callable-reference capture over-includes unit enum variants and associated constants (Shape::Square seeds as if callable); they stay edge-free only because resolveSeedCandidates refuses to degrade an unresolved qualified name to a simple-name lookup (review finding 18, #2522). Capture-side type filtering would false-negative on tuple-variant constructors, so the guard IS the contract: documented as a hard invariant (Go's mis-shaped multi-value forms also rely on it) and pinned end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(php): remove nonexistent optional_parameter node type tree-sitter-php has no 'optional_parameter' — defaults ride on simple_parameter — so the entry was dead weight the #1920 literal gate does not cover for capture-option Sets (review finding 19, #2522). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cobol): detect procedure pointers on fixed-format sources Two stacked defects made the feature a no-op on classic sequence-numbered fixed format (review finding 20/H3, #2522): 1. parseDataItemClauses' USAGE alternation knew POINTER but not PROCEDURE-POINTER/FUNCTION-POINTER, so the dataItems filter was dead. 2. The raw-line fallback scanned UNCLEANED text, where the sequence number satisfied the leading digits and the LEVEL NUMBER got captured as the pointer name. It now scans preprocessed lines and requires a letter- initial name (COBOL data names must contain a letter). 161 COBOL preprocessor/copy-expander tests stay green; free-format matrix case unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cobol): skip comment lines in SET seed/copy scans A commented-out SET (indicator-column '*'/'/' or free-format '*>') produced a live seed and a false CALLS edge from dead code (review finding 21/M1, #2522). The scan now skips indicator-column comment lines and strips inline '*>' tails before matching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(architecture): document callable-flow-only mode and skipped-key reporting The Callable-value flow section omitted scopeResolutionEdgeMode: 'callable-flow-only' — a real emit-pipeline branch that suppresses all ordinary emission for standalone providers (review finding 22, #2522) — and predated the skipped-key names/stats surfacing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(scope-resolution): correct value-ref resolution attribution and stale pdg-gating comments The value-ref contract comment claimed MethodRegistry resolution — the mechanism is the post-finalize findCallableBindingInScope walker owned by emitPropertyDispatchCalls (resolveReferenceSites skips these sites). Three 'only under --pdg' calleeIdSink comments were falsified by the #2437 gating change (callee-id-sink.ts's header was updated; these copies were missed). Review finding 23, #2522. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(ingestion): direct unit coverage for synthesizeCallableFlowCaptures The 1,100-line shared synthesizer had no test naming it — only downstream consumers were covered (review finding 24, #2522). Pins seed/invoke/ formal/argument emission, subscript container binding, store-gated member invokes, produced-value guards, and the bareNamesAreCalls knob over a minimal options object so assertions target the synthesizer's own semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(resolvers): deepen shallow-language coverage; fix Kotlin/Swift reassignment gaps it exposed Adds the COBOL SET x TO y copy-branch scenario and conditional-assignment scenarios for Kotlin, C#, Swift, and Dart (10 languages previously had one generic case each — review finding 25, #2522). The new scenarios exposed two real capture gaps, fixed here: - tree-sitter-kotlin's 'assignment' node is fieldless, so nested reassignments (chosen = ::target inside a block) produced no flow facts; Kotlin's extractAssignment now decomposes it positionally. - tree-sitter-swift fields its assignment as target:/result:, neither in the shared fallback's field lists; both added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(infra): literal-validation gate for callable-capture option Sets The #1920 gate validates query literals and exported configs but not the module-private *_CALLABLE_CAPTURE_OPTIONS Sets consumed by the shared synthesizer — a typo'd node type silently captures nothing (PHP shipped a dead 'optional_parameter'; review finding 26, #2522). Every <key>NodeTypes Set literal is now validated against its language's grammar; name-carrying sets (callableProtocolMethods, memberPointerOperators) are deliberately outside the contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(storage): centralize corrupt-fixture casts into makeStoreEntry The callable-flow store tests scattered 'as unknown as' double-casts per fixture (review finding 27, #2522; standing no-as-any rule). One typed helper now owns the single controlled escape hatch for building malformed serialization-boundary payloads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(bench): refresh capture fingerprints after review fixes python-scope: the committed baseline (8d5c3699) never matched this branch's code — CI's benchmarks arm was red on the PR head (review finding 2/HIGH, #2522); regenerated (a99e69ab), scaling 1.04 in budget. scope-capture: ruby/cpp/swift/java/kotlin drifted from the review-fix commits (bare-name suppression, passing modes + ->* recovery, assignment fields, protocol narrowing, positional assignment); all 14 languages re-verified PASS with ratios <= 1.18 against the 1.5 budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(docs): untrack docs/plans working documents docs/ is gitignored (local working docs); the plan files were force-added past the ignore. Untracked from the index only — they stay on disk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(golden): regenerate captures goldens after callable-flow review fixes The per-language digest guards (csharp/go/php/python/ruby/rust/swift) locked the pre-fix capture output; the review-fix series intentionally changed it — store-gated member invokes, subscript container binding, Ruby bare-name suppression, Swift assignment fields, positional pairing. Regenerated with UPDATE_GOLDEN=1; clean verification run 59/59; all other parity/golden guards (pipeline-graph, spring-route, python parity) pass untouched at 33/33. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): prototypes are callees, not callable value cells The cross-function visibility fix indexed EVERY signature-bearing declaration as a value binding — including plain function/method prototypes (void f(int);). Every call to a declared function then became an indirect invoke, and with emitCanonicalInvokeReference (C/C++) minted a free-call reference that resolved through the registry, bypassing the precise passes' two-phase/ambiguity/subobject suppression — eight phantom CALLS edges in the cpp resolver suite on CI. Only declarations whose binding identifier sits under a pointer/ parenthesized declarator (callable-typed variables like void (*fp)(int);) create value cells now. cpp resolver suite 331/331; callable-value-flow + C/C++ suites 181/181 (the cross-function fp regression still passes); cpp fingerprint rebaselined, both bench gates PASS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
560291ad6e
|
fix(ingestion): qualify Ruby same-tail nested mixin modules + route IMPLEMENTS by scope (#1991) (#2006)
* fix(ingestion): qualify Ruby same-tail nested mixin modules + route IMPLEMENTS by scope (#1991) A Ruby `module` maps to the Trait label but is not a typeDeclaration, so the structure phase never qualified its node id: two same-tail nested mixin modules (App::Loggable / Web::Loggable) collapsed onto one Trait:f.rb:Loggable node and the bare-name `include Loggable` cross-wired IMPLEMENTS (first-wins tail). Structure phase: expose buildQualifiedName as a `qualifyScopeName` ClassExtractor hook and thread it for Trait nodes in parsing-processor + parse-worker (lockstep), so a module node keys by its qualified scope path (App.Loggable). Not Option A — `Trait` is not in CLASS_LIKE_LABELS and the qualified-id selection gates it out; qualifyScopeName bypasses the typeDeclaration gate that makes extractQualifiedName bail on modules. getQualifiedOwnerName also falls back to qualifyScopeName so methods inside a nested module own through the same qualified Trait id (no dangling HAS_METHOD). Resolution: emitRubyMixinEdges resolves a bare mixin reference lexically by the including class's enclosing scope (`App::S` + `Loggable` -> `App::Loggable`), and the simple-tail fallback is now delete-on-collision (refuse to guess on a same-tail tie) instead of first-wins. New single-file fixture + tests: two distinct Trait nodes, S IMPLEMENTS App.Loggable only, T IMPLEMENTS Web.Loggable only, no dangling HAS_METHOD; both resolver legs + worker path. Module->Trait preserved; Trait NOT added to CLASS_LIKE_LABELS. ruby-captures-golden regenerated additively. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ingestion): single-source the Ruby Trait scope-label predicate; regen ruby bench baseline (#1991) F5 follow-up to #1991: replace the four hardcoded `nodeLabel === 'Trait'` checks (two each in the sequential parsing-processor.ts and worker parse-worker.ts definition paths) with a single isQualifiableScopeLabel() in ast-helpers.ts so the lockstep paths can't drift. Value-identical predicate — no behavior change. Also regenerate the ruby scope-capture bench baseline: #1991 added the ruby-nested-mixin-tail-collision fixture (and updated the ruby captures-golden), but the bench baseline was never regenerated, so the order-independent fingerprint drifts (bf6b13a -> f0d9b4c6, fixture_count 85 -> 86). Pure fixture-corpus drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
083aedbc41
|
refactor(ingestion): delete legacy call-resolution DAG + heritage processor (RING4-1, #942) (#2023)
* refactor(ingestion): delete legacy call-resolution DAG + heritage processor (#942) RING4-1: all 16 production languages (incl. Vue #940) are registry-primary, so the legacy resolution legs only ran under the now-removed CI parity gate. Calls and inheritance now resolve exclusively through scope-resolution (Registry.lookup, preEmitInheritanceEdges, emitHeritageEdges, buildMro → MethodDispatchIndex). Removed: - Call-resolution DAG: call-processor.ts legacy body (processCalls, processCallsFromExtracted, resolveCallTarget + all resolver/dispatch/chain helpers), model/resolve.ts MRO-via-HeritageMap, model/heritage-map.ts, type-env DAG types; inferImplicitReceiver/selectDispatch LanguageProvider hooks + Ruby impls; DispatchDecision/ImplicitReceiverOverride/ReceiverEnriched. - Legacy heritage path: heritage-processor.ts, heritage-types.ts, heritage-extractors/, @heritage.* tree-sitter queries, heritageExtractor/ heritageDefaultEdge/interfaceNamePattern wiring, worker + parse-impl heritage passes (parse-worker/parsing-processor lockstep), cross-file-impl DAG pass. - Scope-parity infrastructure entirely (no legacy↔registry parity left to run): scripts/run-parity.ts, scripts/ci-list-migrated-languages.ts, ci-scope-parity.yml, test:parity, and the scope-parity ci.yml gate. Resolver integration tests still run via the normal tests job. Kept (shared infra, NOT call-DAG-only): type-env.ts buildTypeEnv (field extraction / structure phase / embeddings), model/resolve.ts c3Linearize + gatherAncestors (mro-processor mroPhase), route/fetch/exported-type-map helpers in call-processor.ts, preEmitInheritanceEdges (legacy-edge dedup simplified). Acceptance: grep for resolveCallTarget/inferImplicitReceiver/selectDispatch/ buildHeritageMap/HeritageMap/processHeritage/heritageExtractor/@heritage. is zero across src + test. tsc clean (both packages); resolver integration suite green (bit-compatible EXTENDS/IMPLEMENTS/CALLS); scope-capture fingerprints unchanged (python re-baselined: removed redundant ignored captures). ARCHITECTURE.md updated to scope-resolution-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): apply autofix feedback (#942) ce-code-review autofix pass on the RING4-1 deletion: - parse-cache.ts: bump SCHEMA_BUMP 2→3 — ParseWorkerResult lost its `heritage` field, so stale on-disk caches must invalidate (prevents a rollback replaying a heritage-less cache into legacy code) [api-contract P2]. - parse-impl.ts: drop 3 now-unused type imports (ExtractedCall, ExtractedAssignment, FileConstructorBindings) left by the deferred-block removal — would fail the eslint CI gate [correctness+maintainability P1]. - AGENTS.md / CLAUDE.md / scope-resolver.ts contract doc: fix stale pointers to the deleted "§ Call-Resolution DAG" section + removed hooks; preserve the language-neutrality rule [project-standards P1]. - registry-primary-flag.ts / cross-file.ts / parse-impl.ts: refresh stale comments referencing deleted symbols (legacy DAG, runCrossFileBindingPropagation). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ingestion): remove the vestigial isRegistryPrimary flag (#942) With the legacy call-resolution DAG deleted, the per-language `REGISTRY_PRIMARY_<LANG>` / `isRegistryPrimary` / `MIGRATED_LANGUAGES` flag had only one meaningful state — every production language resolves via scope-resolution — and an explicit `=0` override could only *disable* resolution with no fallback (a footgun the review flagged). Removing it. - Delete `registry-primary-flag.ts` and the now-dead `shadow-harness.ts` (legacy↔registry shadow-parity tool) + its test. - Collapse the three flag gates to their behavior-preserving outcome (`SCOPE_RESOLVERS == MIGRATED_LANGUAGES`, so this is a no-op): - scope-resolution phase now runs for every registered `SCOPE_RESOLVERS` entry (was `∩ MIGRATED_LANGUAGES`). - import-processor `addImportGraphEdge` + parse-impl `shouldAccumulate`: the legacy emit/accumulate paths were already inert for migrated languages (scope-resolution owns IMPORTS via the imports-to-edges bridge); drop the flag term. - Collapse flag-branching tests to the scope-resolution path and delete the csharp legacy-`=0`-leg describe blocks; remove the ruby/rust-scope env-forcing hooks (no-ops now). - Refresh docs/comments (ARCHITECTURE.md "one registration", scope-resolver cookbook, phase deps) — adding a language is now a single `SCOPE_RESOLVERS` registration. Verified: tsc clean (both packages); resolver integration tests green (747 assertions across cobol/csharp/ruby/rust/typescript/go, IMPORTS edges intact); grep for the flag symbols is zero across src + test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(format): prettier formatting on #942 changes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): drop legacy heritage-capture tests + re-baseline scope-capture fingerprints (#942) Two CI failures from the #942 cleanup, surfaced by the tri-review + CI: - tree-sitter-languages.test.ts: two tests asserted `@heritage.*` captures (Rust trait-impl, Dart extends/implements/with) that this PR removed. The acceptance grep used `@heritage\.` (with `@`); these reference the runtime capture name `heritage.trait` (no `@`), so they slipped the earlier sweep. Inheritance is now covered by the resolver integration suite. (fixed macos-latest) - Re-baselined the scope-capture bench fingerprints for csharp/rust/ruby/java/ javascript/kotlin (baselines.json) + python (python-scope/baseline-fingerprint.txt). The earlier test-cleanup reworded comments inside the lang-resolution fixture files (Shapes.cs, child.rs, derived.rb, IA.java/Plain.java, Service.js, F.kt, app.py) to scrub deleted-symbol references for the acceptance grep; those are the bench corpus, so capture node positions shifted. Capture LOGIC is unchanged — verified `--check` passes for all 14 langs + python. (fixed benchmarks) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs/chore: scrub remaining REGISTRY_PRIMARY + deleted-symbol references (#942) Tri-review P3 follow-ups (verified): - TESTING.md: rewrite the "Scope-resolution parity" section — the legacy dual-leg (REGISTRY_PRIMARY_<LANG>=0/1) and `npm run test:parity` no longer exist; resolver tests run once on the sole scope-resolution path in the normal tests job. - scripts/bench-scope-resolution.ts: drop the inert `REGISTRY_PRIMARY_PYTHON=1` env set + usage hint (the flag is gone). - ruby/scope-resolver.ts, php/captures.ts: re-point doc-comments off the deleted heritage-map.ts / heritage-processor.ts to the current behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): prettier format + regenerate scope-capture goldens (#942) Two more CI failures, same root cause as the bench re-baseline (the test-cleanup reworded comments in lang-resolution bench/golden-corpus fixtures): - quality/format: prettier on tree-sitter-languages.test.ts (blank line left by the deleted heritage-capture tests) + TESTING.md (the rewritten section). - tests/ubuntu/coverage: `csharp-captures-golden` (and python/ruby/rust) drifted because the edited fixtures feed the per-language capture-golden snapshots too (not just the bench). Regenerated via UPDATE_GOLDEN=1. Verified safe: only the edited-fixture entries changed; csharp `captureGroups` unchanged (38) — digest shifted from comment-position only; capture LOGIC untouched. 1168 scope- resolution tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(resolvers): drop createResolverParityIt wrapper, use vitest it directly The parity-aware `it` wrapper became a no-op when #942 removed the legacy call-resolution DAG (it just returned vitest's `it`). Remove it entirely so the resolver tests call vitest's `it` directly instead of shadowing it with a local `const it` (or `pit`/`rustParityIt`): - helpers.ts: delete createResolverParityIt + its now-unused vitestIt import and VitestIt type. - 16 files: drop `const it = createResolverParityIt('x')` and import `it` from vitest instead. - ruby.test.ts (pit) + rust.test.ts (rustParityIt): rename calls to `it`. - Scrub every comment that described the removed wrapper / dual-mode parity skip / legacy_skip gate (vue-scope, js/ts/dart/php/python headers, rust x2, cpp, swift x4, rust-coverage). Genuine test rationale is kept; only the vestigial two-leg framing is dropped. Accurate "legacy DAG (removed in #942)" historical notes are retained. No fixtures touched (no bench/golden re-baseline). tsc clean; rust+ruby resolver suites green (323 tests, incl. #1992 worker-path parity after a local dist build). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c60ad9f7ab
|
fix(ingestion): fully-qualified nested-type identity for C++/Ruby — structure (#1978) + resolution (#1982) (#1981)
* fix(ingestion): qualify nested-type node identity for C++/Ruby (#1978) Nested types sharing a tail name in one file — C++ `Outer::Inner` vs `Other::Inner`, Ruby `Outer::Inner` vs `Other::Inner` modules — silently merged into a single graph node keyed by the simple tail (`Struct:file:Inner`), cross-wiring their methods/properties onto one owner. Key class-like type nodes (Class/Struct/Interface/Enum/Record) by their normalized fully-qualified path (`Struct:file:Outer.Inner`) instead of the simple name. Gated per-language by a new `qualifiedNodeId` config flag (default false → byte-identical for every other language); enabled here for C++ and Ruby. - class-types.ts / generic.ts: `qualifiedNodeId` flag on ClassExtractor + config - ast-helpers.ts: findEnclosingClassInfo gains an optional getQualifiedOwnerName hook + EnclosingClassInfo.qualifiedClassId, so member-owner edges resolve to the qualified class node id (owner id == node id by construction) - parsing-processor.ts + parse-worker.ts: flag-gated qualified node-id + owner edges on both the sequential and worker parse paths (incl. routed properties) - call-processor.ts: same qualifier in the routed-property pre-pass (lockstep with the worker `kind === 'properties'` block) - configs/c-cpp.ts, configs/ruby.ts: qualifiedNodeId: true Method/Property node ids stay simple-qualified; only type nodes get the qualified id. Deferred to a resolution-side follow-up: Ruby SAME-TAIL routed-property/mixin owner identity under registry-primary (`emitRubyMixinEdges` keys owners by the simple tail name, last-wins); and Rust inherent-impl methods (impl_item is not a typeDeclaration — its #1978 test is describe.skip). Tests: same-tail collision fixtures + #1978 resolver tests for C++/Ruby (positive owner identity, R7), a worker-path parity block, and an unambiguous nested attr_accessor case; the C++ #1975 out-of-line test updated to assert qualified-id distinctness (forward-decl + out-of-line now unify). Verified green on both parity legs, the worker path, and tsc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): scope #1978 resolver tests to registry-primary leg; fix lint - helpers.ts: exclude the new #1978 C++/Ruby resolver tests from the legacy parity leg (LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES). They PASS on legacy too — the fix lives in the SHARED structure phase, not the legacy resolution path — so this is a deliberate registry-primary-only scoping (not a legacy gap), keeping the legacy path untouched and uncoupled from the new node-identity behavior. - rust.test.ts: drop the `eslint-disable vitest/no-disabled-tests` directive. That rule isn't configured in this repo, so eslint errored "Definition for rule 'vitest/no-disabled-tests' was not found" and failed `quality / lint`. The describe.skip needs no disable directive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): satisfy CI for the new #1978 fixtures (format + golden + fingerprint) Adding the {cpp,ruby,rust}-nested-tail-collision fixtures changed the lang-resolution corpus, which the scope-capture golden snapshots and the fingerprint baselines gate on. These are pure fixture-corpus additions — #1978 does not touch the scope-capture phase (captures.ts / emit*ScopeCaptures are unchanged). Verified: the regenerated ruby/rust golden diffs are additive-only (no existing fixture's capture digest changed), so the cpp/ruby/ rust fingerprint drift is solely the new fixtures. - prettier --write test/integration/resolvers/{ruby,rust}.test.ts - regenerate ruby/rust captures-golden snapshots (UPDATE_GOLDEN=1; +1 fixture each) - rebaseline cpp/ruby/rust scope-capture fingerprints (bench/scope-capture/baselines.json) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ingestion): extract shared qualified-name normalizer (#1982) Move normalizeQualifiedName/splitQualifiedName out of class-extractors/ generic.ts into utils/qualified-name.ts so the structure-phase buildQualifiedName, the scope-resolution inheritance resolver, and the per-language capture emitters can all key against ONE normalizer. A raw '::' qualifier must normalize to the exact '.'-joined key the QualifiedNameIndex already holds, or the qualified lookup silently misses (the #1982 resolution-side foundation). Pure relocation — byte-identical function bodies; tsc clean; existing C++ nested-collision tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve same-tail C++ nested-type heritage to the correct qualified node (#1982) Registry-primary C++ inheritance (preEmitInheritanceEdges -> resolveInheritanceBaseInScope) resolved a same-tail nested base by its SIMPLE TAIL with first-wins, so `struct DerivedB : Other::Inner` mis-resolved EXTENDS to Outer.Inner (the wrong sibling; 0 dangling, so undetected). The namespace qualifier was discarded at the C++ inheritance capture. Fix (additive, qualified-first): - ReferenceSite gains an optional `rawQualifiedName`; the C++ inheritance capture emits `@reference.qualified-name` (qualifier-preserving, template-stripped: Other::Inner, ns::Base<T> -> ns::Base) only when the base is qualified, registered as a sub-tag so it can't shadow the `@reference.inherits` anchor. - resolveInheritanceBaseInScope resolves the qualifier against the full-path QualifiedNameIndex FIRST (which already carries Outer.Inner / Other.Inner keys from the structure phase), with progressive-prefix lookup for relative bases and refuse-on-tie, falling through to the existing simple-tail walk on miss — so unqualified bases and the single-candidate cross-file case are unchanged. Registry-primary cpp.test.ts 278/278 (incl. worker-path: rawQualifiedName survives worker serialization). Legacy leg unaffected (207 pass / 71 skip) — the new resolution-side assertions are registry-primary-only via helpers.ts. tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve same-tail Ruby mixin/attr_accessor owners to the correct qualified node (#1982) emitRubyMixinEdges keyed its owner map by the SIMPLE tail (def.qualifiedName split-popped) with last-wins, and the __heritage__/__property__ markers carried only the immediate owner name — so `module Outer; class Inner` and `module Other; class Inner` collapsed onto one `Inner` key and cross-wired their include/attr_accessor edges onto whichever Inner was processed last. Fix (lockstep, full-qualified): - ruby/captures.ts: build the marker owner from the FULL enclosing class/module chain (buildEnclosingQualifiedName walks all ancestors, normalizing the compact `class Outer::Inner` scope_resolution form via the shared splitQualifiedName) so the marker owner byte-matches the resolution def's qualifiedName. - ruby/scope-resolver.ts: key graphIdByName by the full def.qualifiedName instead of the simple tail. Top-level owners/mixins are unchanged (full == simple). Registry-primary ruby.test.ts 142/142 incl. a new worker-path block (the deferred note's duplicate-edge concern: markers survive worker serialization, exactly one HAS_PROPERTY per attr). Legacy leg unaffected (136 pass / 6 skip) — new assertions registry-primary-only via helpers.ts. tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): rebaseline #1982 golden/fingerprint + lint/format sweep Cross-cutting verification artifacts for the #1982 same-tail resolution fix: - ruby capture golden regenerated: ONLY the ruby-nested-tail-collision fixture drifts (+10 capture groups from its new include/attr_accessor + the now full-qualified __heritage__/__property__ marker owner). All other ruby fixtures byte-identical (proves the owner-qualification is localized to nested owners). - bench/scope-capture/baselines.json: rebaseline cpp + ruby fingerprints (the only two that drift; 12 other languages byte-identical). cpp = additive @reference.qualified-name capture; ruby = the localized owner change. Provenance notes record both. scaling linear (~1.0), 14/14 PASS. - generic.ts: drop the now-unused normalizeQualifiedName import (lint error). - walkers.ts / ruby.test.ts: prettier formatting. Verified: cpp 278/278 + ruby 142/142 (registry-primary), both legacy legs clean (skips registry-primary-only assertions), go/java/csharp 542 (cross-language regression — the qualified-first branch is gated on rawQualifiedName, set only by C++, so non-C++ inheritance resolution is unchanged). tsc + eslint(0 errors) + prettier clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve nested Ruby mixin included by short name (#1982) emitRubyMixinEdges keyed graphIdByName by the full def.qualifiedName on the owner side, but the __heritage__ marker carries the mixin target as the bare written name (arg.text). A nested mixin module included by its short name (include Loggable where it is App::Loggable) missed the full-qn map and its IMPLEMENTS edge was silently dropped (0 dangling, undetectable). The shipped same-tail fixture used only top-level mixin modules, so CI stayed green. Add a secondary simple-tail fallback map consulted only when the full-qn mixin lookup misses; owner lookups stay full-qn so same-tail owner disambiguation is preserved. Characterization test + fixture (registry-primary only); golden regenerated additively. Addresses PR #1981 review (4417182679) P1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): normalize qualified Ruby mixin arg in heritage marker (#1982) `include Outer::Mixin` embedded the raw `Outer::Mixin` into the ':'-delimited __heritage__ marker, so the `::` collided with the field separator and emitRubyMixinEdges mis-split it (className became empty), dropping the IMPLEMENTS edge. Normalize the mixin arg via splitQualifiedName(...).join('.') before emit so the marker carries the dotted form, which both parses correctly and matches the mixin def's qualifiedName. Simple names are unchanged (no golden drift). Addresses PR #1981 review (4417182679) secondary R2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve C++ same-tail nested heritage inside a namespace (#1982) A namespace-nested C++ type's scope-model qualifiedName carried its enclosing CLASS chain (A.Inner) but dropped the enclosing NAMESPACE, while the structure-phase graph node is keyed by the full path (NS.A.Inner). resolveDefGraphId's qualifiedKey therefore missed and fell back to simpleKey('Inner'), collapsing same-tail nested bases across sibling namespace members — DB : B::Inner pointed at NS.A.Inner. The shipped fixture was top-level only, so it could not catch this. Fix without disturbing the qualifiedName-keyed resolution index (an earlier attempt that rewrote qualifiedName regressed brace-init / UDC / two-phase namespace resolution): tagNamespacePrefixes records each namespace-nested def's enclosing-namespace prefix on a sidecar field, and resolveDefGraphId retries the node lookup with the namespace-prefixed key before the simpleKey fallback. The helper is language-agnostic (acts only on Namespace scopes) and opt-in — only the C++ provider calls it. Namespaced fixture + sequential & worker tests (registry-primary only). All 280 cpp resolver tests pass; tsc clean. Addresses PR #1981 review (4417182679) P2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): worker-path parity for Ruby mixin IMPLEMENTS + C++ DerivedA (#1982) The Ruby worker-path parity block asserted only attr_accessor (HAS_PROPERTY); add an IMPLEMENTS assertion so a dropped/cross-wired mixin owner on the worker path is caught (the __heritage__ marker owner must survive serialization). The C++ worker heritage block asserted only DerivedB; add a DerivedA assertion with a toHaveLength(1) duplicate guard. Registry-primary only. Addresses PR #1981 review (4417182679) test-coverage gap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): distinct Rust same-tail nested-mod inherent-impl ownership (#1982) Rust methods live in `impl Inner` blocks, and findEnclosingClassInfo keyed the inherent-impl owner by the target's RAW tail (`Impl:lib.rs:Inner`), so two same-tail `impl Inner` blocks under different mods (mod outer / mod other) collapsed onto ONE Impl node and their methods cross-wired. The shipped fixture test for this was skipped/deferred. Qualify an UNSCOPED inherent-impl target by its enclosing `mod_item` scope (`outer.Inner`) in BOTH the owner walk (ast-helpers.qualifyRustImplTargetByModScope) and the Impl-node materialization (parsing-processor + parse-worker, lockstep) so the owner edge and node id agree byte-for-byte. Gated on the Impl label + impl_item + an unscoped type_identifier target — Rust-impl-exclusive, so C++/Ruby and the rust captures golden are untouched; a SCOPED `impl a::Inner` keeps its full raw text (#1975, unchanged). The previously-skipped distinct-ownership test is now active and passing; rust 170/170, cpp+ruby+golden 437/437, tsc clean. Done in-PR at maintainer request (was deferred as a follow-up). Addresses PR #1981 review (4417182679) test-coverage gap R7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ingestion): single qualified-name normalizer + module-scoped Ruby PROPERTY_PREFIX (#1982) Replace cpp/captures.ts's parallel normalizeCppNamespaceQName with the shared normalizeQualifiedName (behaviorally equivalent for C++ qualified-identifier inputs: '::'->'.' with leading/trailing-:: handling; no interior whitespace reaches it). Promote Ruby's PROPERTY_PREFIX to module scope alongside HERITAGE_PREFIX (was function-local — asymmetric with no behavioral effect). Maintainability only; cpp+ruby resolver suites 428/428, tsc clean. Addresses PR #1981 review (4417182679) maintainability item. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf+fix(ingestion): single enclosing-class walk + root-anchored base guard (#1982) U7 (perf): preEmitInheritanceEdges resolved the deriving class AND resolveQualifiedInheritanceBase re-walked findEnclosingClassDef for the same site. Resolve callerClass once and thread it into resolveInheritanceBaseInScope -> resolveQualifiedInheritanceBase -> enclosingScopeSegments, so the enclosing class is walked once per qualified site. Add a 'program' early-exit to buildEnclosingQualifiedName (ruby/captures.ts). Behavior-preserving. U8 (P3): a root-anchored C++ base ": ::A::Inner" names the GLOBAL type, but resolveQualifiedInheritanceBase prepended the deriving class's enclosing segments and could mis-bind to an enclosing-relative same-path type. Detect the leading "::" on the raw qualifier and try only the root-anchored key. Discriminating fixture + test (registry-primary only). cpp+ruby+rust resolver suites 599/599; tsc clean. Addresses PR #1981 review (4417182679) perf + P3 items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): rebaseline ruby+cpp scope-capture fingerprints for new #1982 fixtures The four new fixtures (ruby-nested-mixin-shortname, ruby-qualified-mixin, cpp-namespaced-collision, cpp-global-base-anchor) grow the lang-resolution corpus, drifting the ruby and cpp order-independent capture fingerprints. Verified purely additive: the ruby captures golden shows only the two new fixtures added (existing byte-identical), and removing the two cpp fixtures reverts the cpp fingerprint to the prior baseline (so the U3/U6/U8 code changes are scope-resolution / behavior-preserving, not capture-emission). measure.mjs --check PASS (14 languages). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(ingestion): prettier-wrap ruby resolver test call (#1982) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5f0d690c60
|
fix(ingestion): materialize graph nodes for scoped class/module/impl declarations (#1975) (#1977)
* test(ingestion): failing target tests + graph-integrity helper for scoped-declaration nodes (U1, #1975) Adds findDanglingEdges() and pipeline-level tests asserting that Ruby namespaced class/module declarations materialize a Class/Trait node with a resolving HAS_METHOD edge. Red by design on the pre-fix base (5 failing) — the fix lands in U2 (shared core) + U3 (Ruby enablement). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): materialize graph nodes for Ruby namespaced class/module declarations (U2/U3, #1975) Widen the Ruby legacy structure query so `class Foo::Bar` / `module Baz::Qux` (name field is a scope_resolution node) match @definition.class/.module as separate top-level patterns. The node is keyed by its full scoped name, which matches the HAS_METHOD owner id that findEnclosingClassInfo derives from the same name field — so the previously-dangling ownership edges now resolve, and distinct namespaces (Foo::Bar vs Baz::Bar) stay distinct nodes (no collision). No change to findEnclosingClassInfo (zero call-resolution blast radius) and no scope-extractor/golden/bench impact — the fix is purely the legacy structure query gate. Finalizes the U1 target assertions to the qualified-name identity. Validated: 134/134 Ruby resolver tests pass on BOTH legs; tsc --noEmit clean; dangling HAS_METHOD edges on the ruby-namespaced fixture drop from 3 to 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve C++ out-of-line nested definition method ownership (U4, #1975) For an out-of-line `struct Outer::Inner { ... }`, the container name is a qualified_identifier, so findEnclosingClassInfo derived the owner id from the full `Outer::Inner` text — but the type is keyed by its in-class declaration (the nested `Inner` node), leaving the method's HAS_METHOD edge dangling. Reduce a qualified_identifier container name to its tail segment for the owner id/name, matching how inline nested definitions are already keyed. Node-type scoped, so Ruby's scope_resolution names stay full (distinct-by-namespace) and no language is named in shared code. Only out-of-line-def methods (already dangling) change behavior — zero impact on bare classes or call resolution. Validated: C++ 268/268 default leg, 205+63-skip legacy leg, no regression; 2 new target tests pass both legs; Ruby namespaced tests still pass; tsc clean; scope-capture bench rebaselined (cpp +cpp-out-of-line-class fixture) — --check PASS (13 langs). Dangling HAS_METHOD on the new fixture: 1 -> 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve Rust scoped impl-target method ownership (U5, #1975) `impl path::Type` and `impl Trait for path::Type` name the target with a scoped_type_identifier. Two coordinated fixes: - findEnclosingClassInfo: reduce a scoped_type_identifier impl target to its trailing type name (both the trait-impl `for` branch and the inherent branch), matching the type's own tail-keyed declaration. - tree-sitter-queries: add a @definition.impl arm for scoped inherent impls so the Impl node is materialized (keyed by the same tail) instead of missing. Together the trait-impl method owns through the real Struct node and the inherent-impl method owns through a real Impl node — no dangling edges. Rust's scoped_type_identifier has a name: field, so the tail extraction is exact. Validated: Rust 163/163 on BOTH legs, no regression; new target test passes; C++/Ruby suites unaffected; tsc clean; scope-capture bench rebaselined (rust +rust-scoped-impl fixture) — --check PASS (13 langs). Dangling 1 -> 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): cross-namespace collision test + regenerate ruby/rust captures goldens (U6, #1975) - Add ruby-tail-collision fixture + test: Foo::Bar and Baz::Bar share the tail 'Bar' but must stay two distinct Class nodes (locks the KTD-2 anti-collision guarantee from full-scoped-name keying). No dangling, no cross-wiring. - Regenerate the ruby + rust captures goldens for the fixtures added in U3-U6 (ruby-tail-collision, rust-scoped-impl). Both diffs are additive-only — a single new entry each, existing entries byte-identical (no capture-logic drift; the fixes are in the legacy structure query + findEnclosingClassInfo, not the scope-extractor). - Re-baseline the ruby scope-capture fingerprint (81->82 fixtures). N/A-language verification: C#/Java/PHP have no class-declaration scoped-name gap and show no regression (606 passed; the 2 C# worker-pool failures are the known worktree 'parse-worker.js not built' limitation, unrelated to this change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert(ingestion): drop C++/Rust scoped-owner reduction; ship Ruby-only (#1975) The self-tri-review of PR #1977 (review 4411683756) found — and reproduced — that the C++/Rust tail-reduction in findEnclosingClassInfo collides same-tail types declared in the same file (struct Outer::Inner + struct Other::Inner -> one Struct:Inner node, methods silently mis-attributed; same-named members merge). Root cause is pre-existing: GitNexus keys nested-type nodes by their tail name within a file, so even plain inline same-tail nested types already merge. A correct fix needs fully-qualified nested-type node identity — a broad change deferred to #1978. This reverts the C++ (qualified_identifier) and Rust (scoped_type_identifier impl) owner reductions in ast-helpers.ts, the Rust @definition.impl scoped arm, and the cpp/rust fixtures+tests+golden+bench entries. The Ruby fix is unaffected (it keys the node by the full scoped text — no collision) and stays: namespaced class/module node materialization + the cross-namespace collision test. Validated Ruby-only: 136/136 both legs; ruby+rust captures goldens 19/19; bench --check PASS (14 langs); tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): collision-safe C++/Rust scoped-declaration node ownership (#1975) Re-introduces the C++/Rust fix the tri-review reverted, using a collision-safe approach instead of owner tail-reduction (which merged same-tail types in one file). Key the scoped DECLARATION's node by its full qualified text so it matches the owner id and stays distinct from a same-tail type elsewhere: - C++: widen the legacy structure query to materialize a node for out-of-line defs (class/struct Outer::Inner — name is qualified_identifier), keyed by the full text. No findEnclosingClassInfo change needed — BASE already derives the full-text owner, which now matches. Outer::Inner and Other::Inner stay distinct; 3-level A::B::C resolves. (A redundant forward-decl node remains.) - Rust: @definition.impl arm for scoped inherent impls (keyed full) + findEnclosingClassInfo inherent-impl branch accepts scoped_type_identifier with full text. impl a::Inner and impl b::Inner stay distinct. Collision-aware fixtures + positive owner-identity assertions (per the tri-review) replace the single-type fixtures. Deferred to #1978: Rust trait impls on a scoped struct path (impl T for a::Inner) and the pre-existing inline same-tail node collision — both need qualified struct-node identity. Validated: Ruby 136/136, C++/Rust 434/434 both legs (371+63-skip legacy); ruby+rust captures goldens 19/19 (additive); bench --check PASS (14 langs); tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(format): apply prettier to scoped-declaration changes (#1975) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6643afbcda
|
fix(ruby): scope-resolution namespaced class/module definitions — F62 (#1933) (#1972)
* fix(ruby): namespaced class/module definition captures — F62 (#1933) * chore(bench): regenerate Ruby golden captures after F62 scope_resolution patterns * fix(ruby): namespaced class/module definition captures — F62 (#1933) * chore: remove unused imports from ruby-namespaced test * chore: add comment about capture-only scope in ruby-namespaced test --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> |
||
|
|
0fc0211d26
|
fix(ingestion): migrate all languages' inheritance to scope-resolution on the worker path (#1951) (#1956) | ||
|
|
d1d2a64d0f
|
perf(ingestion): linearize scope-capture across all languages + Python import resolution (O(n²)→O(n)) (#1918)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* bench(python-scope): build-free measure harness + baseline fingerprint for emitPythonScopeCaptures
ce-optimize scaffolding for the python-scope-capture run. Mirrors the Go
scope-capture harness (#1848): imports the .ts hotpath via tsx, times
emitPythonScopeCaptures on a synthetic DAO source at 250/800 entities, and
pins an order-independent sha256 capture fingerprint over the whole
lang-resolution/python-* corpus + a fixed 20-entity DAO as the correctness gate.
Baseline (current code) is O(n^2): 250->800 entities (3.2x) -> 10.7x time
(1062->11343ms), scaling_ratio 3.34.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* optimize(python-scope-capture): thread captured nodes to kill O(n^2) findNodeAtRange re-walks
emitPythonScopeCaptures re-derived each tree-sitter match's AST node via
findNodeAtRange(tree.rootNode, ...) on every match, scanning all of root's named
children per call -> O(matches x rootChildren) ~ O(n^2). The same #1848 bug Go
had (fixed in
|