From d5e9c8e65090238c59eaed7cb55ba67ba360a014 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Thu, 30 Jul 2026 17:28:49 +0000 Subject: [PATCH] fix(resolution): stop the fold fabricating edges, and harden the epistemic lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses every finding from the multi-engine review of this PR. P0 — THE FOLD FABRICATED A CALLS EDGE. `foldReceiverChain` resolved its base through `resolveCompoundReceiverClass`, whose bare-identifier branch falls through to a plain class-name lookup EVEN WHEN a receiver typeBinding exists but names no class. The dotted-chain head is deliberately narrower — it consults the class namespace only when there is no binding at all. So a local that merely SHADOWS a class name was typed as that class: const Config = make(1); // a number Config.db.query(); // emitted entry -> Database.query A/B, reverting only the fold wiring: with fold: ["entry -> query", "entry -> make", "Config -> Database"] without fold: ["entry -> make", "Config -> Database"] A confident wrong edge, and a direct violation of R4 — the one outcome this whole line of work exists to prevent. Fixed with an opt-in `strictBaseBinding` that makes the fold's base obey the same rule as the cascade's head. The flag is OFF for the cascade, so no existing edge moves; measured, every shape that resolved before still resolves and no other language's drop count changed. P0 — `impact()` INTERPOLATED A FUNCTION INTO USER-FACING TEXT. `summary?.counts?.[symName]` walks `Object.prototype`, so `constructor`, `toString`, `valueOf`, `hasOwnProperty` and friends returned a Function rather than `undefined` — and `NaN <= 0` is false, so the `<= 0` guard let it through: impact({target: "constructor"}) -> "function Object() { [native code] } call sites invoking `constructor`…" plus a bogus `epistemic: 'lower-bound'`. `constructor` is an ordinary member name in a code graph, so this was reachable in normal use. The lookup is now one exported, prototype-safe helper (`lookupUnresolvedCallCount`) that both the backend and its test use, instead of an inline index expression. P1 — BOTH SCHEMA CONSTANTS WERE BUMPED BEFORE THE FINAL DIVERGENCE. `SCHEMA_BUMP` 32 landed at the TypeScript-only commit and `INCREMENTAL_SCHEMA_VERSION` 26 before the fold's semantics were final, but the rollout to the other 13 languages then changed captures AND edges under the same numbers. An index or cache stamped by an intermediate build was therefore treated as current at HEAD while serving pre-rollout captures for 13 languages and the fabricated edges above. Re-bumped to 33 / 27 so each number tracks the FINAL state rather than the first divergence, with the reuse-gate ladder updated to match. P2 — the summary counted property reads and writes as "call sites". It now filters on the `siteKind` this series added for exactly that purpose. On the fixture corpus that is 25 of 124 drops, so names whose CALL count was never short no longer report `lower-bound`, and the consumer's "N call sites invoking X" is now literally true. P2 — two tests did not test what they claimed. The negative assertion named a member (`noSuchMember`) that appeared nowhere else in the repo, so it could not fail; it now uses a chain with a genuinely broken middle step and asserts the tail does not bind while the head still does. And "resolves a mixed call/field chain through the fold" used a shape the pre-existing cascade already handled — it now uses an optional-chained spelling, verified to FAIL with the fold reverted. P3s: stale "NOT WIRED" JSDoc removed (it was wired 71 lines below); the zero-width character class added to `isEncodableSegment` (`\s` misses U+200B-F and U+FEFF, a trojan-source vector that would encode, persist, then silently match nothing); the unreachable `?? ''` replaced; the store sanitizer's doc no longer claims parity with `sanitizeCallableFlowSites` it does not have; the truncation marker documented as a reserved forward-compatible contract rather than live; `--check`'s scope stated plainly so a green run is not read as a performance guard; and the orphaned "current stamp passes the gate" comment and its duplicate assertion removed. Test gaps closed: behavioural proof for a SECOND language (`cpp-receiver-chain-arrow`, the `->` base the rollout actually fixed, with the value-dot control that shows why `cpp-chain-call/` could never catch it); the `hoistTypeBindingsToModule: false` branch that six wired languages run; adversarial payloads through `decodeReceiverChain`; the MAX_CHAIN_DEPTH boundary that must still WORK; and per-field sanitation proven to leave a VALID sibling's chain intact. Every regression test was verified to fail without its fix. 4412 tests green, tsc clean in both packages, all bench gates pass. --- .../bench/receiver-resolution/baseline.json | 10 +-- .../bench/receiver-resolution/measure.mjs | 9 +++ gitnexus/bench/scope-capture/baselines.json | 62 ++++++++++-------- .../passes/compound-receiver.ts | 20 +++++- .../scope-resolution/unresolved-receivers.ts | 35 ++++++++++ .../ingestion/utils/receiver-chain-codec.ts | 17 ++++- gitnexus/src/mcp/local/local-backend.ts | 8 ++- gitnexus/src/storage/parse-cache.ts | 21 +++--- gitnexus/src/storage/parsedfile-store.ts | 12 +++- gitnexus/src/storage/repo-manager.ts | 12 +++- .../cpp-receiver-chain-arrow/main.cpp | 14 ++++ .../cpp-receiver-chain-arrow/models.h | 11 ++++ .../test/integration/resolvers/cpp.test.ts | 30 +++++++++ .../integration/resolvers/typescript.test.ts | 64 +++++++++++++++++-- .../unit/call-summary-schema-version.test.ts | 18 ++++-- .../test/unit/incremental-parse-cache.test.ts | 13 ++-- gitnexus/test/unit/parsedfile-store.test.ts | 25 ++++++++ .../test/unit/receiver-chain-codec.test.ts | 47 ++++++++++++++ .../receiver-chain-fold.test.ts | 26 ++++++++ .../unresolved-receivers.test.ts | 48 +++++++++++++- 20 files changed, 435 insertions(+), 67 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-receiver-chain-arrow/main.cpp create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-receiver-chain-arrow/models.h diff --git a/gitnexus/bench/receiver-resolution/baseline.json b/gitnexus/bench/receiver-resolution/baseline.json index d0c05d498a..904bc658b5 100644 --- a/gitnexus/bench/receiver-resolution/baseline.json +++ b/gitnexus/bench/receiver-resolution/baseline.json @@ -19,20 +19,20 @@ } }, "countArm": { - "callDrops": 99, - "totalDropsAllKinds": 124, + "callDrops": 101, + "totalDropsAllKinds": 128, "bySiteKind": { - "call": 99, - "read": 25 + "call": 101, + "read": 27 }, "callDropsByExtension": { ".java": 49, ".cs": 8, ".ts": 7, + ".cpp": 7, ".tsx": 6, ".py": 5, ".go": 5, - ".cpp": 5, ".php": 4, ".rs": 3, ".kt": 3, diff --git a/gitnexus/bench/receiver-resolution/measure.mjs b/gitnexus/bench/receiver-resolution/measure.mjs index e5611968b1..7a279ea872 100644 --- a/gitnexus/bench/receiver-resolution/measure.mjs +++ b/gitnexus/bench/receiver-resolution/measure.mjs @@ -412,6 +412,15 @@ const KNOWN_BLIND = [ * moves these numbers and requires a rebaseline; that treadmill is the accepted * cost of the guard, the same trade the scope-capture bench already makes. */ +/** + * NOTE ON SCOPE: this is the GATED projection — shape states plus call-drop + * counts. The perf arm (`--perf N`: wall-clock, RSS, bytes/site) is deliberately + * NOT part of it: those measurements need an A/B against a control build, which + * `--check` has no way to construct, so asserting them here would compare against + * numbers from a different machine and fail on noise. The perf figures recorded in + * BASELINE.md are therefore a POINT-IN-TIME MEASUREMENT, not a CI guard — do not + * read a green `--check` as evidence that performance has not regressed. + */ function projection(output) { return { shapeArm: Object.fromEntries( diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index 5c530dbfa2..d0ef387ca3 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -1,11 +1,12 @@ { "_comment": "Per-language baselines for bench/scope-capture/measure.mjs --check. fingerprint = order-independent sha256 over the lang-resolution/-* fixture corpus + a 20-entity synthetic source (correctness gate; re-baseline intentionally on a legitimate capture change). scaling_budget = max allowed (t800/t250)/(800/250); ~1.0 is linear, ~3.2 is quadratic. The synthetic source is now HERITAGE-BEARING for every language (each Entity extends/implements/embeds/uses-trait/conforms-to a shared base) so the #1951 @reference.inherits synth is gated at scale, not just the base capture loop. All languages thread the tree-sitter captured node instead of re-deriving it with findNodeAtRange(tree.rootNode,...) per match, so all are linear (go #1915, python #1918, ruby/php/rust/csharp #1951, java #1956).", "go": { - "fingerprint": "57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a", + "fingerprint": "5d6c59c2f2c0dd937c53bf5d736e0f8376b2899a381e488a33aec23524823efb", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 3d4e32e7490c830516126e28931827949baa3594cb521f7a3d8dcfed95b6018a -> 57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a; scaling 1.058 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: provider-owned callable assignment/copy/formal/argument/invoke facts with invocation/constructor-result suppression. Prior 09ecd94911b830f52fa8807560abcbd79f163d02a2072870c1a59297e9a326e1 -> 3d4e32e7490c830516126e28931827949baa3594cb521f7a3d8dcfed95b6018a; scaling 1.039 < 1.5.", - "_rebaselined": "#1976: F33 generic composite literal constructor inference adds generic_type captures in composite_literal patterns; fingerprint drift expected." + "_rebaselined": "#1976: F33 generic composite literal constructor inference adds generic_type captures in composite_literal patterns; fingerprint drift expected.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a -> 5d6c59c2f2c0dd937c53bf5d736e0f8376b2899a381e488a33aec23524823efb." }, "cobol": { "fingerprint": "d45bb091b0893d0de4fae2486b31ba21719c9377bf35a0908fd3a36fa1c3bf4e", @@ -24,7 +25,7 @@ "_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)." }, "cpp": { - "fingerprint": "a70625bb0a9ef74e760d9d79cc5557485d0f0d3fb935e8a22a0c9556c65b5bb1", + "fingerprint": "7e27aea46f3e17f33c41babbe0ddd982d1ab5920f143864763e0a1c6aef882a5", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature/cv metadata. Prior dde874d2c30bda9f634f9799281a66de800cad9f76cf65e7c31839e2ae9da9ff -> 57860dd2a8d4b06c6d2dd0d854c08b781faee3da8f2b6c42ba0c68a9f70e5ccb; scaling 1.090 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: C++ overload-aware function/reference/member-pointer flow facts with invocation/constructor-result suppression. Prior 3a503a1513e7eede3f7a223dcce0896c06d15bdfa920445224c9025848c0d710 -> dde874d2c30bda9f634f9799281a66de800cad9f76cf65e7c31839e2ae9da9ff; scaling 1.034 < 1.5.", @@ -35,21 +36,23 @@ "_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression). #2094: deleted C++ declarations retain @declaration.is-deleted metadata; deleted operator and pointer-return shapes plus the expanded deleted-overload fixture are included. Intended capture drift; scaling remains linear (1.139 < 1.5).", "_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift \u2014 no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures \u2014 pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture \u2014 pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5). #1899: braced-init call arguments emit a conservative parameter-type capture; fixture_count 277, scaling remains linear (1.141 < 1.5).", "_rebaselined_2522_review_fixes": "PR #2522 review fixes: outermost-chain passing modes; ->* ERROR-recovery role order; member-store visibility. Prior 57860dd2a8d4b06c6d2dd0d854c08b781faee3da8f2b6c42ba0c68a9f70e5ccb -> f29bc3f7b1622954d6f6b7647bc9cf6c7a2629ffcc0fe00ac7918e4925876b65; scaling ratio re-verified within budget.", - "_rebaselined_2522_prototype_value_cells": "Plain function/method prototypes no longer index as callable value cells (only pointer/parenthesized variable declarators do) \u2014 removes the spurious indirect-invoke facts that leaked phantom CALLS past two-phase suppression. Prior f29bc3f7b1622954d6f6b7647bc9cf6c7a2629ffcc0fe00ac7918e4925876b65 -> a70625bb0a9ef74e760d9d79cc5557485d0f0d3fb935e8a22a0c9556c65b5bb1; scaling re-verified within budget." + "_rebaselined_2522_prototype_value_cells": "Plain function/method prototypes no longer index as callable value cells (only pointer/parenthesized variable declarators do) \u2014 removes the spurious indirect-invoke facts that leaked phantom CALLS past two-phase suppression. Prior f29bc3f7b1622954d6f6b7647bc9cf6c7a2629ffcc0fe00ac7918e4925876b65 -> a70625bb0a9ef74e760d9d79cc5557485d0f0d3fb935e8a22a0c9556c65b5bb1; scaling re-verified within budget.", + "_rebaselined_receiver_chain_2747": "#2747: additionally adds the `cpp-receiver-chain-arrow` fixture, the behavioural proof for a `->` BASE receiver (`svc->getUser()->save()`) that the rollout fixed and that `cpp-chain-call/` could never catch because it uses the value `.` form. Prior a70625bb0a9ef74e760d9d79cc5557485d0f0d3fb935e8a22a0c9556c65b5bb1 -> 7e27aea46f3e17f33c41babbe0ddd982d1ab5920f143864763e0a1c6aef882a5." }, "csharp": { "_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.", - "fingerprint": "05a85bae70cf9c94f42459c843cfc36e3e81c872e5dcc7d77bc42fbc390f4bfe", + "fingerprint": "8a282254b93b3ef2ff34c2fdba819ebc95c53c4fcb09942cbad99f96d3687855", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior f31544530924748f9aa37d11cec570bc10c3ddf9d9b237e6df7a17623fd2bb3a -> 75cf380209fa7d1a8a3ec873be1a9424b4e5173be0b08234c2291e8521a9b3c1; scaling 1.061 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: C# method-group/delegate callable flow facts with invocation-result suppression. Prior 2bb5bc8c19cb8eb08c9590545ad8a1968a7152951f7e12746e2d7901d542fed9 -> f31544530924748f9aa37d11cec570bc10c3ddf9d9b237e6df7a17623fd2bb3a; scaling 1.115 < 1.5.", "_note": "#2046: F35 qualified-constructor captures now emit @reference.qualified-name + a simple-name @reference.name on `new Ns.Foo()`/`new A.B.Foo()`; namespace_declaration/file_scoped_namespace_declaration now emit @declaration.namespace name captures (feeding the non-destructive namespacePrefix sidecar for `new B.Foo()` same-tail disambiguation). + csharp-interface-only-base and csharp-namespace-qualified-ctor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.11).", - "_rebaselined_2563_instance_ownership": "#2563: csharp-using-static adds same-file ownership, local-function, overload, partial-class, and cross-namespace same-name coverage. Prior 75cf380209fa7d1a8a3ec873be1a9424b4e5173be0b08234c2291e8521a9b3c1 -> e05dc27456bde8175948586c9e7689033a378fa40e9ca4ce78cce41fbea0f2f8; scaling 1.058 < 1.5." + "_rebaselined_2563_instance_ownership": "#2563: csharp-using-static adds same-file ownership, local-function, overload, partial-class, and cross-namespace same-name coverage. Prior 75cf380209fa7d1a8a3ec873be1a9424b4e5173be0b08234c2291e8521a9b3c1 -> e05dc27456bde8175948586c9e7689033a378fa40e9ca4ce78cce41fbea0f2f8; scaling 1.058 < 1.5.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 05a85bae70cf9c94f42459c843cfc36e3e81c872e5dcc7d77bc42fbc390f4bfe -> 8a282254b93b3ef2ff34c2fdba819ebc95c53c4fcb09942cbad99f96d3687855." }, "rust": { - "fingerprint": "05acbaca48427e0d9e0793bcd0ce4057712d3716b5e7868189c12e05ef8dd300", + "fingerprint": "83812d82f0e2c3eb552f3246381ca3dd5ccd6783d63aba3325f1343e7772280c", "scaling_budget": 1.5, - "_rebaselined_mod_node_identity_2745_review": "#2745 review: added rust-2742-mod-members, rust-2742-nested-mods and rust-2742-type-vs-module under lang-resolution for the container/owner-edge fix, nested inline modules, and the imported-type-vs-module precedence. emitRustScopeCaptures is unchanged — verified by removing ONLY those three fixture dirs and re-running, which reproduces the prior fingerprint exactly, so the shift is purely corpus growth (fixture_count 196 -> 202, capture_groups_fp 3432 -> 3556). Prior 90fda086a4e13aa069a5981f63ed58ab1c71f1ed3da5e1480a080e1992b0d3e5 -> 05acbaca48427e0d9e0793bcd0ce4057712d3716b5e7868189c12e05ef8dd300; scaling 1.022 local / 1.057 CI < 1.5. NOTE for the next fixture author: a new rust-* fixture drifts BOTH this bench baseline and the rust-captures-golden snapshot. Updating only the golden is how this reached CI red.", + "_rebaselined_mod_node_identity_2745_review": "#2745 review: added rust-2742-mod-members, rust-2742-nested-mods and rust-2742-type-vs-module under lang-resolution for the container/owner-edge fix, nested inline modules, and the imported-type-vs-module precedence. emitRustScopeCaptures is unchanged \u2014 verified by removing ONLY those three fixture dirs and re-running, which reproduces the prior fingerprint exactly, so the shift is purely corpus growth (fixture_count 196 -> 202, capture_groups_fp 3432 -> 3556). Prior 90fda086a4e13aa069a5981f63ed58ab1c71f1ed3da5e1480a080e1992b0d3e5 -> 05acbaca48427e0d9e0793bcd0ce4057712d3716b5e7868189c12e05ef8dd300; scaling 1.022 local / 1.057 CI < 1.5. NOTE for the next fixture author: a new rust-* fixture drifts BOTH this bench baseline and the rust-captures-golden snapshot. Updating only the golden is how this reached CI red.", "_rebaselined_dyn_trait_object_2604": "#2604: RUST_SCOPE_QUERY now captures function_signature_item (abstract trait methods, no body) as a scope + declaration, so a &dyn Trait receiver can dispatch a CALLS edge to the trait's own method. Additive capture shift across every bench fixture with a required trait method. Prior df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29 -> f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846; scaling 1.033 < 1.5.", "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c -> df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29; scaling 1.065 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Rust fn-value callable flow facts with invocation/constructor-result suppression. Prior ac610bbe97666bf285923479dd7b43a2fe4c5354aae8df1bcbafdc04fb220f82 -> 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c; scaling 1.024 < 1.5.", @@ -57,32 +60,36 @@ "_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED \u2014 @declaration.macro/@reference.macro + MacroRegistry \u2192 USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures \u2014 pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f.", "_rebaselined_import_disambiguation_2514": "#2514: added rust-import-* and rust-dup-* fixtures under lang-resolution for the range-binding ambiguity latch + import-disambiguated resolution (for-loops / struct destructuring across explicit/aliased/glob use imports). emitRustScopeCaptures is unchanged; the corpus fingerprint shifts purely because the fixture set grew (130 -> 174). Prior f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846 -> 655aed01cf1b6b84fa0c64d48dfb2526ecb67f47d90f0a91edabacd269a212db; scaling 1.06 < 1.5.", "_rebaselined_self_type_binding_2714": "#2714: a Rust `Self` type binding now records the enclosing impl's type instead of the literal 'Self'. `let fresh = Self { .. }` inside `impl User` binds `fresh: User`; recorded verbatim it bound `fresh: Self`, which resolves to nothing. The type-env channel already substituted this (type-extractors/rust.ts findEnclosingImplType); the scope-resolution channel did not, so the two disagreed. The gap was invisible while lookupCore Step 1 still walked the lexical chain for NAMED receivers \u2014 the impl scope binds the method by name, so fresh.validate() resolved by accident \u2014 and became a lost CALLS edge when #2714 stopped that walk. Only the rust fingerprint moves; the other 14 languages are byte-identical.", - "_rebaselined_module_tree_2730": "#2730 + #2741 review: RUST_SCOPE_QUERY captures mod_item as @declaration.namespace (a Rust module is an item, mirroring the C++ namespace_definition capture) and tags scoped call sites with @reference.qualified-name so the written path survives to resolution. Both are additive captures: every bench fixture holding a mod block or a Foo::bar() call gains groups, and the corpus also grew by the rust-2730-* fixtures added for the fix and its review (workspace-crates, type-qualified, gaps, samename-wrapper, crate-layout). Prior 7f1240b38457468f06b7931e0c2c578f218f922774d0dc7e2ee6ef3b08d4d689 -> 90fda086a4e13aa069a5981f63ed58ab1c71f1ed3da5e1480a080e1992b0d3e5; scaling 1.061 < 1.5; fixture_count 196. Only the rust fingerprint moves; the other 14 languages are byte-identical. The earlier revision of this note cited 655aed01... as the prior value, which was two rebaselines stale (it predates #2604 and #2714); the CI gate compares live fingerprints, not this prose, so nothing caught it." + "_rebaselined_module_tree_2730": "#2730 + #2741 review: RUST_SCOPE_QUERY captures mod_item as @declaration.namespace (a Rust module is an item, mirroring the C++ namespace_definition capture) and tags scoped call sites with @reference.qualified-name so the written path survives to resolution. Both are additive captures: every bench fixture holding a mod block or a Foo::bar() call gains groups, and the corpus also grew by the rust-2730-* fixtures added for the fix and its review (workspace-crates, type-qualified, gaps, samename-wrapper, crate-layout). Prior 7f1240b38457468f06b7931e0c2c578f218f922774d0dc7e2ee6ef3b08d4d689 -> 90fda086a4e13aa069a5981f63ed58ab1c71f1ed3da5e1480a080e1992b0d3e5; scaling 1.061 < 1.5; fixture_count 196. Only the rust fingerprint moves; the other 14 languages are byte-identical. The earlier revision of this note cited 655aed01... as the prior value, which was two rebaselines stale (it predates #2604 and #2714); the CI gate compares live fingerprints, not this prose, so nothing caught it.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 05acbaca48427e0d9e0793bcd0ce4057712d3716b5e7868189c12e05ef8dd300 -> 83812d82f0e2c3eb552f3246381ca3dd5ccd6783d63aba3325f1343e7772280c." }, "php": { - "fingerprint": "4a688fa5a7016546f7f3c6d44de023608ae80c5b0e3670c16f6e61b3632608fd", + "fingerprint": "3745662053c76b6ae0a84a29aad319626ed5ccb88f7b9376c2680d3dc6502e28", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior df7b1565f9115d66b1ae32e4a408d651afb2521b14e5ca615f3be426c29af618 -> 4a688fa5a7016546f7f3c6d44de023608ae80c5b0e3670c16f6e61b3632608fd; scaling 1.078 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: PHP first-class callable and variable-invocation flow facts with invocation-result suppression. Prior 31c9e3f3cb7094a2bf9021cf9db859036e002f8b44605cd993b470fc600e97cb -> df7b1565f9115d66b1ae32e4a408d651afb2521b14e5ca615f3be426c29af618; scaling 1.074 < 1.5.", "_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04). | #2481/#2482: PHP imports carry a symbol-kind capture so function/constant imports resolve by declaring file; capture shape changes, scaling remains linear (~1.04).", - "_note": "PR #1931: F53 import multi-clause, F54 enum_case, F55 anonymous_class \u2014 fixture count 138\u2192140, fingerprint drift expected." + "_note": "PR #1931: F53 import multi-clause, F54 enum_case, F55 anonymous_class \u2014 fixture count 138\u2192140, fingerprint drift expected.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 4a688fa5a7016546f7f3c6d44de023608ae80c5b0e3670c16f6e61b3632608fd -> 3745662053c76b6ae0a84a29aad319626ed5ccb88f7b9376c2680d3dc6502e28." }, "ruby": { - "fingerprint": "fea3edf82f521995147874b7f6c5f9e2eb88efdebf6365668f3260e913f0b558", + "fingerprint": "fc81941b0a921074fa80dc448284de9a23bd07358ddc84d4894797cc08c3fe83", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior cff273ae6cb7232c977d9241581834a2a2fa8bcf6369f7bd8f2471cd4419a6ef -> bf50ec6a53c8c91680dc6feac63a8956e78b1059249232dc25a0cfed25f31236; scaling 1.103 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Ruby Method/Proc callable flow facts with invocation/constructor-result suppression. Prior b5ea93bb3d0469c3821a8c70f5d5991c6f326e41097c119ad691154301dcc753 -> cff273ae6cb7232c977d9241581834a2a2fa8bcf6369f7bd8f2471cd4419a6ef; scaling 1.086 < 1.5.", "_rebaselined": "#1956 synth-widening: + ruby-qualified-base fixture; synth now reduces a scope_resolution superclass (class C < Mod::Super) to its trailing constant (matching the #1940 legacy leg), at parity. Linear (~1.03). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.", "_note": "F62: + scope_resolution class/module declaration captures \u2014 fixture count 78\u219281, fingerprint drift expected. #1975: + ruby-tail-collision fixture (Foo::Bar vs Baz::Bar stay distinct nodes) \u2014 pure fixture-corpus drift, scope-extractor captures unchanged; 81\u219282. #1991: + ruby-nested-mixin-tail-collision fixture (85\u219286). Recomputed on the #942 merge (fixture-comment rewording shifts capture byte-positions, capture LOGIC unchanged): bf6b13a -> b5ea93bb.", - "_rebaselined_2522_review_fixes": "PR #2522 review fixes: bare identifiers are calls, not callable references (bareNamesAreCalls). Prior bf50ec6a53c8c91680dc6feac63a8956e78b1059249232dc25a0cfed25f31236 -> 070e4e11502442998ddf4048c2981cf1b2b735a87362ff854c5d14d71f98f4e2; scaling ratio re-verified within budget." + "_rebaselined_2522_review_fixes": "PR #2522 review fixes: bare identifiers are calls, not callable references (bareNamesAreCalls). Prior bf50ec6a53c8c91680dc6feac63a8956e78b1059249232dc25a0cfed25f31236 -> 070e4e11502442998ddf4048c2981cf1b2b735a87362ff854c5d14d71f98f4e2; scaling ratio re-verified within budget.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior fea3edf82f521995147874b7f6c5f9e2eb88efdebf6365668f3260e913f0b558 -> fc81941b0a921074fa80dc448284de9a23bd07358ddc84d4894797cc08c3fe83." }, "swift": { - "fingerprint": "115c5da807e36bb12fdeba28e44f2b6484ef322ff26c19fa0f191febaf774248", + "fingerprint": "a6fca5f052ae5ec635b56051e28a168c864a988b2221a3279ddd69807378ba0b", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 5f923c6604d825d12b249f31c155b0f4d13a8379d532e5dde64a0f9b15cf4725 -> 7687ee2466e16020a12440a03fbda53e63aa05f94b4481f6133c09867a0d560d; scaling 1.042 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Swift function-value callable flow facts with invocation-result suppression. Prior 180ac68e780bdf6f9089d53f51cbb9a66aed3e7774631cc3fcbaae5020213998 -> 5f923c6604d825d12b249f31c155b0f4d13a8379d532e5dde64a0f9b15cf4725; scaling 1.043 < 1.5.", "_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression).", - "_rebaselined_2522_review_fixes": "PR #2522 review fixes: assignment target:/result: fields join the shared fallback. Prior 7687ee2466e16020a12440a03fbda53e63aa05f94b4481f6133c09867a0d560d -> 115c5da807e36bb12fdeba28e44f2b6484ef322ff26c19fa0f191febaf774248; scaling ratio re-verified within budget." + "_rebaselined_2522_review_fixes": "PR #2522 review fixes: assignment target:/result: fields join the shared fallback. Prior 7687ee2466e16020a12440a03fbda53e63aa05f94b4481f6133c09867a0d560d -> 115c5da807e36bb12fdeba28e44f2b6484ef322ff26c19fa0f191febaf774248; scaling ratio re-verified within budget.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 115c5da807e36bb12fdeba28e44f2b6484ef322ff26c19fa0f191febaf774248 -> a6fca5f052ae5ec635b56051e28a168c864a988b2221a3279ddd69807378ba0b." }, "dart": { "fingerprint": "ba93c90dcd341259e8e088816bc8c76ad27882419f665e35c056dc22fa54cf73", @@ -95,7 +102,7 @@ "_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0." }, "java": { - "fingerprint": "6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197", + "fingerprint": "310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata; same-name lexical regions use an O(ancestor-depth) ID-set lookup. Prior d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a -> 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4; scaling 0.992 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Java method-reference/SAM callable flow facts with invocation-result suppression. Prior 062d754764aaa8a6772fb90875c710502a63e3e7a300e633942381ed914faada -> d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a; scaling 1.074 < 1.5.", @@ -106,15 +113,17 @@ "_rebaselined_2555_enum_constant_bodies": "PR for #2555: enum constant bodies emit synthesized E$N classes + @reference.inherits to the host enum; anonymous naming follows JLS 13.1 immediately-enclosing-type chains INCLUDING anonymous enclosing types (NestHost$1$1, N$1$1); six new java-* fixtures joined the corpus. Prior d79c3b92acfc866094981499b977388ca14f90839bca0c040342ab1cec00aa90 -> 975b68aaac6d06094260fb0c67f9b1bc03692ba7220669d192aca9dccd5fc0ca; scaling 1.05 < 1.5.", "_rebaselined_2564_record_capture": "PR for #2564: JAVA_QUERIES gained a (record_declaration name: (identifier) @name) @definition.record capture, previously entirely missing (record_declaration had no structure-phase capture at all, unlike class/interface/enum) - a record's methods existed as ownerless Method nodes with no HAS_METHOD edge. Two new java-* fixtures (java-record-methods, java-new-expr-chain-call) joined the corpus. Prior 975b68aaac6d06094260fb0c67f9b1bc03692ba7220669d192aca9dccd5fc0ca -> 85fc7af9c3c1bceac76cb4f27214410b04967682a2eaa7e468e26efd1f4e2537; scaling 1.059 < 1.5.", "_rebaselined_2561_enum_constant_receiver": "PR for #2561: synthesizeJavaAnonymousClassDeclarations now emits a class-scope @type-binding.annotation/name/type per enum constant (constant simple name -> its E$N synthesized class when bodied, else the host enum) so E.CONST.method() resolves through the existing compound-receiver chain walk. Two drivers of the drift, both in the java-enum-constant-body fixture (this bench's corpus IS test/fixtures/lang-resolution): (1) one extra type-binding match per enum_constant from the capture change; (2) review follow-up added a body-less Plain.java enum + EnumConst.dispatchToConstant/dispatchInherited methods (bodied-override, inherited-via-MRO, and body-less dispatch call sites). The review's fail-safe hardening (bodied constant binds ONLY to E$N, never the host enum, when name synthesis fails on a malformed tree) is output-neutral on this well-formed corpus (verified: fingerprint identical with and without it). Prior 85fc7af9c3c1bceac76cb4f27214410b04967682a2eaa7e468e26efd1f4e2537 -> d04298a91beec76d0fa7099b3d71265723be60c1df688969aa954f135dd49686; scaling < 1.5.", - "_rebaselined_2562_local_classes": "#2562: Java block-local classes, enums, records, and interfaces use source-type-relative JLS 13.1 Host$NLocal identities with javac-compatible per-(host, simple-name) numbering; anonymous numbering remains separate. Lexical aliases begin at each declaration and end with its immediate block. Expanded java-local-class-naming fixtures cover declaration order, disjoint blocks, initializers, lambdas, local type kinds, and recursive local/member/anonymous host chains. Prior d04298a91beec76d0fa7099b3d71265723be60c1df688969aa954f135dd49686 -> 6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197; scaling 1.204 < 1.5." + "_rebaselined_2562_local_classes": "#2562: Java block-local classes, enums, records, and interfaces use source-type-relative JLS 13.1 Host$NLocal identities with javac-compatible per-(host, simple-name) numbering; anonymous numbering remains separate. Lexical aliases begin at each declaration and end with its immediate block. Expanded java-local-class-naming fixtures cover declaration order, disjoint blocks, initializers, lambdas, local type kinds, and recursive local/member/anonymous host chains. Prior d04298a91beec76d0fa7099b3d71265723be60c1df688969aa954f135dd49686 -> 6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197; scaling 1.204 < 1.5.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197 -> 310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee." }, "java-local-types": { - "fingerprint": "a9ad88de21ca6747a923260dbdf677fb74a004abbf9d57781f745e3a9027530b", + "fingerprint": "3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236", "scaling_budget": 1.5, - "_added": "#2562 performance follow-up: co-scales same-host, same-name local classes and anonymous classes to gate JLS binary-name ordinal allocation. Precomputed per-sequence ordinals reduce the focused 100->800 workload from 176->6655ms to 141->752ms; normalized 250->800 scaling is 1.054." + "_added": "#2562 performance follow-up: co-scales same-host, same-name local classes and anonymous classes to gate JLS binary-name ordinal allocation. Precomputed per-sequence ordinals reduce the focused 100->800 workload from 176->6655ms to 141->752ms; normalized 250->800 scaling is 1.054.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior a9ad88de21ca6747a923260dbdf677fb74a004abbf9d57781f745e3a9027530b -> 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236." }, "typescript": { - "fingerprint": "cad25be9f81d6e021ebae8dcb166bc0af3a1ba8021f1506f6ca93fd4c2649000", + "fingerprint": "9e112415f1169f08576826c12ea1d137d1994e34b44c45986c9ffee83b8b4edc", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 27f937bfb47d4bded316ea3c785ff659c8cd88a5761d928f113477a08c802c78 -> e05446620c5b80b7aae291cfdf32f693580fada2ae687124769b04a0c03bfe63; scaling 0.983 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: lexical callable bindings, direct-callee argument metadata, and invocation-result suppression. Prior db5933cc6760234ed7d495123410feba6de243646d583f20d43032b9459f81fd -> 27f937bfb47d4bded316ea3c785ff659c8cd88a5761d928f113477a08c802c78; scaling 0.975 < 1.5.", @@ -123,10 +132,11 @@ "_note": "#1968: F44, F85, F87 \u2014 fingerprint drift expected.", "_rebaselined_2522": "#2522 intentional @reference.value-ref/property-key capture additions. GitHub Actions run 29553361660 job 87800394279: prior 3f44a4a6892698df2d145c8ff2812c3b318807648983c88aca28fbd694f172f9 -> 25de86fd3377132c4e35d3d98f4f94a58e0cfeb7c22948a8ea3be4e793be74fd; scaling ratio 0.987 < 1.5.", "_rebaselined_2550_instance_model": "PR #2549 (#2545/#2551): object literals emit @scope.object (was unscoped, then @scope.block during development). Prior e05446620c5b80b7aae291cfdf32f693580fada2ae687124769b04a0c03bfe63 -> 3280b13d3f9378ab23eee31c2edc779b5a9ae1e7bb510c23a24855b44406d2f4; scaling 0.981 < 1.5.", - "_rebaselined_receiver_owner_2701": "#2701: every non-arrow function form now carries a `@receiver-owner.this` marker on the same node as `@scope.function`, so a scope that BINDS its own `this` can stop the receiver walk (`Scope.ownsReceivers`). Verified before re-baselining by diffing the capture-name histogram over this same fixture corpus against 1d3088173f6f93827641b476d614d5d15cd4f3ea: the ONLY delta is @receiver-owner.this (typescript +143, javascript +32) \u2014 every other capture count is byte-identical, so no existing capture moved. Prior 3280b13d3f9378ab23eee31c2edc779b5a9ae1e7bb510c23a24855b44406d2f4 -> 281e95484203b481094729ca249ef0423c41273eac35e424cdfd032a0dac7699." + "_rebaselined_receiver_owner_2701": "#2701: every non-arrow function form now carries a `@receiver-owner.this` marker on the same node as `@scope.function`, so a scope that BINDS its own `this` can stop the receiver walk (`Scope.ownsReceivers`). Verified before re-baselining by diffing the capture-name histogram over this same fixture corpus against 1d3088173f6f93827641b476d614d5d15cd4f3ea: the ONLY delta is @receiver-owner.this (typescript +143, javascript +32) \u2014 every other capture count is byte-identical, so no existing capture moved. Prior 3280b13d3f9378ab23eee31c2edc779b5a9ae1e7bb510c23a24855b44406d2f4 -> 281e95484203b481094729ca249ef0423c41273eac35e424cdfd032a0dac7699.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior cad25be9f81d6e021ebae8dcb166bc0af3a1ba8021f1506f6ca93fd4c2649000 -> 9e112415f1169f08576826c12ea1d137d1994e34b44c45986c9ffee83b8b4edc." }, "javascript": { - "fingerprint": "1c71ef628eb75a3b111afa8c2a7c351c16a7f5aab9fac2f098f82b2866312aa8", + "fingerprint": "83344b7cba093702f4528eeee44e438809c229d43b12e69ed288812ce7ffc7bc", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior b59fe8135b6a31a12bc3f872b224054b16592588153ae3661d03958d787c76f3 -> 479927409bbdd9852a36172c8260aa56df260e99129a7a9c20a0d1903dd5538b; scaling 1.050 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: lexical callable bindings, direct-callee argument metadata, and invocation-result suppression. Prior 917a9cd975ba035bdad71fdb70cd72eeddec58c25797e5a1addfa6172808a55c -> b59fe8135b6a31a12bc3f872b224054b16592588153ae3661d03958d787c76f3; scaling 1.093 < 1.5.", @@ -135,10 +145,11 @@ "_rebaselined": "#1956 synth-widening: + javascript-qualified-base fixture; synthesizeJsInheritanceReferences now handles a member_expression base (class S extends ns.Base -> Base), matching the #1940 legacy leg + the TS terminalTsTypeNameNode property_identifier case, at parity. Linear (~1.05). | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.", "_rebaselined_2522": "#2522 intentional @reference.value-ref/property-key capture additions. GitHub Actions run 29553361660 job 87800394279: prior d72f03c6c502235d2d4b74d66baa5c7d361f040d7a1b72e84acad61210d05ae8 -> 5567dd47e7ba29821a518c4a9852adc3b774e25ef3e7a6e2b3ecb7b59ddab73c; scaling ratio 1.031 < 1.5.", "_rebaselined_2550_instance_model": "PR #2549 (#2545/#2551): object literals emit @scope.object. Prior 479927409bbdd9852a36172c8260aa56df260e99129a7a9c20a0d1903dd5538b -> f1ccf42a36895c8e34dcb724286f247d469835f2dcbb23ad3347190adc7fde1c; scaling 1.096 < 1.5.", - "_rebaselined_receiver_owner_2701": "#2701: every non-arrow function form now carries a `@receiver-owner.this` marker on the same node as `@scope.function`, so a scope that BINDS its own `this` can stop the receiver walk (`Scope.ownsReceivers`). Verified before re-baselining by diffing the capture-name histogram over this same fixture corpus against 1d3088173f6f93827641b476d614d5d15cd4f3ea: the ONLY delta is @receiver-owner.this (typescript +143, javascript +32) \u2014 every other capture count is byte-identical, so no existing capture moved. Prior f1ccf42a36895c8e34dcb724286f247d469835f2dcbb23ad3347190adc7fde1c -> 90601494695b834d3a9af7ac4844eac603f4f432809a05554cc59de0674a4354." + "_rebaselined_receiver_owner_2701": "#2701: every non-arrow function form now carries a `@receiver-owner.this` marker on the same node as `@scope.function`, so a scope that BINDS its own `this` can stop the receiver walk (`Scope.ownsReceivers`). Verified before re-baselining by diffing the capture-name histogram over this same fixture corpus against 1d3088173f6f93827641b476d614d5d15cd4f3ea: the ONLY delta is @receiver-owner.this (typescript +143, javascript +32) \u2014 every other capture count is byte-identical, so no existing capture moved. Prior f1ccf42a36895c8e34dcb724286f247d469835f2dcbb23ad3347190adc7fde1c -> 90601494695b834d3a9af7ac4844eac603f4f432809a05554cc59de0674a4354.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 1c71ef628eb75a3b111afa8c2a7c351c16a7f5aab9fac2f098f82b2866312aa8 -> 83344b7cba093702f4528eeee44e438809c229d43b12e69ed288812ce7ffc7bc." }, "kotlin": { - "fingerprint": "9f159f8810d342ef1c821f466efd6920dad9a190f06000056e6cd2815861b195", + "fingerprint": "d3c4d2fa0d82d248a2299cfc888b067187ad1faf2c87a97f93c6ed835eefc3f1", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior bddba25d5a88152bbbee8d70e82c944b5302accb4b625df782adb1d4f7a7ac12 -> e856951c2a779163d555dadc8e1bf59304a86caed78ac1f450d9caa2b50f63d1; scaling 1.090 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Kotlin callable-reference flow facts with invocation-result suppression. Prior 4900431791f2b9280009deb2b82659c26ead8aa6fb8731190a7c505dec5a9041 -> bddba25d5a88152bbbee8d70e82c944b5302accb4b625df782adb1d4f7a7ac12; scaling 0.880 < 1.5.", @@ -147,6 +158,7 @@ "_rebaselined_2271": "PR #2271: re-vendored tree-sitter-kotlin 0.3.8 -> unreleased fwcd main c8ac3d26 for `fun interface` support + new kotlin-fun-interface fixture in the corpus. Drift is both corpus-additive (the fixture) and grammar-driven (the new grammar parses `fun interface` as a class_declaration, not an ERROR node). Baselined to the NEW grammar's fingerprint, so this --check passes only once the regenerated prebuilds land \u2014 until then CI loads the committed 0.3.8 binary and the bench is red, same as the kotlin fun-interface integration tests. scaling ~0.83 (linear).", "_rebaselined_2522_review_fixes": "PR #2522 review fixes: fieldless assignment nodes decomposed positionally. Prior e856951c2a779163d555dadc8e1bf59304a86caed78ac1f450d9caa2b50f63d1 -> 4b31f46cfb004ba769a96feeb06ae4ef109c77410f54e7aaab4a688df599b112; scaling ratio re-verified within budget.", "_rebaselined_2550_instance_model": "PR #2549 (#2545): anonymous object expressions (object_literal) emit @scope.class, and the kotlin-object-literal-scope fixture joined the corpus. Prior 4b31f46cfb004ba769a96feeb06ae4ef109c77410f54e7aaab4a688df599b112 -> a6fce0dff00e88d41d85023eaf3f35016b5217c7e5225f24a598e4c70bb63091; scaling 0.951 < 1.5.", - "_rebaselined_2563_instance_ownership": "#2563: kotlin-instance-ownership adds unrelated, inherited, outer-instance, and anonymous-object coverage. Prior a6fce0dff00e88d41d85023eaf3f35016b5217c7e5225f24a598e4c70bb63091 -> 9f159f8810d342ef1c821f466efd6920dad9a190f06000056e6cd2815861b195; scaling 1.257 < 1.5." + "_rebaselined_2563_instance_ownership": "#2563: kotlin-instance-ownership adds unrelated, inherited, outer-instance, and anonymous-object coverage. Prior a6fce0dff00e88d41d85023eaf3f35016b5217c7e5225f24a598e4c70bb63091 -> 9f159f8810d342ef1c821f466efd6920dad9a190f06000056e6cd2815861b195; scaling 1.257 < 1.5.", + "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 9f159f8810d342ef1c821f466efd6920dad9a190f06000056e6cd2815861b195 -> d3c4d2fa0d82d248a2299cfc888b067187ad1faf2c87a97f93c6ed835eefc3f1." } } diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts index 0a3425777e..39a040a7ed 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts @@ -107,6 +107,17 @@ interface ResolveCompoundReceiverOptions { * carrying it into a recursive call would re-fold it against an inner * expression it does not describe. */ readonly receiverChain?: string; + /** Resolve a BARE identifier the way the dotted-chain head does: when a + * receiver typeBinding exists for the name, that binding decides the type and + * nothing else does. Off by default, so the text cascade keeps its existing + * (more permissive) behaviour; the structural fold turns it ON. + * + * Without it, the bare-identifier branch falls through to a plain class-name + * lookup EVEN WHEN a binding existed but named no class — which types a local + * that merely SHADOWS a class as that class. That fabricated a `CALLS` edge + * (`const Config = make(1); Config.db.query()` emitted `entry → Database.query`), + * the exact wrong-edge failure this work exists to avoid. */ + readonly strictBaseBinding?: boolean; } /** Is this hop the language's construction selector applied to the class @@ -276,7 +287,8 @@ function typeOfMemberOnClass( * class-name receivers, map-tuple sentinels, member aliases and call-result * aliases. A second implementation of that would drift from it. * - * NOT WIRED into any resolution path yet — see U10. + * Called from `resolveCompoundReceiverClass` ahead of the text cascade, and only + * when the site carries a `receiverChain` (see the `depth === 0` gate below). */ export function foldReceiverChain( chain: DecodedReceiverChain, @@ -298,6 +310,7 @@ export function foldReceiverChain( ...options, fieldFallback: false, receiverChain: undefined, + strictBaseBinding: true, }); if (current === undefined) return undefined; @@ -467,6 +480,11 @@ export function resolveCompoundReceiverClass( if (compound !== undefined) return compound; } } + // Mirror the dotted-chain head rule below (`headType ? … : …`): a binding + // that EXISTS but resolves to no class means "not a typed receiver", not + // "try the class namespace instead". Only the structural fold opts in; the + // cascade keeps its historical fallthrough so no existing edge moves. + if (tb !== undefined && options.strictBaseBinding === true) return undefined; return findClassBindingInScope(inScope, workingText, scopes); } diff --git a/gitnexus/src/core/ingestion/scope-resolution/unresolved-receivers.ts b/gitnexus/src/core/ingestion/scope-resolution/unresolved-receivers.ts index ec7b6e5f72..db6cfb03b1 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/unresolved-receivers.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/unresolved-receivers.ts @@ -47,6 +47,15 @@ export function summarizeUnresolvedReceivers( for (const outcome of outcomes) { if (outcome.kind !== 'suppressed' || outcome.reason !== 'receiver-unresolved') continue; if (outcome.name.length === 0) continue; + // CALL sites only. Case 0's recorder gates on the receiver's punctuation, not + // on what the reference IS, so property reads (`d.source.kind`) and writes + // (`x.argtypes = [...]`) are recorded alongside lost method calls — measured at + // 25 of 124 drops on the fixture corpus. Counting them made the consumer's + // "N call sites invoking X were dropped" literally false, and flagged symbols + // whose CALL count was never short. `siteKind` exists to make this separable. + // A missing `siteKind` counts as a call: the only emitter always sets it, and + // erring toward `lower-bound` is the safe direction for an epistemic signal. + if (outcome.siteKind !== undefined && outcome.siteKind !== 'call') continue; totalSites++; counts.set(outcome.name, (counts.get(outcome.name) ?? 0) + 1); } @@ -67,3 +76,29 @@ export function summarizeUnresolvedReceivers( ...(omittedNames > 0 ? { omittedNames } : {}), }; } + +/** + * Look up the dropped-CALL count for a member name. + * + * THE one place that reads `UnresolvedReceiverSummary.counts`. The map is + * revived from JSON, so it carries `Object.prototype`: a bare + * `counts[symName]` returns a FUNCTION for `constructor`, `toString`, + * `valueOf`, `hasOwnProperty` and friends, and a `<= 0` guard does not catch it + * because `Number(fn)` is `NaN` and `NaN <= 0` is false. `constructor` is an + * ordinary member name in a code graph, so that was reachable in normal use and + * interpolated a function into user-facing text. + * + * Returns `undefined` when the name was never recorded, and only ever returns a + * finite positive number otherwise. + */ +export function lookupUnresolvedCallCount( + summary: UnresolvedReceiverSummary | undefined, + symName: string, +): number | undefined { + const counts = summary?.counts; + if (counts === undefined || symName.length === 0) return undefined; + if (!Object.hasOwn(counts, symName)) return undefined; + const sites = counts[symName]; + if (typeof sites !== 'number' || !Number.isFinite(sites) || sites <= 0) return undefined; + return sites; +} diff --git a/gitnexus/src/core/ingestion/utils/receiver-chain-codec.ts b/gitnexus/src/core/ingestion/utils/receiver-chain-codec.ts index 0a3f824e3f..4bb68eace7 100644 --- a/gitnexus/src/core/ingestion/utils/receiver-chain-codec.ts +++ b/gitnexus/src/core/ingestion/utils/receiver-chain-codec.ts @@ -29,7 +29,13 @@ * character of the segment and the name follows immediately, so a member * whose name begins with `c` or `f` needs no escaping (`ccount` decodes as a * call to `count`). - * - A trailing `|~` segment is the TRUNCATION MARKER: the chain hit + * - A trailing `|~` segment is the TRUNCATION MARKER. NOTE: no current producer + * mints one. `extractMixedChain` signals "stopped early" by returning + * `baseReceiverName: undefined`, and the encoder requires a base, so a + * truncated chain is unrepresentable rather than merely unused. The marker and + * the decoder/fold guards are kept as a forward-compatible contract: a future + * producer that CAN report a partial chain must set it, and the fold already + * refuses such chains. Read the `truncated` field as "reserved", not "live". the chain hit * `MAX_CHAIN_DEPTH` and what is encoded is a base-side PREFIX of the real * chain. A consumer MUST treat a truncated chain as unusable for typing — * the missing tail is exactly what determines the final type — but never as @@ -69,11 +75,15 @@ export interface DecodedReceiverChain { * every supported language satisfy this; anything that does not is a payload * we refuse to mint rather than escape. */ function isEncodableSegment(value: string): boolean { + // `\s` does NOT match zero-width characters (U+200B ZWS, U+200C/D ZWNJ/ZWJ, + // U+200E/F LRM/RLM, U+FEFF BOM). An identifier carrying one is a trojan-source + // vector: it would encode and persist cleanly, then match no binding at + // resolution time — a silent, unexplained miss. Refuse to mint instead. return ( value.length > 0 && !value.includes(SEPARATOR) && !value.includes(TRUNCATED) && - !/\s/.test(value) + !/[\s\u200B-\u200F\u2028\u2029\uFEFF]/.test(value) ); } @@ -116,7 +126,8 @@ export function decodeReceiverChain(value: unknown): DecodedReceiverChain | unde if (parts.length < 3) return undefined; // version + base + at least one step if (parts[0] !== VERSION) return undefined; - const baseReceiverName = parts[1] ?? ''; + // `parts.length < 3` above already proves this element exists. + const baseReceiverName = parts[1] as string; if (!isEncodableSegment(baseReceiverName)) return undefined; let stepParts = parts.slice(2); diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 6abb87d35a..dd96779440 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -93,6 +93,7 @@ import { findImportCycles } from '../../core/graph/import-cycles.js'; import { decodeTaintPath } from '../../core/ingestion/taint/path-codec.js'; import { decodeReachingDefReason } from '../../core/ingestion/cfg/reaching-def-reason-codec.js'; import { EXTENSIONS } from '../../core/ingestion/import-resolvers/utils.js'; +import { lookupUnresolvedCallCount } from '../../core/ingestion/scope-resolution/unresolved-receivers.js'; import { fnLineOf, isPdgDegradedLayerStatus, @@ -5715,8 +5716,11 @@ export class LocalBackend { try { const meta = await loadMeta(path.dirname(repo.lbugPath)); const summary = meta?.unresolvedReceiverMembers; - const sites = summary?.counts?.[symName]; - if (sites === undefined || sites <= 0) return []; + // Prototype-safe: see `lookupUnresolvedCallCount`. A bare `counts[symName]` + // returns a Function for `constructor`/`toString`/… and `NaN <= 0` is false, + // so the old guard let it through into user-facing text. + const sites = lookupUnresolvedCallCount(summary, symName); + if (sites === undefined) return []; return [ `${sites} call ${sites === 1 ? 'site' : 'sites'} invoking \`${symName}\` ${ sites === 1 ? 'was' : 'were' diff --git a/gitnexus/src/storage/parse-cache.ts b/gitnexus/src/storage/parse-cache.ts index c8706047d5..3612bc9dd1 100644 --- a/gitnexus/src/storage/parse-cache.ts +++ b/gitnexus/src/storage/parse-cache.ts @@ -128,16 +128,19 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j // JLS 13.1 immediate-host chains (#2555). // v18: Worker$N anonymous bodies. v17: callable-value-flow operand identity. // v16: direct callee identity. +// v34: the receiver-chain capture is emitted by ALL 14 language emitters, not +// just TypeScript. v33 landed with the TypeScript-only emission; the rollout to +// the other 13 languages changed the capture set AGAIN, so a cache stamped 33 by +// an intermediate build of this series is not equivalent to one stamped at this +// commit — it would be treated as current while every non-TypeScript file +// replayed pre-rollout captures, leaving the feature silently inert for 13 of 14 +// languages. Bumped here so the version tracks the FINAL capture set rather than +// the first divergence. // v33: TypeScript call matches carry `@reference.receiver-chain`, a compact -// encoding of a receiver that is itself an expression. Lands at the first commit -// where the capture SET actually diverges — not earlier with the field that -// carries it, which changed nothing a warm cache could replay wrongly. A cache -// written before this has no chain capture at all, so replaying it would make -// the feature look inert rather than broken, which is the harder failure to -// diagnose. (Shipped as v32 on its own branch; `main` took 32 for #2742 first, -// so it is renumbered here — see the v21 note on re-checking against origin/main -// at MERGE time, not branch time.) -const SCHEMA_BUMP = 33; +// encoding of a receiver that is itself an expression. (Shipped as v32 on its own +// branch; `main` took 32 for #2742 first, so this series is renumbered — see the +// v21 note on re-checking against origin/main at MERGE time, not branch time.) +const SCHEMA_BUMP = 34; const GITNEXUS_PKG_VERSION = (() => { try { // package.json sits at gitnexus/package.json — two levels up from diff --git a/gitnexus/src/storage/parsedfile-store.ts b/gitnexus/src/storage/parsedfile-store.ts index 43cfc8406d..c9a6e0482c 100644 --- a/gitnexus/src/storage/parsedfile-store.ts +++ b/gitnexus/src/storage/parsedfile-store.ts @@ -350,8 +350,16 @@ function sanitizeCallableFlowSites( } /** - * Same untrusted-boundary contract as `sanitizeCallableFlowSites`, for the - * compact `receiverChain` on a reference site. + * Untrusted-boundary handling for the compact `receiverChain` on a reference site. + * + * NARROWER than its sibling `sanitizeCallableFlowSites`, deliberately and worth + * stating plainly: that function validates every element with a full type + * predicate, whereas this one inspects only the `receiverChain` sub-field and, in + * the common case where nothing was stripped, returns the original array typed as + * `ReferenceSite[]` without structurally validating `name` / `kind` / `atRange`. + * That is still a strict improvement — `referenceSites` had NO sanitation at all + * before this — but it is not parity, and a future change that needs per-site + * structural validation must add it rather than assume it is already here. * * Sanitation is per-FIELD, not per-site: a chain that does not decode is * stripped and the site is kept, because the site is still perfectly usable diff --git a/gitnexus/src/storage/repo-manager.ts b/gitnexus/src/storage/repo-manager.ts index d6f4faf284..560091a9dc 100644 --- a/gitnexus/src/storage/repo-manager.ts +++ b/gitnexus/src/storage/repo-manager.ts @@ -568,7 +568,15 @@ export interface RepoMeta { * — for every unchanged Rust file, which is exactly the symptom #2730 * reported. Force a full re-analyze. * - * v27: receiver expressions are typed from captured structure rather than from + * v28: structural receiver typing is active for ALL 14 languages, and the fold no + * longer types a bare identifier that merely SHADOWS a class name as that class. + * v27 landed with TypeScript-only emission and with the permissive base lookup, so + * an index stamped 27 by an intermediate build carries both pre-rollout edges for 13 + * languages AND the fabricated edges the shadowing bug produced. The reuse gate is a + * strict `===`, so such an index would be treated as current. Re-bumped here so the + * version tracks the final edge semantics. Force a full re-analyze. + * + * v26: receiver expressions are typed from captured structure rather than from * their source text. `svc?.getUser().save()`, `svc!.getUser().save()` and * `svc.getTyped().save()` previously emitted NO `CALLS` edge — the text * cascade split the receiver on punctuation it could not parse — and two of the @@ -615,7 +623,7 @@ export interface RepoMeta { * index holds ids an incremental top-up cannot reconcile and would simply * strand. Force a full re-analyze. */ -export const INCREMENTAL_SCHEMA_VERSION = 27; +export const INCREMENTAL_SCHEMA_VERSION = 28; export interface IndexedRepo { repoPath: string; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-receiver-chain-arrow/main.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-receiver-chain-arrow/main.cpp new file mode 100644 index 0000000000..b15d7eb1e6 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-receiver-chain-arrow/main.cpp @@ -0,0 +1,14 @@ +#include "models.h" + +// The `->` BASE receiver. Before structural receiver typing this emitted no +// CALLS edge to `save` at all, and `cpp-chain-call/` could not catch it because +// that fixture uses the value `.` form, which already worked. +void pointerArrowChain(Service* svc) { + svc->getUser()->save(); +} + +// Control: same chain, same `->save()` tail, value `.` on the base. Resolved +// before this work and must keep resolving. +void valueDotChain(Service svc) { + svc.getUser()->save(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-receiver-chain-arrow/models.h b/gitnexus/test/fixtures/lang-resolution/cpp-receiver-chain-arrow/models.h new file mode 100644 index 0000000000..1294fa6e21 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-receiver-chain-arrow/models.h @@ -0,0 +1,11 @@ +#pragma once + +class User { +public: + void save(); +}; + +class Service { +public: + User* getUser(); +}; diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 3004277877..4bcb4e0bd9 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -4548,3 +4548,33 @@ describe('C++ deleted overload selection (#1893 A2)', () => { ]); }); }); + +// --------------------------------------------------------------------------- +// Structural receiver typing for a `->` base receiver. +// +// The all-language rollout is proven behaviourally for TypeScript only; this is +// the second language, and the one the rollout measurably changed: C++ +// `svc->getUser()->save()` emitted NO edge and recorded NO drop before it. +// --------------------------------------------------------------------------- + +describe('C++ structural receiver chain through a pointer base', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-receiver-chain-arrow'), () => {}); + }, 60000); + + it('resolves svc->getUser()->save() — the `->` base the text cascade could not parse', () => { + const calls = getRelationships(result, 'CALLS'); + expect( + calls.find((c) => c.source === 'pointerArrowChain' && c.target === 'save'), + ).toMatchObject({ target: 'save' }); + }); + + it('keeps the value-dot base resolving (control — worked before this work)', () => { + const calls = getRelationships(result, 'CALLS'); + expect(calls.find((c) => c.source === 'valueDotChain' && c.target === 'save')).toMatchObject({ + target: 'save', + }); + }); +}); diff --git a/gitnexus/test/integration/resolvers/typescript.test.ts b/gitnexus/test/integration/resolvers/typescript.test.ts index 5082434724..d01f68609a 100644 --- a/gitnexus/test/integration/resolvers/typescript.test.ts +++ b/gitnexus/test/integration/resolvers/typescript.test.ts @@ -3398,7 +3398,19 @@ describe('TypeScript structural receiver chains', () => { beforeAll(async () => { repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-ts-receiver-chain-')); writeFixtureRepo(repoDir, { - 'models.ts': `export class Address { + 'models.ts': `export class Database { + query(): void {} +} + +export class Config { + db: Database = new Database(); +} + +export function make(n: number): number { + return n; +} + +export class Address { persist(): void {} } @@ -3416,12 +3428,19 @@ export class Service { } } `, - 'main.ts': `import { Service } from './models'; + 'main.ts': `import { Service, Config, make } from './models'; export function viaOptionalChain(svc: Service | null): void { svc?.getUser().save(); } +// A local that merely SHADOWS an imported class name. Its value is a number, +// so it has NO members — the fold must not type it as the class. +export function shadowsAClassName(): void { + const Config = make(1); + Config.db.query(); +} + export function viaTypeArgs(svc: Service): void { svc.getTyped().save(); } @@ -3430,8 +3449,16 @@ export function viaNonNull(svc: Service | null): void { svc!.getUser().save(); } -export function viaMixedChain(svc: Service): void { - svc.getUser().address.persist(); +// A mixed call/field chain behind a spelling the TEXT cascade cannot parse +// (optional chaining), so this discriminates the fold rather than re-testing +// the pre-existing cascade path. +export function viaMixedChain(svc: Service | null): void { + svc?.getUser().address.persist(); +} + +// User has no member named missing, so the chain dies at its middle step. +export function brokenMiddleStep(svc: Service): void { + svc.getUser().missing.persist(); } export function alreadyWorked(svc: Service): void { @@ -3490,9 +3517,32 @@ export function alreadyWorked(svc: Service): void { }); }); - it('emits no CALLS edge to a member that exists on no class in the chain', () => { - // A broken middle step must produce NO edge, never a wrong one. + it('does NOT fabricate an edge when a local shadows a class name', () => { + // Regression: the fold resolved its base through the permissive + // bare-identifier path, which falls through to a plain class-name lookup + // even when a receiver typeBinding exists but names no class. A local + // `const Config = make(1)` (a number) was therefore typed as the imported + // `class Config`, emitting a confident `CALLS` edge to `Database.query` + // that the text cascade never produced. A missing edge is recoverable; a + // wrong one is not. const calls = getRelationships(result, 'CALLS'); - expect(calls.filter((c) => c.target === 'noSuchMember')).toHaveLength(0); + expect(calls.filter((c) => c.source === 'shadowsAClassName' && c.target === 'query')).toEqual( + [], + ); + }); + + it('emits no CALLS edge when a middle step names no member of the previous class', () => { + // A broken MIDDLE step must produce no edge, never a wrong one. `User` has no + // member `missing`, so the chain cannot be typed past it and `persist` must + // not bind to anything — in particular not to `Address.persist`, which a + // field-walking fallback could otherwise reach. + // `getUser` itself still resolves — only the tail past the broken step must not. + const calls = getRelationships(result, 'CALLS'); + expect(calls.filter((c) => c.source === 'brokenMiddleStep' && c.target === 'persist')).toEqual( + [], + ); + expect( + calls.filter((c) => c.source === 'brokenMiddleStep' && c.target === 'getUser'), + ).toHaveLength(1); }); }); diff --git a/gitnexus/test/unit/call-summary-schema-version.test.ts b/gitnexus/test/unit/call-summary-schema-version.test.ts index ae53af96a9..2404ad1e60 100644 --- a/gitnexus/test/unit/call-summary-schema-version.test.ts +++ b/gitnexus/test/unit/call-summary-schema-version.test.ts @@ -73,12 +73,12 @@ describe('CALL_SUMMARY relation-type exclusion (U-C1)', () => { }); describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { - it('INCREMENTAL_SCHEMA_VERSION is bumped to 27 (structural receiver typing)', () => { + it('INCREMENTAL_SCHEMA_VERSION is bumped to 28 (structural receiver typing, all languages)', () => { // Moves with every bump BY DESIGN — that is the point of pinning it. A // change that alters emitted ids or edges without bumping would otherwise // ship silently, and an existing index would keep serving the old graph // through the reuse gate below. - expect(INCREMENTAL_SCHEMA_VERSION).toBe(27); + expect(INCREMENTAL_SCHEMA_VERSION).toBe(28); }); it('a pre-current stamp fails the `=== INCREMENTAL_SCHEMA_VERSION` reuse gate → forces full re-analyze', () => { @@ -177,8 +177,6 @@ describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { expect(passesReuseGate(22)).toBe(false); // A pre-v24 (v23) index predates the #2708 inline-constructor receivers. expect(passesReuseGate(23)).toBe(false); - // A pre-v25 (v24) index predates mod-qualified Rust node ids (#2742) → must NOT reuse. - expect(passesReuseGate(24)).toBe(false); // A pre-v25 (v24) index predates `unresolvedReceiverMembers` (#2744). An // absent summary is indistinguishable from "nothing was dropped", so a // top-up would keep reporting `epistemic: 'exact'` for exactly the symbols @@ -190,7 +188,17 @@ describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { // changed-files top-up keeps both the missing edge and the false confidence // for every unchanged file → must NOT reuse. expect(passesReuseGate(25)).toBe(false); + // A pre-v27 (v26) index was stamped by an intermediate build of this same + // series: structural typing was TypeScript-only at that point, and the fold + // still typed a bare identifier that merely shadowed a class name as that + // class — so such an index carries both pre-rollout edges for 13 languages + // and fabricated ones. The gate is a strict `===`, so it must NOT reuse. + expect(passesReuseGate(26)).toBe(false); + // A pre-v28 (v27) index was stamped mid-series: TypeScript-only structural + // typing, and the fold still typed a local that merely shadowed a class name + // as that class — so it carries pre-rollout edges AND fabricated ones. + expect(passesReuseGate(27)).toBe(false); // The current stamp passes the gate (incremental top-up eligible). - expect(passesReuseGate(26)).toBe(true); + expect(passesReuseGate(28)).toBe(true); }); }); diff --git a/gitnexus/test/unit/incremental-parse-cache.test.ts b/gitnexus/test/unit/incremental-parse-cache.test.ts index 341ba1d4a9..fa58cd8430 100644 --- a/gitnexus/test/unit/incremental-parse-cache.test.ts +++ b/gitnexus/test/unit/incremental-parse-cache.test.ts @@ -101,11 +101,14 @@ describe('fileContentHash', () => { }); describe('PARSE_CACHE_VERSION', () => { - // 31 -> 32 for the mod-qualified Rust node ids (#2742). Updated deliberately: - // this pin exists so a bump cannot ride along unnoticed, and it caught the - // collision when the pin arrived from main while this branch already held 32. - it('pins SCHEMA_BUMP to 32 so concurrent bumps cannot silently collide (#2736)', () => { - expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(32); + // 32 -> 34 for the receiver-chain capture (#2747): 33 is the TypeScript-only + // emission, 34 the rollout to the other 13 emitters, which changed the capture + // set a second time. Updated deliberately — and this pin earned its keep twice + // over: it caught the #2742 collision when it arrived from main, and it caught + // this series again on rebase, where `main` had independently taken 32 for the + // very number this branch was already using. + it('pins SCHEMA_BUMP to 34 so concurrent bumps cannot silently collide (#2736)', () => { + expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(34); }); it('embeds the gitnexus package version (so upgrades invalidate the cache)', () => { diff --git a/gitnexus/test/unit/parsedfile-store.test.ts b/gitnexus/test/unit/parsedfile-store.test.ts index 381fef97ea..f59f3fd533 100644 --- a/gitnexus/test/unit/parsedfile-store.test.ts +++ b/gitnexus/test/unit/parsedfile-store.test.ts @@ -586,4 +586,29 @@ describe('parsedfile-store receiverChain sanitation', () => { await rm(dir, { recursive: true, force: true }); } }); + + it('strips only the invalid chain and leaves a valid sibling intact', async () => { + // Sanitation is per-FIELD, not per-site or per-file. Every other case here + // uses a single-element array, so the `dropped > 0` .map() branch was never + // shown to preserve a good neighbour. + const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-chain-mixed-')); + try { + await persistParsedFileChunk(dir, 'chunk-0', [ + makeStoreEntry('x.ts', { + referenceSites: [ + siteWith('1|svc|cgetUser'), + siteWith('not-a-chain'), + siteWith('1|other|ffield'), + ], + }), + ]); + const loaded = (await loadParsedFilesForPaths(dir, new Set(['x.ts']))).get('x.ts')!; + expect(loaded.referenceSites).toHaveLength(3); + expect(loaded.referenceSites[0]).toMatchObject({ receiverChain: '1|svc|cgetUser' }); + expect(loaded.referenceSites[1]).not.toHaveProperty('receiverChain'); + expect(loaded.referenceSites[2]).toMatchObject({ receiverChain: '1|other|ffield' }); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); }); diff --git a/gitnexus/test/unit/receiver-chain-codec.test.ts b/gitnexus/test/unit/receiver-chain-codec.test.ts index b79bdc9339..7518e862b2 100644 --- a/gitnexus/test/unit/receiver-chain-codec.test.ts +++ b/gitnexus/test/unit/receiver-chain-codec.test.ts @@ -104,6 +104,53 @@ describe('receiver-chain codec', () => { expect(isValidReceiverChain(payload)).toBe(false); }); + it('refuses to mint a name carrying a zero-width character', () => { + // `\s` does not match these, so without an explicit class they encode and + // persist cleanly and then match no binding at resolution time — a silent, + // unexplained miss, and a trojan-source vector. + for (const invisible of ['\u200B', '\u200C', '\u200D', '\u200E', '\u200F', '\uFEFF']) { + expect( + encodeReceiverChain('svc', [{ kind: 'call', name: `get${invisible}User` }]), + ).toBeUndefined(); + expect( + encodeReceiverChain(`sv${invisible}c`, [{ kind: 'call', name: 'getUser' }]), + ).toBeUndefined(); + } + }); + + it('round-trips a chain of exactly MAX_CHAIN_DEPTH steps', () => { + // The refusal at MAX_CHAIN_DEPTH + 1 is pinned above; pin the boundary that + // must still WORK, so a future off-by-one narrowing is caught too. + const steps = Array.from({ length: MAX_CHAIN_DEPTH }, (_unused, i) => ({ + kind: 'call' as const, + name: `m${i}`, + })); + expect(decodeReceiverChain(encodeReceiverChain('svc', steps))).toMatchObject({ + baseReceiverName: 'svc', + truncated: false, + }); + expect(decodeReceiverChain(encodeReceiverChain('svc', steps))?.steps).toHaveLength( + MAX_CHAIN_DEPTH, + ); + }); + + it('survives adversarial payloads without throwing', () => { + // The "total function" claim was previously verified by reading only. + for (const hostile of [ + '|'.repeat(512), + '1|' + '|'.repeat(400), + '1|svc|c\u0000name', + '1|svc|c\uD800', + `1|svc|c${'x'.repeat(MAX_RECEIVER_CHAIN_BYTES)}`, + '1|'.repeat(300), + {}, + [], + null, + ]) { + expect(() => decodeReceiverChain(hostile)).not.toThrow(); + } + }); + it('rejects an over-cap payload at decode, matching the emit-side refusal', () => { // Emit and load must agree. A bound applied only on load is a writer that // keeps minting what the reader keeps refusing — a permanent, unlogged diff --git a/gitnexus/test/unit/scope-resolution/receiver-chain-fold.test.ts b/gitnexus/test/unit/scope-resolution/receiver-chain-fold.test.ts index f44d6a2223..77eededc23 100644 --- a/gitnexus/test/unit/scope-resolution/receiver-chain-fold.test.ts +++ b/gitnexus/test/unit/scope-resolution/receiver-chain-fold.test.ts @@ -66,6 +66,19 @@ function build() { const ctx = build(); +/** Fold with the language contract flags overridden — used to exercise the + * OFF branch of `hoistTypeBindingsToModule`, which six wired languages + * (c, cobol, dart, python, ruby, swift) actually run. */ +function foldWith(encoded: string, overrides: Record) { + const decoded = decodeReceiverChain(encoded); + expect(decoded).toBeDefined(); + return foldReceiverChain(decoded!, ctx.inScope, ctx.scopes, ctx.index, { + fieldFallback: false, + hoistTypeBindingsToModule: true, + ...overrides, + }); +} + function fold(encoded: string) { const decoded = decodeReceiverChain(encoded); expect(decoded).toBeDefined(); @@ -130,4 +143,17 @@ describe('foldReceiverChain', () => { }), ).toBeUndefined(); }); + + it('does NOT climb to module scope when hoistTypeBindingsToModule is off', () => { + // The flag exists because TypeScript hoists a method's return-type binding + // OUT of the class body, so the fold must walk to Module scope to find it. + // Languages that do NOT hoist must not get that walk — climbing anyway is + // how an unrelated module-level binding of the same name gets picked up, + // which is exactly what the flag's own contract warns against. Same chain, + // opposite answers, so this pins the branch rather than the happy path. + expect(foldWith('1|svc|cgetUser', { hoistTypeBindingsToModule: true })).toMatchObject({ + qualifiedName: 'User', + }); + expect(foldWith('1|svc|cgetUser', { hoistTypeBindingsToModule: false })).toBeUndefined(); + }); }); diff --git a/gitnexus/test/unit/scope-resolution/unresolved-receivers.test.ts b/gitnexus/test/unit/scope-resolution/unresolved-receivers.test.ts index 173cce9ae9..7d6ad9dfed 100644 --- a/gitnexus/test/unit/scope-resolution/unresolved-receivers.test.ts +++ b/gitnexus/test/unit/scope-resolution/unresolved-receivers.test.ts @@ -7,13 +7,14 @@ import { describe, it, expect } from 'vitest'; import { MAX_UNRESOLVED_RECEIVER_MEMBERS, + lookupUnresolvedCallCount, summarizeUnresolvedReceivers, } from '../../../src/core/ingestion/scope-resolution/unresolved-receivers.js'; import type { ResolutionOutcome } from '../../../src/core/ingestion/scope-resolution/resolution-outcome.js'; const range = { startLine: 1, startCol: 0, endLine: 1, endCol: 1 }; -function dropped(name: string): ResolutionOutcome { +function dropped(name: string, siteKind: 'call' | 'read' | 'write' = 'call'): ResolutionOutcome { return { kind: 'suppressed', reason: 'receiver-unresolved', @@ -22,6 +23,7 @@ function dropped(name: string): ResolutionOutcome { filePath: 'a.py', name, range, + siteKind, }; } @@ -80,4 +82,48 @@ describe('summarizeUnresolvedReceivers', () => { const b = summarizeUnresolvedReceivers([dropped('c'), dropped('b'), dropped('a')]); expect(Object.keys(a!.counts)).toEqual(Object.keys(b!.counts)); }); + + it('counts CALL sites only — a property read or write is not a dropped call', () => { + // Case 0's recorder gates on the receiver's punctuation, not on what the + // reference IS, so reads and writes land in the same bucket as lost calls + // (25 of 124 on the fixture corpus). Counting them made the consumer's + // "N call sites invoking X were dropped" literally false. + const summary = summarizeUnresolvedReceivers([ + dropped('save', 'call'), + dropped('name', 'write'), + dropped('kind', 'read'), + ]); + expect(summary).toMatchObject({ counts: { save: 1 }, totalSites: 1 }); + expect(summary?.counts).not.toHaveProperty('name'); + expect(summary?.counts).not.toHaveProperty('kind'); + }); + + it('returns undefined when every drop is a property access', () => { + expect(summarizeUnresolvedReceivers([dropped('name', 'write')])).toBeUndefined(); + }); + + it('does not leak Object.prototype members through the counts lookup', () => { + // `counts` is revived from JSON and carries `Object.prototype`, so a bare + // `counts[symName]` returns a FUNCTION for these names — and `NaN <= 0` is + // false, so a `<= 0` guard lets it through. `impact({target:"constructor"})` + // then reported `epistemic: 'lower-bound'` and interpolated + // `function Object() { [native code] }` as the call count. + const summary = summarizeUnresolvedReceivers([dropped('save')]); + for (const polluted of [ + 'constructor', + 'toString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toLocaleString', + '__proto__', + ]) { + expect(lookupUnresolvedCallCount(summary, polluted)).toBeUndefined(); + } + // A genuinely recorded name still reads back. + expect(lookupUnresolvedCallCount(summary, 'save')).toBe(1); + expect(lookupUnresolvedCallCount(summary, 'neverRecorded')).toBeUndefined(); + expect(lookupUnresolvedCallCount(undefined, 'save')).toBeUndefined(); + }); });