Skip to content

fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has - #13444

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-12358-wrapper-hop-shape-check
Aug 30, 2026
Merged

os-project-manager merged 2 commits into
mainfrom
claude/issue-12358-wrapper-hop-shape-check

Conversation

@claude

@claude claude Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #12358

#11921 gave isReadCall's vocabulary match a shape test — contradictsDriverReadShape — and applied it at exactly one place. The wrapper hop resolved a callee name against the flat, file-scoped functionBodies index with no equivalent test and no receiver check, so any call whose calleeName happened to equal a function declared in the same file was followed into that body, whatever it was called on and whatever it was passed.

That runs in the unsafe direction. Every other narrowness in this gate under-counts; this one invents a member of the read-seam denominator that #5186, #6451, #9165, #8845 and #8901 are all quoted against — and it is silent, because the fake seam prints in --list looking exactly like a real one.

The live instance is still there

Located by content, not by the card's line numbers (which were treated as claims, and happen to be right): in packages/metadata-protocol/src/sys-metadata-repository.ts, close() at 1343 calls w.terminate(); terminate is the local const arrow at 1246 whose only call is self.watchers.delete(subscription) on the watcher registry Set declared at 303. calleeName reads that as delete, and the hop resolved delete to this file's async delete(ref, opts) at 653.

Re-derived, not inherited

The card's table was measured five days ago on 3ddad51b5c. The denominator has since moved 64 to 66, so everything was re-run on this branch's base 5f0a9c4a. The delta reproduced exactly — same +8, the same 8 seams by identity, ablation still removing exactly two:

recognizer read seams
today, walkSameTickInclusive, depth 2 66
probe, walkAll, depth 2 74
probe, with the delete wrapper hop refused 72

The ablation is the control, not the fix. Refusing the delete hop outright drops two seams — close to terminate (the fake) and promoteDraft to dropPromotedDraftRow (a real await this.delete(ref, ...)). In a summary that is indistinguishable from the correct outcome, which is why the pair is pinned in the self-test.

The census, which the card required before any narrowing

Both candidate shapes measured over the whole scan root, on today's recognizer and under the probe that arms the defect:

wrapper-hop guard today walkAll removes
none (before this change) 66 74 —
A. receiver, bare identifiers admitted 66 73 the fake only
A'. receiver, strict this/self only 66 73 the fake only
B1. contradictsDriverReadShape called on the hop 66 74 nothing
B2. required-parameter count 66 73 the fake only
A + B2 (taken) 66 73 the fake only

Every row adds zero seams — measured, not argued: the added-seam set is empty in all six runs.

B1 is a vacuous fix, and the census is what says so. Read literally, "give the hop a contradictsDriverReadShape-style argument test" means calling that predicate on the hop: refuse when the first argument is a function literal. self.watchers.delete(subscription) passes an identifier, so it refuses nothing — on this tree or in principle. B2 is that candidate read at the level the vocabulary-side predicate actually works at: not the literal predicate, but its method — refute the call against the contract already in hand.

Why A + B2 rather than either. The measurement cannot separate them; all three cost nothing and remove exactly the fake seam. Their failure modes are independent and each is one edit away in live code: A alone returns the fake seam if delete's second parameter becomes optional; B2 alone returns it if the registry is reached through a bare identifier after a destructure. The conjunction measures at the same zero cost, so both are asked.

What ships

contradictsWrapperResolution(node, body) beside the predicate it mirrors, asked once at the hop. Two clauses, neither a new vocabulary, neither carrying a staleness obligation — both read off the declaration the index already holds:

  • receiver — a receiver that is itself a property access, an index, or a call result names a member of another object; resolving it to this file's body is a name collision by construction. Bare identifiers are admitted deliberately, because const self = this; is how the live file reaches its own members from a closure.
  • arity — a call supplying fewer arguments than the resolved declaration requires is not a call to it; in a type-checked tree it would not compile. Spread calls are exempt, because the argument count is not knowable.

It reads the declaration through body.parent, which is available because setParentNodes is fixed true in scripts/ts-parse.mjs — so functionBodies keeps its value shape and none of its other consumers move.

