fix(devx): price the read-seam wrapper depth bound and pin it from above - #13472
Merged
os-project-manager merged 1 commit intoAug 30, 2026
Merged
Conversation
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
This was referenced Aug 30, 2026
os-project-manager
marked this pull request as ready for review
August 30, 2026 14:14
os-project-manager
enabled auto-merge
August 30, 2026 14:14
os-project-manager
deleted the
claude/issue-12360-read-wrapper-depth-bound
branch
August 30, 2026 14:33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12360
The card's framing is the whole job: the bound is documented, the number never was.
MAX_READ_WRAPPER_DEPTH = 2has 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 thanMAX_READ_WRAPPER_DEPTHhops 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 onorigin/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.
trylines re-attributed to a different first-matching callee. That still happens here (engine.ts:9741and:10906move fromresolveMasterDetailParent(s)tomediaValueShapeStrictFor) 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:
getMetaItemCachedengine.findOnesaveMetaItemengine.findOnemigrateStoredMetadataengine.findOnepublishPackageDraftsengine.findOneduplicatePackageengine.findOnesweephot.findAll six are unbroken
awaitchains 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 throughsaveMetaItem's own precondition check, not through thegetMetaItemtail 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
walkAllprobe run beside the depth probe and both compared to today's 66 by try line:publishPackageDraftsSo 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 —
walkAllis only a partial mask for this bound.publishPackageDraftsis 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
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:
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.
functionBodiesis keyed by bare name and is last-wins.protocol.tsdeclareslookupthree 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.contradictsWrapperResolutionadmits 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 threelookupbodies readsys_metadatathroughthis.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_DEPTHto 1 reddens exactly one fixture (#13444'sthis-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
THE DEPTH BOUND'S COST: the table, the six seams by name with their chains, thewalkAlldecomposition, the interaction matrix, thelookuphazard, and why the bound is nevertheless held.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.--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.MAX_READ_WRAPPER_DEPTH + 1, which must find exactly one seam or the pin is asserting nothing.maxDepthis threaded throughisReadCall,analyzeReadSeamsand a newcollectReadSeams(split out ofrunReadSeamRule). Nothing in the audit path passes it, so the shipped verdict is the constant's.The deliverable is a measured zero-delta
--listoutput is byte-identical before and after on this tree — verdicts, counts and every seam line.diffof 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) passedand✓ self-test (read-seam invention rule): 53 case(s) passed, and the baseline offer stays marked maintainer-only (#8435)✓ read-seam invention (#5186 + #6451 + #9165, 3 package roots, vocabulary find/findOne/count): 66 read seam(s), none invents an unreported answerand✓ durability-degradation log levels: 29 durability-critical catch seam(s), all loud, rethrowing or propagating to the callerBoth 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:MAX_READ_WRAPPER_DEPTH2 to 3expected violation=false seams=0, got violation=true count=1 seams=1, discrimination case still green, main prints 72analyzeReadSeamsignores the depth option[at maxReadWrapperDepth=3]: expected violation=true seams=1, got violation=false count=0 seams=0, pin still green, main prints 66Gate 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 agrep -naPcontrol-byte sweep over the diff.The two pin tests that read this gate's own source —
sys-metadata-repository.draft-drain.test.tsandprotocol.metadata-store-outage.test.ts— pass:Test Files 2 passed (2) / Tests 44 passed (44), after building the dependency closure.Full-repo
pnpm lintunder 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.mjsexits 3 withPREREQUISITE NOT MET— it grades a savedturbo run testlog 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.ymlcalls the textbookskip-changesetcase in as many words. Theskip-changesetlabel 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-costis now how they check the dependence instead of remembering it.Generated by Claude Code