Skip to content

fix(devx): price the read-seam wrapper depth bound and pin it from above - #13472

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-12360-read-wrapper-depth-bound
Aug 30, 2026
Merged

fix(devx): price the read-seam wrapper depth bound and pin it from above#13472
os-project-manager merged 1 commit into
mainfrom
claude/issue-12360-read-wrapper-depth-bound

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #12360

The card's framing is the whole job: the bound is documented, the number never was. MAX_READ_WRAPPER_DEPTH = 2 has carried an honest limitation notice since the day it was written — the walk "cannot DISCOVER a seam whose read runs outside the try block, or more than MAX_READ_WRAPPER_DEPTH hops away. A ratchet, not a proof." What has never been recorded is what that ratchet costs, and a constant whose price nobody has measured is held in place by inertia rather than by a reason.

This PR does not move the bound. It prices it, proves every seam behind the price is real, and makes the number impossible to raise in silence or to go stale in a comment.

Re-derived on the merged ref, not inherited

The card's figures (64 to 70 at depth 6, "+8/-2", "5 of the 8 deltas") were taken on 3ddad51b5c, five days and two recognizer changes ago. PR #13444 landed in this exact file hours before this branch was cut. Everything below was re-run on origin/main @ 71627f7b4e.

The headline number reproduces exactly: 6 real seams. The route to it does not, and both corrections matter to the next person who re-runs this census.

recognizer read seams
same-tick walk, depth 1 58
today — same-tick walk, depth 2 66
same-tick walk, depth 3 72
same-tick walk, depths 4 to 50 72 (saturated)
  • Saturation is at depth 3, not 6. Every one of the six chains is exactly three hops. The card probed 6 and could only read saturation as "at or below 6"; it is one hop past the bound, for all six.
  • The admitting step is +6 / -0, not "+8 / -2". The 2 were never seams leaving the population — they are two try lines re-attributed to a different first-matching callee. That still happens here (engine.ts:9741 and :10906 move from resolveMasterDetailParent(s) to mediaValueShapeStrictFor) but at depth 4, one level above the level that admits anything, and it moves no count.

Every admitted seam shown real before anything was proposed

The dispatch was explicit that a depth increase is the other direction of the risk #13444 closed, so each of the six was read at its call site — chain traced through the recognizer's own resolution, not assumed:

# seam (try line, first wrapper) chain to the read
1 protocol.ts:10497 getMetaItemCached getMetaItem, findDraft, lookup, engine.findOne
2 protocol.ts:13874 saveMetaItem getMetaItem, findDraft, lookup, engine.findOne
3 protocol.ts:14850 migrateStoredMetadata saveMetaItem, refuseUnmintableMetaType, metaTypeNamespaceExists, engine.findOne
4 protocol.ts:16502 publishPackageDrafts promoteDraftForPublish, lockWriteRefusal, getEffectiveLock, engine.findOne
5 protocol.ts:17565 duplicatePackage saveMetaItem, refuseUnmintableMetaType, metaTypeNamespaceExists, engine.findOne
6 lifecycle-service.ts:664 sweep reapObject, archiveObject, archivePass, hot.find

All six are unbroken await chains on the caller's own tick ending in a genuine driver read. Two are not the tails the card predicted: 3 and 5 reach a read through saveMetaItem's own precondition check, not through the getMetaItem tail assigned to them. Same verdict, and the difference is the argument for reading chains rather than assuming them — the walk stops at the first read it finds, which is not always the one a human tracing by hand would land on.

What admitting them would do to the gate: nothing. All six carry no invented answer, and every parenthetical in the rule's verdict line (8 type-discriminated, 1 pass-through, 1 answer-by-assignment, 1 baselined) is byte-identical at depth 2 and at depth 50. Only the denominator moves, 66 to 72 — and that denominator is what #5186, #6451, #9165, #8845 and #8901 are quoted against. The bound is not buying correctness here; it is holding a published number still.

Which half of "5 of the 8" survived

Decomposed rather than reported as one number, with the walkAll probe run beside the depth probe and both compared to today's 66 by try line:

set count
walkAll @ depth 2 admits 7 was 8 — #13444's guard deleted the fake seam
depth 3 admits 6
both 5 the claim, reproduced exactly
depth only 1 protocol.ts:16502 publishPackageDrafts
walkAll only 2 sys-metadata-repository.ts:883, engine.ts:9571

So the number 5 survives and its denominator does not: read it as "5 of 7", the eighth having been the fake seam PR #13444 removed. And a correction the card could not have made — walkAll is only a partial mask for this bound. publishPackageDrafts is admitted by depth and by depth alone: counting call hops reaches its read, descending lexically through nested declarations does not.

The dispatch hypothesis, measured and falsified

My reading: #13444's new contradictsWrapperResolution may already refuse some of the 6, because a deeper hop is exactly where a wrong-receiver resolution compounds.

It refuses none of them. The full checker output is byte-identical with that guard enabled and disabled at depths 2, 3, 6 and 50. Its subject is the walk, not the depth:

walk depth guard on guard off guard removes
same-tick 2 66 66 0
same-tick 3 72 72 0
same-tick 50 72 72 0
walkAll 2 73 74 1
walkAll 3 74 76 2
walkAll 50 75 77 2

Two readings, and the second is the one to carry forward. The depth axis never arms the defect #13444 closed, so a depth raise is safe from that defect. But if the walk is ever widened as well, raising the depth doubles what that guard has to catch. The two widenings cost independently and risk multiplicatively.

And a resolution hazard the guard cannot see, found while verifying the six rather than looked for. functionBodies is keyed by bare name and is last-wins. protocol.ts declares lookup three times (bodies at 6657, 6717, 7129), and the hop in seams 1 and 2 resolves the call at 6674 to the declaration at 7129 — not the one lexically enclosing it. contradictsWrapperResolution admits it and is right to: the receiver is a bare identifier and the arity matches, because the three declarations are near-copies of one another. Both seams are real anyway (all three lookup bodies read sys_metadata through this.engine.findOne), so nothing is miscounted today. But the verdict is right by luck, and each additional hop multiplies the number of names that must be unique for it to stay right. That is the strongest argument on this tree for leaving the bound alone, and no seam count can make it.

The finding that made this worth landing: the bound was pinned from below only

