diff --git a/gitnexus/bench/receiver-resolution/BASELINE.md b/gitnexus/bench/receiver-resolution/BASELINE.md index fcf326de9..4d1d48aa7 100644 --- a/gitnexus/bench/receiver-resolution/BASELINE.md +++ b/gitnexus/bench/receiver-resolution/BASELINE.md @@ -1,5 +1,46 @@ # Receiver-resolution baseline +## U6 — the depth cap does NOT limit resolution. Measured, not raised. + +The premise was that a chain deeper than `MAX_CHAIN_DEPTH` (3) is discarded +whole rather than truncated, so a 4-hop builder chain "contributes nothing at +all". The first half is true; the second is not. + +`fourHopChain` was added to the TypeScript corpus as a declared extra +specifically to make the question answerable — without a chain longer than the +cap, raising the cap measures nothing: + +```ts +root.getSvc().getUser().address.getCity().save(); +// ^step1 ^step2 ^step3 ^step4 receiver of `save` = 4 steps +``` + +| Cap | Chain minted? | Cell state | +|---|---|---| +| 3 | **none** (confirmed by probing the emitter directly) | **RESOLVES** | +| 4 | `2\|root\|cgetSvc\|cgetUser\|faddress\|cgetCity` | RESOLVES | + +The site resolves at BOTH depths. At 3 it resolves through the text cascade, +which owns the fallback path and runs to its own +`COMPOUND_RECEIVER_MAX_DEPTH` of 8. + +**So the cap bounds which chains are typed structurally, not which calls +resolve.** Raising it moves work from the cascade to the fold without changing a +single edge — measured across the whole matrix: totals identical at 3 and 4, +`callDrops` 102 at both. + +Left at 3. The fixture is committed so the next person to reach for this number +inherits the measurement instead of the intuition. + +What DID need fixing: `unwrapTransparentReceiver` shared `MAX_CHAIN_DEPTH` as +its iteration bound. The two answer unrelated questions — how many chain hops do +we type, versus how many redundant parens might someone write — so raising the +chain cap would have silently widened the paren peel as a side effect. That +coupling got worse when the await/subscript work added a peel call at loop +entry. Now `MAX_TRANSPARENT_WRAPPER_DEPTH`, its own constant. + +--- + ## U9 — the epistemic hedge has TWO producers, and only one is a defect `impact` reports `epistemic: 'lower-bound'` for two independent reasons that were diff --git a/gitnexus/bench/receiver-resolution/baseline.json b/gitnexus/bench/receiver-resolution/baseline.json index 3ab690396..f5264af5e 100644 --- a/gitnexus/bench/receiver-resolution/baseline.json +++ b/gitnexus/bench/receiver-resolution/baseline.json @@ -32,6 +32,7 @@ "awaitParen": "VISIBLE-GAP", "explicitTypeArgs": "RESOLVES", "indexElement": "VISIBLE-GAP", + "fourHopChain": "RESOLVES", "fieldReceiverCall": "RESOLVES", "decoratedReceiverBase": "N/A", "decoratedFieldType": "RESOLVES" diff --git a/gitnexus/bench/receiver-resolution/measure.mjs b/gitnexus/bench/receiver-resolution/measure.mjs index bdd1e5fb3..5d183a377 100644 --- a/gitnexus/bench/receiver-resolution/measure.mjs +++ b/gitnexus/bench/receiver-resolution/measure.mjs @@ -140,11 +140,22 @@ const CORPORA = [ { lang: 'typescript', ext: '.ts', + // `fourHopChain` exists to make MAX_CHAIN_DEPTH answerable. Without a chain + // longer than the cap, raising the cap measures nothing and the question + // "what does depth N buy?" has no instrument behind it. + extraShapeIds: ['fourHopChain'], support: { - 'models.ts': `export class Address { + 'models.ts': `export class City { save(): void {} } +export class Address { + save(): void {} + getCity(): City { + return new City(); + } +} + export class User { name: string = ''; address: Address = new Address(); @@ -185,6 +196,26 @@ export class Service { }, { id: 'explicitTypeArgs', member: 'save', body: 'svc.getTyped().save();', note: 'PF4' }, { id: 'indexElement', member: 'save', body: 'repos[0].save();', note: 'PF5' }, + { + // FOUR STEPS in the receiver of `save`: getSvc, getUser, address, + // getCity. One more than the pre-U6 cap of 3, so the chain is DISCARDED + // WHOLE at depth 3 and types end to end at depth 4 — the whole basis + // for choosing the number. (An earlier version of this fixture had only + // three steps and therefore resolved at both depths, measuring nothing.) + id: 'fourHopChain', + member: 'save', + body: 'root.getSvc().getUser().address.getCity().save();', + raw: `class Root { + getSvc(): Service { + return new Service(); + } +} + +export function fourHopChain(root: Root): void { + root.getSvc().getUser().address.getCity().save(); +} +`, + }, { id: 'fieldReceiverCall', member: 'save', diff --git a/gitnexus/src/core/ingestion/utils/call-analysis.ts b/gitnexus/src/core/ingestion/utils/call-analysis.ts index 7531b03f2..e13b71aa8 100644 --- a/gitnexus/src/core/ingestion/utils/call-analysis.ts +++ b/gitnexus/src/core/ingestion/utils/call-analysis.ts @@ -16,6 +16,24 @@ export const CALL_EXPRESSION_TYPES = new Set([ /** * Hard limit on chain depth to prevent runaway recursion. * For `a.b().c().d()`, the chain has depth 2 (b and c before d). + * + * A chain deeper than this is DISCARDED WHOLE, not truncated: + * `extractMixedChain` returns an undefined base and the encoder refuses to mint + * a partial chain, because a base-side prefix decodes cleanly as a shorter, + * complete-looking chain and would type the receiver against the wrong member. + * Correct, but it means a builder chain one hop too long contributes nothing at + * all rather than degrading. + * + * DELIBERATELY NOT RAISED. Measured (see bench/receiver-resolution/BASELINE.md, + * `fourHopChain`): a 4-step chain mints NOTHING at this cap — confirmed by + * probing the emitter directly — and the site still RESOLVES, because the text + * cascade that owns the fallback path runs to `COMPOUND_RECEIVER_MAX_DEPTH` (8) + * and answers where the structural fold declined. + * + * So the cap bounds which chains are typed STRUCTURALLY, not which calls + * resolve. Raising it moves work from the cascade to the fold without changing + * any edge, and the fixture that proves it is committed so the next person to + * reach for this number has the measurement rather than the intuition. */ export const MAX_CHAIN_DEPTH = 3; @@ -522,11 +540,25 @@ const TRANSPARENT_RECEIVER_WRAPPERS = new Set([ 'parenthesized_expression', // `(svc)` ]); +/** + * Iteration bound for the wrapper peel. Its OWN constant, not `MAX_CHAIN_DEPTH`. + * + * The two answer unrelated questions — "how many chain hops do we type?" versus + * "how many redundant parens might someone write?" — and sharing one number + * meant raising the chain cap silently widened this loop as a side effect. That + * coupling is easy to miss precisely because the shared name reads as + * intentional. `((x))` nests twice; nothing real nests deeply. + */ +const MAX_TRANSPARENT_WRAPPER_DEPTH = 3; + /** Peel transparent wrappers off a base receiver node. */ function unwrapTransparentReceiver(node: SyntaxNode): SyntaxNode { let current = node; - // Bounded: `((x))` nests twice; nothing real nests deeply. - for (let i = 0; i < MAX_CHAIN_DEPTH && TRANSPARENT_RECEIVER_WRAPPERS.has(current.type); i++) { + for ( + let i = 0; + i < MAX_TRANSPARENT_WRAPPER_DEPTH && TRANSPARENT_RECEIVER_WRAPPERS.has(current.type); + i++ + ) { const inner = current.namedChildren?.find((c) => c !== null); if (inner === undefined || inner === null) break; current = inner; diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 1914b0101..3a80eaa71 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -283,7 +283,10 @@ export interface ExtractedCall { * `svc.getUser().save()` → chain=[{kind:'call',name:'getUser'}], receiverName='svc' * `user.address.save()` → chain=[{kind:'field',name:'address'}], receiverName='user' * `svc.getUser().address.save()` → chain=[{kind:'call',name:'getUser'},{kind:'field',name:'address'}] - * Length is capped at MAX_CHAIN_DEPTH (3). + * Length is capped at MAX_CHAIN_DEPTH. Deliberately NOT restating the number + * here: this comment previously hardcoded `(3)` and would have drifted the + * moment the cap moved, which is exactly the kind of stale doc that reads as + * authoritative. */ receiverMixedChain?: MixedChainStep[]; argTypes?: (string | undefined)[];