… on the ledger's current size
The two sample-output sites named in #9947 both stated the regenerated
baseline's size (43) — a figure `--update`, the gate's own sanctioned
remedy, invalidates. Refreshing such a number resets the clock rather
than fixing it, so neither is refreshed here.
Per-site, because they are different things:
- The quoted OLD green line is a FORMAT example. That line no longer
exists in the program (removed by #9931), so no literal is checkable
against anything the gate does — the number goes generic (`N`).
- The dead-scan ablation result is a MEASUREMENT. It now rests on the
cardinality identity the ratchet-DOWN loop enforces — one problem per
baseline entry, true at every ledger size — instead of a magnitude.
No bound is stated in its place: a SHRINK-only ledger admits neither a
durable floor (it rots on the first sanctioned ratchet-down) nor a
durable ceiling (the same `--update` is also the baseline-EXPANDING
path #8435 gates).
Two further sites found while measuring: the header listed the ledger's
legitimate contents as an inventory, one entry of which ("ARIA `role=`
in samples") already matches zero baselined files; it now names the
legitimate KINDS instead. And the self-test's scan fixtures are pinned
as synthetic, so nobody "refreshes" them into a figure the tree can
falsify.
Comment prose only: no verdict, exit code, population or threshold
moves. Every changed line is a comment, and the gate's output is
byte-identical before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
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