Skip to content

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 #9910

Description

@os-steve

Observation only — no gate is red, and this one is not red yet. Found while implementing #9767 (the same defect class in check:published-readme-exports); out of scope there by the card's own scale check, so recorded rather than fixed.

Instance of the #9747 meta-shape (fails toward FALSE GREEN), with a twist worth stating: the hole is latent, and it opens on success.

The line

scripts/check-role-word.mjs:236 is the whole success branch:

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

current holds only files that still carry role-word hits — the ledger. The population that was actually read (files, built by walk() over ROOTS = ['content/docs', 'skills'], thousands of files) is never printed. So the only number in the green line is debt-derived.

Why it is safe today, and why that is the problem

With a non-empty ledger (43 files right now), a scan that reads nothing is caught — but incidentally, by the stale/ratchet-down branch, not by the green line:

check-role-word: 43 problem(s)
  • content/docs/...: baselined file is clean/gone (was N) — ratchet DOWN: ...

That protection is a side effect of still having debt. It evaporates at exactly the moment the gate succeeds at its purpose. With the ledger empty, current = {} and baseline = {} produce no errors in either direction, and the green line has no other number to fall back on.

Measured, not argued. Ablating ROOTS to two non-existent directories and emptying the baseline — both ablations confirmed on disk via git diff --stat before running:

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. Restored with git checkout afterwards; OK (43 baselined file(s), no new occurrences) again.

Why this is the #9767 shape, one step earlier

#9767 was the same sentence in check:published-readme-exports, and it became visible only once #9581 emptied that baseline. This gate is one debt-payment away from the identical reading. The remedy shape #9767 adopted transfers directly and cheaply: print the input volume alongside the ledger count, e.g.

check-role-word: OK (N file(s) scanned across content/docs, skills; 43 baselined, no new occurrences).

A zero in N file(s) scanned is an alarm a reader can act on; a zero in 0 baselined file(s) says nothing at all. Per #8995 the wording would want a --self-test pin in the same edit, since the counts are interpolated.

Scope

One clause in one green line, plus a pin — the same size as #9767. Filed unassigned; not urgent, but it is cheapest to fix before the ledger empties, because after that the gate looks fine.

Generated by Claude Code

