Skip to content

fix(gate): check-role-word's green line reports what it READ, not just the ledger - #9931

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9910-role-word-green-line
Aug 19, 2026
Merged

fix(gate): check-role-word's green line reports what it READ, not just the ledger#9931
os-steve merged 1 commit into
mainfrom
claude/issue-9910-role-word-green-line

Conversation

@os-steve

@os-steveos-steve commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Fixes#9910

What was wrong

check-role-word's entire success branch was one line:

console.log(`check-role-word: OK (${Object.keys(current).length} baselined file(s), no new occurrences).`);

current holds only the files that still carry the word — and a green run is
precisely 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 over ROOTS = ['content/docs', 'skills']) never reached the
output at all.

The measurement (H1, reproduced on this branch)

Ablated ROOTS to two non-existent directories and emptied the baseline.
Both mutations confirmed on disk before running — git diff --stat showed
2 files changed, 2 insertions(+), 46 deletions(-), grep -c 'no-such-dir-a'
returned 1, grep -c "'content/docs', 'skills'" returned 0, and the baseline
parsed to 0 keys:

check-role-word: OK (0 baselined file(s), no new occurrences). EXIT=0

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:

check-role-word: 43 problem(s)
• content/docs/ai/agents.mdx: baselined file is clean/gone (was 5) — ratchet DOWN: ...
... (43 in total) EXIT=1

The dead scan is caught, but only because every baselined file falls out of
current and trips the ratchet-DOWN branch. That protection is a side
effect 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 = {} and baseline = {} 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 behind
existsSync(root) and a renamed root is skipped in silence, and the ledger:

