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(); + }); });