From 10d872529ed43896da6d2e3002eddab1561e55d3 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Fri, 31 Jul 2026 20:31:20 +0000 Subject: [PATCH] fix(resolution): name-free steps were vetoed by the construction selector (#2766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A defect I introduced two commits earlier, not a pre-existing one. `foldReceiverChain` opens each step with: if (options.constructionSyntax?.selector === step.name) return undefined; Correct as written. It became wrong the moment name-free step kinds were added: `step.name` is `undefined` for `await` and `index`, and `constructionSyntax?.selector` is `undefined` for any language that declares none — so `undefined === undefined` matched EVERY name-free step and vetoed the whole fold before it ran. That is why subscript and await receivers minted a chain, fired the Case 0 gate, had a resolvable base binding, and still produced no edge. The fold logic downstream was fine; nothing downstream ever executed. Three reading passes missed it because the line looks obviously correct — you have to watch `step.name` be `undefined` at runtime. An instrumented run found it immediately, which is the lesson: when captures, plumbing, payload and gate all verify clean, stop reading and instrument. Guarded on `step.name !== undefined`. Load-bearing, not defensive. Shape matrix, RESOLVES 46 -> 55, VISIBLE-GAP 32 -> 23: awaitParen now resolves in typescript, python, csharp, kotlin, dart indexElement now resolves in typescript, cpp, go, python, rust, kotlin Every canonical TypeScript shape now resolves, which broke the #2744 drop-recorder test: it needs a receiver that FAILS to type, and its own comment records that `!` served until structural typing resolved it, then await-paren served until this fix. Both were shapes the resolver merely did not SUPPORT yet, so each improvement moved the goalposts. Re-fixtured to an UNANNOTATED parameter — no type information exists, so no resolver work can type it. Stable by construction rather than by not-yet-implemented. Verified: resolver integration + scope-resolution unit 4367 green, typescript resolver 270 green, scope-capture unchanged (this is resolution, not capture), callDrops unchanged at 102. Co-Authored-By: Claude Opus 5 (1M context) --- .../bench/receiver-resolution/baseline.json | 18 +++++++++--------- .../passes/compound-receiver.ts | 10 +++++++++- .../integration/resolvers/typescript.test.ts | 17 +++++++++++------ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/gitnexus/bench/receiver-resolution/baseline.json b/gitnexus/bench/receiver-resolution/baseline.json index f5264af5e..32191bd8b 100644 --- a/gitnexus/bench/receiver-resolution/baseline.json +++ b/gitnexus/bench/receiver-resolution/baseline.json @@ -29,9 +29,9 @@ "plainDeepChain": "RESOLVES", "optionalChain": "RESOLVES", "nonNullAssert": "RESOLVES", - "awaitParen": "VISIBLE-GAP", + "awaitParen": "RESOLVES", "explicitTypeArgs": "RESOLVES", - "indexElement": "VISIBLE-GAP", + "indexElement": "RESOLVES", "fourHopChain": "RESOLVES", "fieldReceiverCall": "RESOLVES", "decoratedReceiverBase": "N/A", @@ -60,7 +60,7 @@ "nonNullAssert": "N/A", "awaitParen": "N/A", "explicitTypeArgs": "VISIBLE-GAP", - "indexElement": "VISIBLE-GAP", + "indexElement": "RESOLVES", "fieldReceiverCall": "INVISIBLE-GAP", "decoratedReceiverBase": "N/A", "decoratedFieldType": "INVISIBLE-GAP" @@ -72,7 +72,7 @@ "nonNullAssert": "N/A", "awaitParen": "N/A", "explicitTypeArgs": "N/A", - "indexElement": "VISIBLE-GAP", + "indexElement": "RESOLVES", "fieldReceiverCall": "RESOLVES", "decoratedReceiverBase": "RESOLVES", "decoratedFieldType": "RESOLVES" @@ -94,9 +94,9 @@ "plainDeepChain": "RESOLVES", "optionalChain": "N/A", "nonNullAssert": "N/A", - "awaitParen": "VISIBLE-GAP", + "awaitParen": "RESOLVES", "explicitTypeArgs": "N/A", - "indexElement": "VISIBLE-GAP", + "indexElement": "RESOLVES", "fieldReceiverCall": "RESOLVES", "decoratedReceiverBase": "N/A", "decoratedFieldType": "RESOLVES" @@ -118,7 +118,7 @@ "plainDeepChain": "RESOLVES", "optionalChain": "INVISIBLE-GAP", "nonNullAssert": "VISIBLE-GAP", - "awaitParen": "VISIBLE-GAP", + "awaitParen": "RESOLVES", "explicitTypeArgs": "VISIBLE-GAP", "indexElement": "VISIBLE-GAP", "fieldReceiverCall": "RESOLVES", @@ -144,7 +144,7 @@ "nonNullAssert": "N/A", "awaitParen": "N/A", "explicitTypeArgs": "INVISIBLE-GAP", - "indexElement": "VISIBLE-GAP", + "indexElement": "RESOLVES", "fieldReceiverCall": "RESOLVES", "decoratedReceiverBase": "RESOLVES", "decoratedFieldType": "INVISIBLE-GAP" @@ -168,7 +168,7 @@ "nonNullAssert": "VISIBLE-GAP", "awaitParen": "RESOLVES", "explicitTypeArgs": "RESOLVES", - "indexElement": "VISIBLE-GAP", + "indexElement": "RESOLVES", "fieldReceiverCall": "RESOLVES", "decoratedReceiverBase": "N/A", "decoratedFieldType": "RESOLVES" 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 007b06d34..6096476c8 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts @@ -479,7 +479,15 @@ export function foldReceiverChain( // first. That turned a correct edge into a WRONG one (Ruby // `Factory.new.run` → `Product.run`), which is the failure mode this whole // line of work exists to avoid. Decline and let the cascade answer. - if (options.constructionSyntax?.selector === step.name) return undefined; + // `step.name !== undefined` is load-bearing, not defensive. The name-free + // step kinds (`await`, `index`) carry no name, and a language with no + // `constructionSyntax` has no selector — so the bare equality below was + // `undefined === undefined`, which matched EVERY name-free step and vetoed + // the whole fold before it ran. That is why subscript and await receivers + // minted a chain, fired the gate, and still produced no edge. + if (step.name !== undefined && options.constructionSyntax?.selector === step.name) { + return undefined; + } // A position whose declared type named no class can only be advanced by an // unwrapping step. Folding an ordinary member off it would look the member diff --git a/gitnexus/test/integration/resolvers/typescript.test.ts b/gitnexus/test/integration/resolvers/typescript.test.ts index d01f68609..82a5b6b6e 100644 --- a/gitnexus/test/integration/resolvers/typescript.test.ts +++ b/gitnexus/test/integration/resolvers/typescript.test.ts @@ -3344,12 +3344,17 @@ export class Service { `, 'main.ts': `import { Service } from './models'; -export async function droppedCall(svc: Service): Promise { - // An await-parenthesized receiver. Structural typing does NOT cover this - // shape — \`extractMixedChain\` reaches \`await …\`, which is not a chain node, - // so no chain is minted and the site still reaches the drop recorder. The - // \`!\` spelling used to serve here until structural typing resolved it. - (await svc.getUserAsync()).save(); +export async function droppedCall(svc): Promise { + // An UNANNOTATED parameter. The chain mints fine, but the base has no type + // binding to resolve against, so the site reaches the drop recorder. + // + // Third fixture for this case: \`!\` served until structural typing resolved + // it, then the await-parenthesized form served until name-free step kinds + // resolved that too. Both were shapes the resolver merely did not SUPPORT + // yet, so each fix moved the goalposts. An untyped receiver carries no type + // information at all, so no amount of resolver work can type it — which is + // what makes it a stable choice rather than the next one to be fixed. + svc.getUser().save(); } export function droppedWrite(svc: Service | null): void {