Activity

  1. claude commented on Aug 19, 2026

    @claude
    Contributor

    Claim: PM loop round 12
    Session: session_01XqDQYVU5smx29ts9pAErja
    Branch: claude/issue-9910-role-word-green-line
    Worktree: objectstack-issue-9910
    Domain: domain:devx
    File surface: scripts/check-role-word.mjs (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (node scripts/pm/dispatch-gates.mjs --tier scripts/check-role-word.mjs run this round: "no path-derived mandate … This line is a FLOOR, never a clearance")
    Clause-②: no
    Serial constraints cleared: swept all open PRs for any touching check-role-word — none. The sibling green-line fix on the other gate (#9767 / PR #9815, check-published-readme-exports.mjs) is a different file and is already queued; it is cited as precedent, not a dependency.

    ⚠️ The Clause-②: no above is bounded by ruling 1 in the dispatch: this card is scoped visibility-only. If the work turns out to require the gate to refuse where it currently greens, that is a verdict change, the declaration flips to yes, and the dev must stop and report rather than ship it.


    Generated by Claude Code

  2. os-steve commented on Aug 19, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 9910,
      "status": "done",
      "branch": "claude/issue-9910-role-word-green-line",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9931",
      "premise_still_valid": true,
      "summary": "Confirmed the card's premise in full and fixed it visibility-only, per ruling 1 — no verdict, population or exit code changed, so `Clause-②: no` still holds and I did not need to stop. `check-role-word`'s success branch now renders through two named pure functions (`scanClause()` shared by both success paths, `successSummary()`), stating the input volume PER ROOT alongside the ledger: `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))`. Per-root rather than a bare total because `walk()` runs behind `existsSync(root)`, so a renamed root is skipped in silence and a total would hide it behind the other root; every configured root is named even when it contributed nothing. One bounded in-place fix beyond the green line, declared in the PR body: the `--update` confirmation carried the identical ambiguity in its destructive form (it rewrites the baseline from the tree it just read, so on a dead scan it writes `{}` and reported `0 file(s).`, which reads exactly like a debt fully paid) — same defect class, same file, same gate family, one mechanical edit sharing the same helper, no new verification surface. Governed surfaces untouched; no changeset (`skip-changeset` applied and read back).",
      "tests": "All at final commit b814e4ae (working tree clean, `git status --porcelain` empty). GATE UNION re-derived AFTER that commit by `node scripts/pm/dispatch-gates.mjs` with no paths passed (it takes the change set from the merge base itself): '1 path(s) vs merge base d7c424055', naming check:role-word, check:cross-package-test-inputs, and node scripts/check-cross-package-test-inputs.mjs. Ran those plus two I could see implicated: check:ratchet-remedy-authority (it holds check-role-word.mjs in its hand-classified control corpus but computes its own population, so the path derivation cannot name it) and check:nul-bytes (any edit). Verdict lines as each gate printed them, exit codes captured by redirect-then-$? never through a pipe: check:role-word EXIT=0 'check-role-word: OK, no new occurrences of the reserved word.'; check:cross-package-test-inputs EXIT=0 'All 33 self-test cases passed.' + 'OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; check:ratchet-remedy-authority EXIT=0 'OK check-ratchet-remedy-authority: 97 scripts swept (scripts/*.{mjs,mts}); 6 mark the expanding remedy MAINTAINER-ONLY, 3 turn it down outright, 88 hand out no ratchet-expanding remedy.' — check-role-word.mjs stays classified `marked`; check:nul-bytes EXIT=0 'check-nul-bytes: OK (scanned 6305 text file(s) -- 6305 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).' Plus a targeted control-byte self-scan of the edited file, `grep -naP` over the C0 range, no output. H4 ABLATION PAIR, four legs, 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 injected and of removed text). LEG A, dead scan + empty ledger against the FIXED gate: EXIT=0 (unchanged verdict, as ruling 1 requires) but now prints 'Scanned: 0 .md/.mdx file(s) read across 2 root(s) — no-such-dir-a 0, no-such-dir-b 0.' LEG A', the contrasting genuinely-paid-off ledger over a REAL scan (three clean fixtures under temp roots): prints 'Scanned: 3 .md/.mdx file(s) read across 2 root(s)' with the same '0 baselined file(s)' — the two states the old line rendered byte-identically are now distinguishable, which is the whole card. LEG C, PIN ablation, regressing successSummary() back to the ledger-only line: self-test EXIT=1 with exactly the first three named failures, proving the pins can fail. LEG D, ablating ONLY the --update clause: EXIT=1 with ONLY pin (4), proving the four assertions do not overlap. RESTORE after every leg by `git checkout claude/issue-9910-role-word-green-line -- scripts/check-role-word.mjs` (never `git stash`), proved byte-identical by `git hash-object` equalling the committed blob 1ff02760af1581ad3fd90071812f558f2451de61, with the restored green run diffing clean against the pre-ablation run. ABLATION REBUILD STATEMENT: none applies and this is not an omission — the gate is a plain node script executed directly from source by package.json (`node scripts/check-role-word.mjs`), with no dist/ and no build step, so there is no stale-artifact leg for a mutation to be measured against; every leg above ran the edited source itself.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9932: check-role-word skips a configured ROOT that does not exist, silently — the VERDICT half ruling 1 carved out of this card. Legible now (each root prints its own count) but still not refused; and the incidental ledger protection never existed at all for a root whose ledger share is already zero, which is where every newly added root starts. Sibling of #9911. Filed unassigned, `finding` + `tooling` + `domain:devx`, no `pm:queue` — it is a verdict decision for triage, not a queued fix."
      ]
    }

    H1 — reproduction, with the independent confirmation that each mutation landed

    Ablated ROOTS to two non-existent directories and emptied the baseline. Confirmed on disk before running, not by the editor's exit status (the #9914 trap): the patch asserts its match count, then

    git diff --stat  ->  scripts/check-role-word.mjs     |  2 +-
                         scripts/role-word-baseline.json | 46 +----------------------------
                         2 files changed, 2 insertions(+), 46 deletions(-)
    grep -c 'no-such-dir-a'            -> 1   (injected text present)
    grep -c "'content/docs', 'skills'" -> 0   (original text gone)
    wc -c < scripts/role-word-baseline.json -> 3   ; parsed key count -> 0
    

    Then, on main's version of the gate:

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

    Reproduced exactly as the card describes.

    H2 — verdict: the protection is INCIDENTAL. Card confirmed.

    Same ROOTS ablation, ledger left at its real 43 entries (confirmed restored to 43 keys before the run, ablation confirmed still in place at grep -c 1):

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

    A dead scan is caught only because every baselined file falls out of current and trips the ratchet-DOWN branch. Nothing else guards the zero case — I looked for a second guard and there is none: with the ledger empty, current = {} and baseline = {} produce no error in either direction, and both Object.entries loops iterate zero times. So this is a predictive finding, exactly as filed: the defect is one debt-payment away, on a ratchet whose entire purpose is to reach that state. It also has a second, sharper edge the card did not claim, now filed as #9932 — the protection never existed for a root whose ledger share is already zero, which is where every newly added root begins.

    H3 — the real populations. Not the sibling's shape.

    check-published-readme-exports had two analysis halves. This gate makes one pass, so forcing it into two halves would have invented a number. Its real populations are the scan and the ledger, and the scan is meaningfully per root because of existsSync:

    population value on d7c4240 can a clean repo make it vacuous?
    content/docs scanned 179 .md/.mdx file(s) no — this is input volume
    skills scanned 36 .md/.mdx file(s) no
    ledger, files 43 yes — 0 is the success state
    ledger, occurrences 129 yes

    Counts independently re-derived by a standalone walker before the gate was touched, and they match what the fixed gate prints. I considered and rejected a third number: total occurrences is worth printing as ratchet progress but can never be the scan-failure alarm, so it sits inside the ledger clause where a zero correctly reads "clean", never as evidence the scan ran.

    H4 — does the new line fail when it should? Yes, in four separated ways.

    The pins assert the property, not the sentence: (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 merely differ; (3) a root contributing nothing is still named with its zero; (4) the --update confirmation carries the volume too. Ablation legs C and D above show (1)-(3) and (4) failing independently, so a future breakage names its own cause instead of firing four times.


    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions