Observation class — recording only, no verdict asserted, no pm:queue. Unassigned. Found while verifying the six depth-admitted seams #12360 asked for (PR #13472); deliberately not fixed there — that card's declared scope is the depth bound, and this is a resolution defect the bound merely limits the blast radius of.
Measured on origin/main @ 71627f7b4e.
What I found
indexFunctionBodies in scripts/check-durability-degradation-log-level.mjs builds a flat, file-scoped index keyed by bare name, and it is last-wins — byName.set(name, body) with no scope information. isReadCall's wrapper hop resolves a callee name through that index.
packages/metadata-protocol/src/protocol.ts declares lookup three times, all near-identical single-parameter async arrows:
6657 const lookup = async (t: string) => { ... [lexically encloses the call]
6717 const lookup = async (t: string) => { ...
7129 const lookup = async (t: string) => { ... [what the index returns]
(Return-type annotations elided; all three are single-parameter async arrows, which is the only property the resolution turns on.)
The call at protocol.ts:6674 — const rec = await lookup(request.type);, inside findDraft, whose own enclosing lookup is the one at 6657 — is resolved by the hop to the body at 7129. That is the third declaration, in a different method entirely, chosen only because it is last in the file.
Why the shape check cannot catch it
contradictsWrapperResolution (added by PR #13444 for #12358) asks two questions of a hop, receiver and required-parameter count. Both pass here, and correctly so:
- the callee is a bare identifier, which that predicate admits deliberately (its docblock says so:
const self = this; is how the live file reaches its own members from a closure);
- all three declarations take exactly one required parameter, so the arity clause has nothing to refute.
The guard is doing its job. Its job is name collisions across receivers, not name collisions between same-named bodies in one file, and no strengthening of either clause reaches this case as long as the colliding declarations are near-copies.
Direction of the error, and why it costs nothing today
Zero cost on this tree, measured, not assumed. All three lookup bodies read sys_metadata through this.engine.findOne, so the two seams that hop through this call (protocol.ts:10497 getMetaItemCached and :13874 saveMetaItem, both admitted at depth 3) are genuine read seams under any of the three resolutions. The verdict is right — by luck rather than by construction.
The mechanism itself has no safe direction, which is why it is worth a record. A collision where only some of the same-named bodies read would either invent a seam (the unsafe direction — a fake member of the denominator #5186, #6451, #9165, #8845 and #8901 are quoted against) or drop a real one, and nothing in the output distinguishes either case from a correct resolution.
Blast radius scales with the depth bound
Each additional wrapper hop is another name that has to be unique for the chain to stay correct. MAX_READ_WRAPPER_DEPTH is 2 today; this collision already appears at hop 3, i.e. it is reachable only in the depth-probe population. That is a measured argument against raising the bound, and it is recorded as such in the gate's header by PR #13472 — this card is the standalone record so it does not live only inside a comment about a different subject.
Sibling, not a duplicate
#13456 records the same defect class in the sibling instrument: scripts/measure-durability-swallow-family.mjs resolves a bare identifier against a whole-file body index and reaches same-named class methods, and it notes the last-wins collision property in passing. Different file, different measurement, different member population — but very likely the same remedy shape, and they should be read together. That card's suggested remedy (resolve against the lexical scope chain at the call site rather than the flat index, then pin it with a regression control) applies here unchanged.
Dedupe performed
⚠️ The REST search endpoint is unavailable from this seat and this is declared rather than papered over: GET /search/issues returns HTTP 403 for both a targeted query and a known-hit control term, so search was not used and no zero from it was trusted. Repo-scoped REST reads and writes are live (HTTP 200), so dedupe ran through a bounded repo-scoped list of the 31 open finding issues — a non-empty control — plus a local keyword grep over their titles and bodies for functionBodies, last-wins, name collision, indexFunctionBodies, wrapper resolution and lookup. Two hits: #13456, the sibling named above, and #13433, unrelated. Nothing addresses the flat index in this gate.
Generated by Claude Code
Observation class — recording only, no verdict asserted, no
pm:queue. Unassigned. Found while verifying the six depth-admitted seams #12360 asked for (PR #13472); deliberately not fixed there — that card's declared scope is the depth bound, and this is a resolution defect the bound merely limits the blast radius of.Measured on
origin/main@71627f7b4e.What I found
indexFunctionBodiesinscripts/check-durability-degradation-log-level.mjsbuilds a flat, file-scoped index keyed by bare name, and it is last-wins —byName.set(name, body)with no scope information.isReadCall's wrapper hop resolves a callee name through that index.packages/metadata-protocol/src/protocol.tsdeclareslookupthree times, all near-identical single-parameter async arrows:(Return-type annotations elided; all three are single-parameter async arrows, which is the only property the resolution turns on.)
The call at
protocol.ts:6674—const rec = await lookup(request.type);, insidefindDraft, whose own enclosinglookupis the one at 6657 — is resolved by the hop to the body at 7129. That is the third declaration, in a different method entirely, chosen only because it is last in the file.Why the shape check cannot catch it
contradictsWrapperResolution(added by PR #13444 for #12358) asks two questions of a hop, receiver and required-parameter count. Both pass here, and correctly so:const self = this;is how the live file reaches its own members from a closure);The guard is doing its job. Its job is name collisions across receivers, not name collisions between same-named bodies in one file, and no strengthening of either clause reaches this case as long as the colliding declarations are near-copies.
Direction of the error, and why it costs nothing today
Zero cost on this tree, measured, not assumed. All three
lookupbodies readsys_metadatathroughthis.engine.findOne, so the two seams that hop through this call (protocol.ts:10497getMetaItemCachedand:13874saveMetaItem, both admitted at depth 3) are genuine read seams under any of the three resolutions. The verdict is right — by luck rather than by construction.The mechanism itself has no safe direction, which is why it is worth a record. A collision where only some of the same-named bodies read would either invent a seam (the unsafe direction — a fake member of the denominator #5186, #6451, #9165, #8845 and #8901 are quoted against) or drop a real one, and nothing in the output distinguishes either case from a correct resolution.
Blast radius scales with the depth bound
Each additional wrapper hop is another name that has to be unique for the chain to stay correct.
MAX_READ_WRAPPER_DEPTHis 2 today; this collision already appears at hop 3, i.e. it is reachable only in the depth-probe population. That is a measured argument against raising the bound, and it is recorded as such in the gate's header by PR #13472 — this card is the standalone record so it does not live only inside a comment about a different subject.Sibling, not a duplicate
#13456 records the same defect class in the sibling instrument:
scripts/measure-durability-swallow-family.mjsresolves a bare identifier against a whole-file body index and reaches same-named class methods, and it notes the last-wins collision property in passing. Different file, different measurement, different member population — but very likely the same remedy shape, and they should be read together. That card's suggested remedy (resolve against the lexical scope chain at the call site rather than the flat index, then pin it with a regression control) applies here unchanged.Dedupe performed
GET /search/issuesreturns HTTP 403 for both a targeted query and a known-hit control term, so search was not used and no zero from it was trusted. Repo-scoped REST reads and writes are live (HTTP 200), so dedupe ran through a bounded repo-scoped list of the 31 openfindingissues — a non-empty control — plus a local keyword grep over their titles and bodies forfunctionBodies,last-wins,name collision,indexFunctionBodies,wrapper resolutionandlookup. Two hits: #13456, the sibling named above, and #13433, unrelated. Nothing addresses the flat index in this gate.Generated by Claude Code