Measured by ablation, not assumed. Lowering MAX_READ_WRAPPER_DEPTH to 1 reddens exactly one fixture (#13444's this-rooted chain, which needs two hops). Raising it to 3, 4, 6 or 50 left all 51 read-seam fixtures green and every gate in this file green — while the census denominator moved 66 to 72 with nothing anywhere saying so. A tunable five other cards quote must not be movable in silence. That is this card's own numbers going stale twice, one level down.

What ships

  1. The measurement, in the headerTHE DEPTH BOUND'S COST: the table, the six seams by name with their chains, the walkAll decomposition, the interaction matrix, the lookup hazard, and why the bound is nevertheless held.
  2. A JSDoc on the constant itself, which had none — its 18-line rationale block sits 150 lines above, beside DRIVER_READ_CALLEES. Deliberately not moved; the new block names it and adds the half that was missing (the price), so the two read as a pair rather than a duplicate.
  3. --depth-cost — a diagnostic that re-derives the price on demand and always exits 0. It prints admitted and removed seams by name per step, re-attributions per step (invisible in any count, and reading one as a removal is what produced the card's "-2"), and states its own probe ceiling rather than claiming saturation it did not test. Both stale readings of this number were produced by hand-patching the constant and re-running; this is that step, kept.
  4. Two self-test fixtures pinning the bound from above (51 to 53 cases). A three-hop ladder, one hop past the bound, plus its discrimination test — the same source at MAX_READ_WRAPPER_DEPTH + 1, which must find exactly one seam or the pin is asserting nothing.

maxDepth is threaded through isReadCall, analyzeReadSeams and a new collectReadSeams (split out of runReadSeamRule). Nothing in the audit path passes it, so the shipped verdict is the constant's.

The deliverable is a measured zero-delta

--list output is byte-identical before and after on this tree — verdicts, counts and every seam line. diff of the complete run, not just the totals.

Tests

Both legs, both quoted from the gate's own verdict lines, on final head 0f2bb1ce:

  • --self-test: ✓ self-test (log-level rule): 63 case(s) passed and ✓ self-test (read-seam invention rule): 53 case(s) passed, and the baseline offer stays marked maintainer-only (#8435)
  • main check: ✓ read-seam invention (#5186 + #6451 + #9165, 3 package roots, vocabulary find/findOne/count): 66 read seam(s), none invents an unreported answer and ✓ durability-degradation log levels: 29 durability-critical catch seam(s), all loud, rethrowing or propagating to the caller

Both new fixtures proven non-vacuous by ablation. Predictions were written first and both held; each mutation was confirmed on disk by blob hash before the reading was taken, and each restore verified by blob hash against the HEAD blob plus an empty git diff HEAD:

ablation prediction observed
MAX_READ_WRAPPER_DEPTH 2 to 3 only the pin reddens; main goes to 72 exactly that — expected violation=false seams=0, got violation=true count=1 seams=1, discrimination case still green, main prints 72
analyzeReadSeams ignores the depth option only the discrimination case reddens; main unchanged exactly that — [at maxReadWrapperDepth=3]: expected violation=true seams=1, got violation=false count=0 seams=0, pin still green, main prints 66

Gate families derived on my own final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (13 families, from git's own change set, not a hand-written list). All green: check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:durability-log-level, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs. Convention obligations green: pnpm check:pm-dispatch-gates (944 cases), node scripts/pm/bare-root-worklist.mjs --self-test (51 live rows, none stale), node scripts/check-self-test-wired.mjs (151 scripts), pnpm check:nul-bytes (75 assertions), plus a grep -naP control-byte sweep over the diff.

The two pin tests that read this gate's own source — sys-metadata-repository.draft-drain.test.ts and protocol.metadata-store-outage.test.ts — pass: Test Files 2 passed (2) / Tests 44 passed (44), after building the dependency closure.

Full-repo pnpm lint under the shared verify lock: 5494 files, 0 errors, 0 warnings, with the edited file confirmed present in the run (counts read from --format json, not from a summary line).

Recorded as NOT MEASURED, not as a failure: 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. Its own text says the branch is unreachable in CI.

Changeset

None. 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. The skip-changeset label is applied. PR #13444, on this same file hours ago, took the same route.

The decision this measurement does not take

Whether the bound should move is now a question with a price attached rather than an unknown, and it is deliberately left to the maintainer. The case for raising it: 6 verified-real seams are missing from a population that five cards quote. The case against, all measured here: admitting them changes no finding, only a number; it triggers the census re-run #8901's restart conjunct (b) reserves; and the last-wins name-resolution hazard grows with every hop. The card's own standing value is preserved either way — anyone re-running that census must read 66 as bound-dependent, and --depth-cost is now how they check the dependence instead of remembering it.


Generated by Claude Code

The bound was documented from the day it was written; what it COSTS never
was. Measured on 71627f7: 6 real read seams, all admitted at depth 3,
unchanged to depth 50, all six carrying `no invented answer` — so the
bound holds a denominator still, it does not buy a finding.

Before this change the constant was constrained from BELOW only: lowering
it to 1 reddens one fixture, raising it to 3/4/6/50 left all 51 read-seam
fixtures and every gate in this file green while the census moved 66 to
72. Two fixtures now pin it from above, and `--depth-cost` re-derives the
price on demand so it cannot go stale in a comment again.

Zero-delta: the full `--list` output is byte-identical before and after.

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

Labels

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

Projects

None yet

2 participants