Uh oh!
There was an error while loading. Please reload this page.
fix(devx): read the ledger note against its own field, and stop --lower minting the drift - #10820
Merged
Merged
Conversation
…er minting the drift `DEBT`/`TEST_DEBT` pair a measured `errors` with free prose, and nothing read the prose: a note could narrate any count at all, including one its own field contradicted, with every gate green. Four hand repairs of that shape are on record (#7038, #8982, #10721, #10775), the last two on the same day. None of them closed the class, because `--lower` MINTS it -- its regex capture ends at the digits, so the sanctioned one-command way to close a surplus is also the way to desynchronise a note. Both halves land together, because neither is safe alone. A check without the tool fix leaves the ledger RED after the next `--lower` run, in whoever's PR ran it, breaking the "closing an entry is FREE" invariant #5278/#6376 paid for. The tool fix without the check leaves today's instances undetected. COMPOSITION reads exactly one thing -- the house-convention tier itemisation the notes already open with -- and abstains, silently, everywhere else. A tier counted twice or a further tier count later in the note means the note quotes its own history and the entry is skipped rather than guessed at. Per-code tallies (`TS2835 x67, ...`) are deliberately NOT summed: they are partial by construction, and plugin-rest's sums to 147 while saying "composition as counted at 153". `--lower` now plans a `compositionAt` declaration alongside each lowering that would strand an itemisation, and writes it as a sibling field beside the number it already locates -- no prose surgery, no invented composition. The staleness becomes a declared fact the check reads and the green summary reports, instead of a silent one nothing reads at all. `compositionAt` may only ever declare a pile LARGER than the field, so it cannot launder a raise. Measured: over all 33 entries on main, 11 are checked and 0 fire. Over the tree before #10721 landed, exactly the two known instances fire and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
os-zhuang
marked this pull request as ready for review
August 21, 2026 13:18
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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#10722
What was wrong
scripts/check-type-check-coverage.mjsfreezes two ledgers whose entries pair a measurederrors:with a free-prosenote:. The ratchet compareserrorsagainst tsc and never reads the prose, so a note could narrate any count at all — including one its own field contradicts — with every gate green. Four hand repairs of that one shape are on record (#7038, #8982, #10721, #10775), the last two on the same day.None of them closed the class, because
--lowermints it. Its capture group ends at the digits, so the sanctioned one-command way to close a surplus is also the way to desynchronise a note.Reproduced, not quoted. Driving the base
lowerLedgerEntries()over the real@objectstack/metadataentry with a synthetic89 → 80measurement:One command, a fresh instance of the class, no gate anywhere the wiser.
What lands, and why both halves together
Neither half is safe alone, and the order is the whole argument:
--lowerrun leaves the ledger red, in whoever's PR ran the tool, and the operator has to hand-edit prose to get green. That breaks the "closing an entry is FREE" invariant [finding] DEBT ledger counts in check-type-check-coverage.mjs drift silently — @objectstack/metadata-protocol records 28, actually reports 63 #5278/[finding][devx]check:type-check-debt的 ledger 余量会让新写的 pin 变哑:mongodb 曾有 33 条余量吞掉一次真实回退,另有 5 条目前带 4–19 余量 #6376 paid for.1. COMPOSITION — the check
Reads exactly one thing: the house-convention tier itemisation the notes already open with (
code-tier N; config-tier N; noise N, either word order). It abstains, silently, everywhere else:spec-monorepo's "the workspace root itself: code-tier 4 …" qualifies);code-tier 9-shaped count anywhere later in the note, means the note quotes its own history → skipped, no verdict.metadata-protocolquotes the misleading note [finding] DEBT ledger counts in check-type-check-coverage.mjs drift silently — @objectstack/metadata-protocol records 28, actually reports 63 #5278 found and is skipped for exactly that reason;plugin-rest's tally sums to 147 and says so in the same breath ("composition as counted at 153"). Summing them would red an entry whose author was being precise.False-positive direction: an entry whose opening itemisation is deliberately partial, with the remainder described in words rather than digits, would fire wrongly. None exists.
False-negative direction: everything else, and it is most of the ledger — 21 of 33 entries carry no tier itemisation at all. That is the intended trade: a floor under one known class, not a reader of prose.
2.
--lowerstops minting itplannedLowerings()now computesdeclareCompositionAt— the size the itemisation was tallied at — where it is pure and fixture-testable;lowerLedgerEntries()writes it as a sibling field beside the number the regex already locates. No prose surgery (the docblock's own warning against parsing this file's prose still stands), no invented composition (the tool knows the new total and nothing about which tier gave it up).The same lowering, after:
compositionAtmay only ever declare a pile LARGER than the field, so it cannot launder a raise; equal-to-errorsis refused too, so the field only ever shrinks away. Declared-stale entries are named in the gate's green summary, for the reason the surplus is (#6376): a declaration nobody can see is the half that does the damage.3. The
:167-173rule now covers downward driftIt only ever said "raise", on the reasoning that a note describing a larger pile "misleads in the safe direction".
@objectstack/metadatadrifted down (92 → 89), and a note itemising 34 + 24 + 34 over a field reading 89 does not mislead safely — it contradicts its own field. The paragraph now says so, names both same-day repairs, and points at the mechanical half in each place.Evidence
Union re-run at
f4d00e4e7d(the final commit), exit codes captured before any pipe:pnpm check:type-check-coveragepnpm check:cross-package-test-inputsnode scripts/check-cross-package-test-inputs.mjspnpm check:entry-guard(by hand —dispatch-gatesis blind to it, #10784/#10705)node scripts/check-parse-guard.mjs(by hand, same reason)pnpm check:nul-bytespnpm check:ratchet-remedy-authority(this file is in itsCONTROLcorpus)pnpm exec eslint scripts/check-type-check-coverage.mjsnode scripts/pm/dispatch-gates.mjs(no paths, derives its own change set) → exit 0, 4 families, all run above.Self-test:
✓ 34 semantic case(s) + 24 observation + 25 re-measure + 28 built-closure + 19 auto-lowering case(s) hold— 11 new COMPOSITION fixtures (the two real instances transplanted, the ambiguity guard, and everycompositionAtreconciliation branch), 4 new plan cases, 4 new rewrite cases, and 2 round-trip cases that lower the fixture and read the result back through the check.Corpus check, shipped implementation over every
DEBT+TEST_DEBTentry:main699132f259^(before the #10721 repair)service-automation(code-tier 5overerrors: 3) andmetadata(34+24+34 = 92 overerrors: 89)699132f259(before the #10775 repair)metadataBoth known instances, on the trees that carried them. Nothing else, on any of the three.
Ablation. Mutated
tierItemisation()toif (typeof note !== 'string' || true) return null;so the rule can never fire. Confirmed on disk by anchor count, not by an editor's exit code — injected anchor1, original anchor0. Predicted 10 self-test failures (7 semantic, 1 plan, 2 round-trip, 0 rewrite — the rewrite cases pass the declaration explicitly). Observed 10, and the failing labels were exactly the predicted set. Restored and verified byte-identical (sha256sum -c→OK). No build is involved: this is a plain.mjsrun directly by node, with nodist/between the edit and the run.Declared narrowing
pnpm check:type-check-debtrefuses without a built workspace closure (--re-measure cannot run: 55 workspace dependenc(ies) … have no built type entry point on disk), as expected. Its structural half — which is where COMPOSITION runs — executed green before the refusal. No ledger number can move from this diff: it adds no TypeScript, and everyerrors:value in both ledgers is byte-identical toorigin/main.Out of scope
No changeset: gate tooling publishes nothing.
skip-changesetapplied additively.Generated by Claude Code
Generated by Claude Code