This is not the receiver allowlist contradictsDriverReadShape rejects. That objection is measured and it stands, for the vocabulary hit, where the receiver is the driver binding and requiring a this-rooted receiver drops 14 of 66 seams, 12 of them real. This clause is not a list of receiver names and does not require this: it reads receiver depth, admits every bare identifier, and measures at zero seams over the whole scan root.

The deliverable is a measured zero-delta

The defect is latent — today's recognizer never reaches the fake read, because the wrapper recursion's walkSameTickInclusive stops at the withTxn callback. So this PR changes no behaviour on today's tree, and that is the point:

  • the full checker output is byte-identical before and after — verdicts, counts and every --list line (diff of the complete run, not just the totals);
  • rebuilt against the widening that would arm the defect, the probe goes 74 to 73, removing close to terminate and keeping promoteDraft to dropPromotedDraftRow;
  • no baseline entry was added and none was needed: an entry says a human read a seam, not that a rule stopped inventing one.

Tests

Six new read-seam self-test cases (45 to 51). Every one is non-vacuous, proven by ablation rather than asserted — each was run with a specific mutation and predicted to redden, and all five predictions held:

ablation fixtures that redden
contradictsWrapperResolution returns false the two expectSeams: 0 cases (1 seam each)
blunt: refuse the delete hop by name the real-chain, spread and optional-parameter cases
receiver restricted to this/super the bare-identifier admission case
count declared parameters, not required the optional-parameter case
drop the spread exemption the spread case

The bare-identifier admission case was added because the second ablation found nothing to redden without it — tightening the receiver clause to this/super passed the entire suite.

Gates run on final head 33643001, after the last commit: the 13 path-derived families from node scripts/pm/dispatch-gates.mjs (derived from my own diff, not from a hand-written list), plus the gate-script convention obligations pnpm check:pm-dispatch-gates, node scripts/pm/bare-root-worklist.mjs --self-test and node scripts/check-self-test-wired.mjs, plus pnpm check:nul-bytes. All green. pnpm check:durability-log-level prints 66 read seam(s), none invents an unreported answer, and --self-test prints 63 case(s) passed and 51 case(s) passed. Full-repo pnpm lint was run under the shared verify lock: 5490 files, 0 errors, 0 warnings.

Two families exited non-zero for reasons that are not findings and are recorded as NOT MEASURED: node scripts/check-test-completeness.mjs exits 3 with PREREQUISITE NOT MET (it grades a saved turbo run test log that CI tees and this run has none), and the dispatch brief's check-self-test-wired is a script path, not a pnpm script — pnpm check:self-test-wired does not exist and exits 254; the real gate node scripts/check-self-test-wired.mjs is green.

No changeset: this is a CI-internal gate script that publishes nothing from any package, which .github/workflows/lint.yml calls the textbook skip-changeset case in as many words. All three prior merged PRs on this exact file — including #12137, this card's direct predecessor — were single-file with no changeset. The skip-changeset label is applied.

Out of scope, deliberately untouched and still open: #12360, the MAX_READ_WRAPPER_DEPTH = 2 bound, which the table above shows costs the census 6 further seams. It is a separate card on the same file.


Generated by Claude Code

os-project-manager and others added 2 commits August 30, 2026 12:12
…ary hit already has

#11921 gave `isReadCall`'s vocabulary match a shape test
(`contradictsDriverReadShape`) and applied it at exactly one place. The wrapper
hop resolved a callee name against the flat, file-scoped `functionBodies` index
with no equivalent test and no receiver check, so any call whose name collided
with a same-file declaration was followed into that body.

`contradictsWrapperResolution` asks the same question of the resolved
declaration: a receiver that is itself a property access, index or call result
names another object's member, and a call supplying fewer arguments than the
declaration requires is not a call to it. Measured zero-delta on today's tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
…e check

The receiver clause reads receiver DEPTH, not `this`-rootedness. Nothing
asserted that admission, so tightening it to `this`/`super` only passed the
whole suite. `const self = this;` is how the live file reaches its own members
from a closure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 30, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 30, 2026 12:35
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 30, 2026
Merged via the queue into main with commit b9186f6 Aug 30, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-12358-wrapper-hop-shape-check branch August 30, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant