Uh oh!
There was an error while loading. Please reload this page.
fix(gate): check-role-word's green line reports what it READ, not just the ledger - #9931
Conversation
…t the ledger The success line's only number came from the ledger (`current`, which on a green run is exactly the baseline), so "the debt is paid" and "the scan read nothing" printed the same sentence. It is safe today only by accident: with a non-empty ledger a dead scan is caught by the ratchet-DOWN branch, and that protection evaporates the moment this ratchet succeeds at its purpose. Both success texts now state the input volume, per root — `existsSync` skips a missing root in silence, so a bare total would hide one root behind the other. `--update` carries it too, where the ambiguity is destructive rather than merely misleading: it rewrites the baseline from the tree it just read. Visibility only. Verdicts, populations and exit codes are byte-for-byte what they were; the self-test pins the PROPERTY (a scanned tree and an unscanned one cannot render the same success with the ledger empty), not the new wording. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
PM review — ACCEPT. Stayed inside ruling 1, so |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9910
What was wrong
check-role-word's entire success branch was one line:currentholds only the files that still carry the word — and a green run isprecisely the run where its key set equals the baseline's. So the only number in
the green line was ledger-derived; the population the gate actually read
(
files, walked overROOTS = ['content/docs', 'skills']) never reached theoutput at all.
The measurement (H1, reproduced on this branch)
Ablated
ROOTSto two non-existent directories and emptied the baseline.Both mutations confirmed on disk before running —
git diff --statshowed2 files changed, 2 insertions(+), 46 deletions(-),grep -c 'no-such-dir-a'returned 1,
grep -c "'content/docs', 'skills'"returned 0, and the baselineparsed to 0 keys:
A gate that read zero files, over an empty ledger, reporting success.
The protection today is incidental (H2 — confirmed)
Same ablation with the ledger left at its real 43 entries:
The dead scan is caught, but only because every baselined file falls out of
currentand trips the ratchet-DOWN branch. That protection is a sideeffect of still owing debt, and it evaporates at the exact moment this ratchet
succeeds at its purpose. Nothing else guards the zero case: with the ledger
empty,
current = {}andbaseline = {}raise nothing in either direction.This is a predictive finding — the defect is one debt-payment away, on a
ratchet whose whole point is to reach that state.
The populations this gate really has (H3)
Not the sibling's two analysis halves — this gate makes one pass. Its real
populations are the scan, which is per root because
walk()runs behindexistsSync(root)and a renamed root is skipped in silence, and the ledger:content/docsscanned.md/.mdxfile(s)skillsscanned.md/.mdxfile(s)A bare total would hide one dead root behind whatever the other still
contributes, so every configured root is named on every green run, including
one that contributed nothing — a root omitted from the line is the same
silence in a new place.
After
The suffix list is derived from
EXTENSIONSrather than spelled again, sowidening the scan cannot leave the sentence describing the old one.
Scope note: the
--updateconfirmation is included, deliberately--updateprintedrole-word baseline updated: 0 file(s).— the sameambiguity, one line away in the same file, but destructive rather than
merely misleading:
--updaterewrites the baseline from the tree it just read,so on a dead scan it writes
{}over the ledger and then reports it in wordsthat read exactly like a debt fully paid. Both success texts now share one
scanClause()helper so they cannot drift apart. This is the same defect class,the same gate family, one mechanical edit, and it adds no verification surface.
Verdicts are untouched
Visibility only. No threshold moves, no baseline entry is added or removed, no
input is newly refused. The dead-scan ablation above still exits 0 on this
branch — it just says so now:
...against a genuinely paid-off ledger over a real scan (two clean
.mdx/.mdunder a temp docs root, one under a temp skills root), which the old line
rendered byte-identically:
The pin is on the PROPERTY, not the wording (H4)
Four non-overlapping
--self-testassertions, so each way this can rot iscaught by exactly one named failure: (1) a scanned tree and an unscanned one
must not render the same success with the ledger empty; (2) the zero must be
legible as an input volume, since (1) alone passes on any two strings that
differ; (3) a root contributing nothing is still named with its zero; (4) the
--updateconfirmation carries the volume too. A pin on the new sentence's textwould rot at the first rephrasing — and worse, a rephrasing back to
ledger-only numbers would keep such a pin green.
Ablation pair. Regressing
successSummary()to the old ledger-only line(confirmed on disk:
grep -c 'Scanned: ${scanClause'→ 0) turns the self-testred with exactly those first three failures, exit 1; the
--updatepinstays green, proving the four do not overlap. Ablating only the
--updateclause fires only pin (4). Both restored with
git checkout claude/issue-9910-role-word-green-line -- scripts/check-role-word.mjsand proved byte-identical by
git hash-objectmatching the committed blob(
1ff02760af…), withgit status --porcelainempty. Nogit stashwas used,and no build step is involved — this gate is executed directly from source by
package.json, so there is nodist/for an ablation to be measured againststale.
Gates
Union re-derived after the final commit by
node scripts/pm/dispatch-gates.mjs(no paths passed — it takes the change set from the merge base itself), run at
b814e4aewith a clean working tree:pnpm check:role-wordcheck-role-word: OK, no new occurrences of the reserved word.pnpm check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared…+All 33 self-test cases passed.node scripts/check-cross-package-test-inputs.mjspnpm check:ratchet-remedy-authorityOK … 97 scripts swept …; 6 mark the expanding remedy ⛔ MAINTAINER-ONLY—check-role-word.mjsstays classifiedmarkedpnpm check:nul-bytescheck-nul-bytes: OK (scanned 6305 text file(s) … no raw ASCII control bytes)check:ratchet-remedy-authorityis not named by the path derivation (itcomputes its own population), but it holds
check-role-word.mjsin itshand-classified control corpus, so it was run anyway.
Not in scope
#9911(the sibling gate greening whentargets.size === 0) and#9902(three tools with no shallow guard) are separately carded and separately owned;
neither is touched here. No changeset — this is a
scripts/gate and nothingpublished changes;
skip-changesetis applied.Precedent: PR #9815, which landed the same remedy shape on
check-published-readme-exports. Its two halves are that gate's; the scan/rootsplit above is this one's.
Generated by Claude Code