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

fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has#13444
os-project-manager merged 2 commits into
mainfrom
claude/issue-12358-wrapper-hop-shape-check

Conversation

@claude

@claudeclaudeBot 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:

recognizerread seams
today, walkSameTickInclusive, depth 266
probe, walkAll, depth 274
probe, with the delete wrapper hop refused72

The ablation is the control, not the fix. Refusing the delete hop outright drops two seams — close to terminate (the fake) andpromoteDraft 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 guardtodaywalkAllremoves
none (before this change)6674
A. receiver, bare identifiers admitted6673the fake only
A'. receiver, strict this/self only6673the fake only
B1. contradictsDriverReadShape called on the hop6674nothing
B2. required-parameter count6673the fake only
A + B2 (taken)6673the 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 keepingpromoteDraft 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:

ablationfixtures that redden
contradictsWrapperResolution returns falsethe two expectSeams: 0 cases (1 seam each)
blunt: refuse the delete hop by namethe real-chain, spread and optional-parameter cases
receiver restricted to this/superthe bare-identifier admission case
count declared parameters, not requiredthe optional-parameter case
drop the spread exemptionthe 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-managerand 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
@claudeclaudeBot 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 queueAug 30, 2026
Merged via the queue into main with commit b9186f6Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-project-manager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has by claude[bot] · Pull Request #13444 · objectstack-ai/objectstack · GitHub
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

fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has#13444
os-project-manager merged 2 commits into
mainfrom
claude/issue-12358-wrapper-hop-shape-check

Conversation

@claude

@claudeclaudeBot 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:

recognizerread seams
today, walkSameTickInclusive, depth 266
probe, walkAll, depth 274
probe, with the delete wrapper hop refused72

The ablation is the control, not the fix. Refusing the delete hop outright drops two seams — close to terminate (the fake) andpromoteDraft 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 guardtodaywalkAllremoves
none (before this change)6674
A. receiver, bare identifiers admitted6673the fake only
A'. receiver, strict this/self only6673the fake only
B1. contradictsDriverReadShape called on the hop6674nothing
B2. required-parameter count6673the fake only
A + B2 (taken)6673the 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 keepingpromoteDraft 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:

ablationfixtures that redden
contradictsWrapperResolution returns falsethe two expectSeams: 0 cases (1 seam each)
blunt: refuse the delete hop by namethe real-chain, spread and optional-parameter cases
receiver restricted to this/superthe bare-identifier admission case
count declared parameters, not requiredthe optional-parameter case
drop the spread exemptionthe 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-managerand 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
@claudeclaudeBot 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 queueAug 30, 2026
Merged via the queue into main with commit b9186f6Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-project-manager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has by claude[bot] · Pull Request #13444 · objectstack-ai/objectstack · GitHub
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

fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has#13444
os-project-manager merged 2 commits into
mainfrom
claude/issue-12358-wrapper-hop-shape-check

Conversation

@claude

@claudeclaudeBot 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:

recognizerread seams
today, walkSameTickInclusive, depth 266
probe, walkAll, depth 274
probe, with the delete wrapper hop refused72

The ablation is the control, not the fix. Refusing the delete hop outright drops two seams — close to terminate (the fake) andpromoteDraft 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 guardtodaywalkAllremoves
none (before this change)6674
A. receiver, bare identifiers admitted6673the fake only
A'. receiver, strict this/self only6673the fake only
B1. contradictsDriverReadShape called on the hop6674nothing
B2. required-parameter count6673the fake only
A + B2 (taken)6673the 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 keepingpromoteDraft 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:

ablationfixtures that redden
contradictsWrapperResolution returns falsethe two expectSeams: 0 cases (1 seam each)
blunt: refuse the delete hop by namethe real-chain, spread and optional-parameter cases
receiver restricted to this/superthe bare-identifier admission case
count declared parameters, not requiredthe optional-parameter case
drop the spread exemptionthe 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-managerand 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
@claudeclaudeBot 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 queueAug 30, 2026
Merged via the queue into main with commit b9186f6Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-project-manager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has by claude[bot] · Pull Request #13444 · objectstack-ai/objectstack · GitHub
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

fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has#13444
os-project-manager merged 2 commits into
mainfrom
claude/issue-12358-wrapper-hop-shape-check

Conversation

@claude

@claudeclaudeBot 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:

recognizerread seams
today, walkSameTickInclusive, depth 266
probe, walkAll, depth 274
probe, with the delete wrapper hop refused72

The ablation is the control, not the fix. Refusing the delete hop outright drops two seams — close to terminate (the fake) andpromoteDraft 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 guardtodaywalkAllremoves
none (before this change)6674
A. receiver, bare identifiers admitted6673the fake only
A'. receiver, strict this/self only6673the fake only
B1. contradictsDriverReadShape called on the hop6674nothing
B2. required-parameter count6673the fake only
A + B2 (taken)6673the 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 keepingpromoteDraft 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:

ablationfixtures that redden
contradictsWrapperResolution returns falsethe two expectSeams: 0 cases (1 seam each)
blunt: refuse the delete hop by namethe real-chain, spread and optional-parameter cases
receiver restricted to this/superthe bare-identifier admission case
count declared parameters, not requiredthe optional-parameter case
drop the spread exemptionthe 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-managerand 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
@claudeclaudeBot 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 queueAug 30, 2026
Merged via the queue into main with commit b9186f6Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-project-manager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has by claude[bot] · Pull Request #13444 · objectstack-ai/objectstack · GitHub
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

fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has#13444
os-project-manager merged 2 commits into
mainfrom
claude/issue-12358-wrapper-hop-shape-check

Conversation

@claude

@claudeclaudeBot 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:

recognizerread seams
today, walkSameTickInclusive, depth 266
probe, walkAll, depth 274
probe, with the delete wrapper hop refused72

The ablation is the control, not the fix. Refusing the delete hop outright drops two seams — close to terminate (the fake) andpromoteDraft 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 guardtodaywalkAllremoves
none (before this change)6674
A. receiver, bare identifiers admitted6673the fake only
A'. receiver, strict this/self only6673the fake only
B1. contradictsDriverReadShape called on the hop6674nothing
B2. required-parameter count6673the fake only
A + B2 (taken)6673the 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 keepingpromoteDraft 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:

ablationfixtures that redden
contradictsWrapperResolution returns falsethe two expectSeams: 0 cases (1 seam each)
blunt: refuse the delete hop by namethe real-chain, spread and optional-parameter cases
receiver restricted to this/superthe bare-identifier admission case
count declared parameters, not requiredthe optional-parameter case
drop the spread exemptionthe 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-managerand 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
@claudeclaudeBot 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 queueAug 30, 2026
Merged via the queue into main with commit b9186f6Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-project-manager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has by claude[bot] · Pull Request #13444 · objectstack-ai/objectstack · GitHub
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

fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has#13444
os-project-manager merged 2 commits into
mainfrom
claude/issue-12358-wrapper-hop-shape-check

Conversation

@claude

@claudeclaudeBot 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:

recognizerread seams
today, walkSameTickInclusive, depth 266
probe, walkAll, depth 274
probe, with the delete wrapper hop refused72

The ablation is the control, not the fix. Refusing the delete hop outright drops two seams — close to terminate (the fake) andpromoteDraft 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 guardtodaywalkAllremoves
none (before this change)6674
A. receiver, bare identifiers admitted6673the fake only
A'. receiver, strict this/self only6673the fake only
B1. contradictsDriverReadShape called on the hop6674nothing
B2. required-parameter count6673the fake only
A + B2 (taken)6673the 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 keepingpromoteDraft 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:

ablationfixtures that redden
contradictsWrapperResolution returns falsethe two expectSeams: 0 cases (1 seam each)
blunt: refuse the delete hop by namethe real-chain, spread and optional-parameter cases
receiver restricted to this/superthe bare-identifier admission case
count declared parameters, not requiredthe optional-parameter case
drop the spread exemptionthe 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-managerand 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
@claudeclaudeBot 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 queueAug 30, 2026
Merged via the queue into main with commit b9186f6Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-project-manager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has by claude[bot] · Pull Request #13444 · objectstack-ai/objectstack · GitHub
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

fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has#13444
os-project-manager merged 2 commits into
mainfrom
claude/issue-12358-wrapper-hop-shape-check

Conversation

@claude

@claudeclaudeBot 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:

recognizerread seams
today, walkSameTickInclusive, depth 266
probe, walkAll, depth 274
probe, with the delete wrapper hop refused72

The ablation is the control, not the fix. Refusing the delete hop outright drops two seams — close to terminate (the fake) andpromoteDraft 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 guardtodaywalkAllremoves
none (before this change)6674
A. receiver, bare identifiers admitted6673the fake only
A'. receiver, strict this/self only6673the fake only
B1. contradictsDriverReadShape called on the hop6674nothing
B2. required-parameter count6673the fake only
A + B2 (taken)6673the 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 keepingpromoteDraft 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:

ablationfixtures that redden
contradictsWrapperResolution returns falsethe two expectSeams: 0 cases (1 seam each)
blunt: refuse the delete hop by namethe real-chain, spread and optional-parameter cases
receiver restricted to this/superthe bare-identifier admission case
count declared parameters, not requiredthe optional-parameter case
drop the spread exemptionthe 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-managerand 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
@claudeclaudeBot 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 queueAug 30, 2026
Merged via the queue into main with commit b9186f6Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-project-manager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has by claude[bot] · Pull Request #13444 · objectstack-ai/objectstack · GitHub
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

fix(devx): the read-seam wrapper hop gets the shape check the vocabulary hit already has#13444
os-project-manager merged 2 commits into
mainfrom
claude/issue-12358-wrapper-hop-shape-check

Conversation

@claude

@claudeclaudeBot 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:

recognizerread seams
today, walkSameTickInclusive, depth 266
probe, walkAll, depth 274
probe, with the delete wrapper hop refused72

The ablation is the control, not the fix. Refusing the delete hop outright drops two seams — close to terminate (the fake) andpromoteDraft 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 guardtodaywalkAllremoves
none (before this change)6674
A. receiver, bare identifiers admitted6673the fake only
A'. receiver, strict this/self only6673the fake only
B1. contradictsDriverReadShape called on the hop6674nothing
B2. required-parameter count6673the fake only
A + B2 (taken)6673the 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 keepingpromoteDraft 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:

ablationfixtures that redden
contradictsWrapperResolution returns falsethe two expectSeams: 0 cases (1 seam each)
blunt: refuse the delete hop by namethe real-chain, spread and optional-parameter cases
receiver restricted to this/superthe bare-identifier admission case
count declared parameters, not requiredthe optional-parameter case
drop the spread exemptionthe 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-managerand 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
@claudeclaudeBot 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 queueAug 30, 2026
Merged via the queue into main with commit b9186f6Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-project-manager