populationtoday
content/docs scanned179 .md/.mdx file(s)
skills scanned36 .md/.mdx file(s)
ledger43 file(s), 129 occurrence(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

check-role-word: OK, no new occurrences of the reserved word.
Scanned: 215 .md/.mdx file(s) read across 2 root(s) — content/docs 179, skills 36.
Ledger: 43 baselined file(s) still carrying it (129 occurrence(s)) in scripts/role-word-baseline.json.

The suffix list is derived from EXTENSIONS rather than spelled again, so
widening the scan cannot leave the sentence describing the old one.

Scope note: the --update confirmation is included, deliberately

--update printed role-word baseline updated: 0 file(s). — the same
ambiguity, one line away in the same file, but destructive rather than
merely misleading: --update rewrites the baseline from the tree it just read,
so on a dead scan it writes {} over the ledger and then reports it in words
that 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:

check-role-word: OK, no new occurrences of the reserved word.
Scanned: 0 .md/.mdx file(s) read across 2 root(s) — no-such-dir-a 0, no-such-dir-b 0.
Ledger: 0 baselined file(s) still carrying it (0 occurrence(s)) in scripts/role-word-baseline.json.

...against a genuinely paid-off ledger over a real scan (two clean .mdx/.md
under a temp docs root, one under a temp skills root), which the old line
rendered byte-identically:

check-role-word: OK, no new occurrences of the reserved word.
Scanned: 3 .md/.mdx file(s) read across 2 root(s) — tmp/docs 2, tmp/skills 1.
Ledger: 0 baselined file(s) still carrying it (0 occurrence(s)) in scripts/role-word-baseline.json.

The pin is on the PROPERTY, not the wording (H4)

Four non-overlapping --self-test assertions, so each way this can rot is
caught 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
--update confirmation carries the volume too. A pin on the new sentence's text
would 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-test
red with exactly those first three failures, exit 1; the --update pin
stays green, proving the four do not overlap. Ablating only the --update
clause fires only pin (4). Both restored with
git checkout claude/issue-9910-role-word-green-line -- scripts/check-role-word.mjs
and proved byte-identical by git hash-object matching the committed blob
(1ff02760af…), with git status --porcelain empty. No git stash was used,
and no build step is involved — this gate is executed directly from source by
package.json, so there is no dist/ for an ablation to be measured against
stale.

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
b814e4ae with a clean working tree:

gateverdict line
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.mjssame script as above
pnpm check:ratchet-remedy-authorityOK … 97 scripts swept …; 6 mark the expanding remedy ⛔ MAINTAINER-ONLYcheck-role-word.mjs stays classified marked
pnpm check:nul-bytescheck-nul-bytes: OK (scanned 6305 text file(s) … no raw ASCII control bytes)

check:ratchet-remedy-authority is not named by the path derivation (it
computes its own population), but it holds check-role-word.mjs in its
hand-classified control corpus, so it was run anyway.

Not in scope

#9911 (the sibling gate greening when targets.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 nothing
published changes; skip-changeset is applied.


Precedent: PR #9815, which landed the same remedy shape on
check-published-readme-exports. Its two halves are that gate's; the scan/root
split above is this one's.

Generated by Claude Code

…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
@os-steveos-steve added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/m labels Aug 19, 2026 — with Claude
@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PM review — ACCEPT. Stayed inside ruling 1, so Clause-②: no holds. Arming.

Verified at b814e4ae: 1 file, +149/-4, GOVERNED_HITS=NONE, no non-green gates.

Ruling 1 held, and that is why this queues today

Verdict, population and exit codes unchanged — proven, not asserted, by LEG A: a dead scan against the fixed gate still exits 0. So the Clause-②: no I declared on the card stays honest and this PR is not caught by the 条款② entry gate. You had a standing instruction to stop and report if the fix demanded a verdict change; instead you found the split, took the visibility half, and carded the verdict half as #9932. That is the right division and it is what kept the tier correct.

⭐ Per-root, not a bare total — and the reason is the better finding

walk() runs behind existsSync(root), so a renamed root is skipped in silence, and a total would hide it behind the other root.

Scanned: 215 .md/.mdx file(s) read across 2 root(s) — content/docs 179, skills 36.
Ledger: 43 baselined file(s) still carrying it (129 occurrence(s))

The sibling precedent (#9815) printed per-half totals; you did not copy that shape, you asked what this gate's silent-failure mode is and printed the decomposition that exposes it. Naming every configured root even when it contributed nothing is the part that makes a vanished root visible — a total would have satisfied the card and left the real hole open. H3 asked exactly this and the answer was not the sibling's.

⭐ The bounded in-place fix — the same bug in its destructive form

the --update confirmation carried the identical ambiguity: it rewrites the baseline from the tree it just read, so on a dead scan it writes {} and reports 0 file(s). — which reads exactly like a debt fully paid.

The read path printed a misleading line. The write path destroys the ledger and prints the same misleading line as its confirmation — an operator would see "0 files, debt paid" at the exact moment the baseline was wiped by a scan that read nothing. Same defect class, same file, same gate family, one mechanical edit sharing the same helper, declared in the PR body. Correctly in scope, and materially worse than the carded symptom.

⭐ H4 — #9914's lesson applied the same day, and applied properly

Four legs, and every mutation confirmed on disk by an observation independent of the editor's exit code: the patch script asserts each replacement matched exactly once and aborts otherwise, then git diff --stat plus grep -c of both the injected and the removed text. That is stronger than what #9914 asks for — checking the removal too closes the case where the anchor matched something adjacent.

  • A — dead scan + empty ledger, fixed gate: EXIT=0, prints Scanned: 0 … — no-such-dir-a 0, no-such-dir-b 0.
  • A' — genuinely paid-off ledger over a real scan: Scanned: 3 … with the same 0 baselined file(s)

the two states the old line rendered byte-identically are now distinguishable. That pair is the card, and A' is the leg most people would skip — proving the good state still reads as good is what stops a "fix" that merely makes everything look broken.

  • C — regressing successSummary() → self-test EXIT=1 with exactly the first three named failures;
  • D — ablating only the --update clause → EXIT=1 with only pin (4), proving the four assertions do not overlap.

D is the one I want to name. Overlapping pins are how a suite reports four assertions and tests two things. Restores proved byte-identical by git hash-object against the committed blob 1ff02760…, never git stash.

And the rebuild statement: "none applies and this is not an omission" — plain node script run from source, no dist/, so there is no stale artifact for a mutation to be measured against. Stating why a step is inapplicable beats silently omitting it; a reviewer cannot tell omission from inapplicability otherwise.

H2 — confirmed, and extended past the card

The incidental protection is real: the stale/ratchet-down branch only fires on a non-empty ledger. Your extension is the sharper half —

the incidental protection never existed at all for a root whose ledger share is already zero, which is where every newly added root starts

So it is not merely "one debt-payment away"; for a freshly added root it is broken on arrival. That widens the finding from a future risk to a present one, and it is the strongest argument for #9932.

#9932 — filed correctly, and I am leaving it unqueued

You filed it with finding / tooling / domain:devx and no pm:queue, because refusing a vanished root is a verdict decision for triage, not a queued fix. Right — it is the half ruling 1 carved out, and queueing it would smuggle back the Clause-② exposure this card was scoped to avoid. Leaving it as filed.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review August 19, 2026 08:40
@os-steve
os-steve enabled auto-merge August 19, 2026 08:40
@os-steve
os-steve added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit 52db5dfAug 19, 2026
30 checks passed
@os-steve
os-steve deleted the claude/issue-9910-role-word-green-line branch August 19, 2026 08:57
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: check-role-word's green line counts only the LEDGER, so when the debt is finally paid a total scan failure and a clean repo print the same OK

2 participants

@os-steve@claude