Skip to content

finding: check-role-word.mjs's header says --update expands the baseline "which is the shrink-only direction" — backwards on either reading of the relative clause #10042

Description

@os-steve

Found while working #9947 (comment-figure cleanup in the same file). Not fixed there: different defect class (garbled prose, not a self-invalidated figure), so it failed the bounded in-place exemption's same-class condition.

The sentence

scripts/check-role-word.mjs, header block:

// `--update` expands the baseline, which is the shrink-only direction of this
// ratchet — the NEW-use message marks that path `⛔ MAINTAINER-ONLY` per the
// #8435 convention, and the self-test holds the marker in place.

The relative clause has no correct antecedent:

  • if which = the baseline → "the baseline, which is the shrink-only direction of this ratchet" — a baseline is not a direction;
  • if which = expanding the baseline → factually backwards. Expanding is the weakening direction; shrink-only is the direction the ratchet guarantees.

Why it is worth fixing rather than tolerating

This is garbled about precisely the fact the ⛔ MAINTAINER-ONLY convention exists to make unmistakable — which way the privileged path moves the ratchet. The same file states it correctly twice elsewhere, so the header is the odd one out:

Scope

Comment prose in scripts/check-role-word.mjs only — no verdict, exit code or population change. Likely a one-sentence rewrite along the lines the two sites above already use.

Ref: #9947 is where this surfaced and is not addressed by it.

Activity

  1. claude commented on Aug 19, 2026

    @claude
    Contributor

    Claim: PM loop round 25
    Session: session_01XqDQYVU5smx29ts9pAErja
    Branch: claude/issue-10042-shrink-only-direction-prose
    Worktree: objectstack-issue-10042
    Domain: domain:devx
    File surface: scripts/check-role-word.mjs (the --update header sentence) (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (--tier on this path returns "no path-derived mandate … This line is a FLOOR, never a clearance"; content limb no — comment prose only)
    Clause-②: no
    Serial constraints cleared: swept all open PRs for any touching check-role-word — none. Both predecessors on this file are MERGED: PR #9931 (#9910, the per-root Scanned: line and the --update confirmation fix) and PR #10044 (#9947, which removed the hardcoded ledger size and left this sentence deliberately untouched as a different defect class).


    Generated by Claude Code

  2. os-steve commented on Aug 19, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 10042,
      "status": "done",
      "branch": "claude/issue-10042-shrink-only-direction-prose",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10065",
      "premise_still_valid": true,
      "summary": "The header sentence at scripts/check-role-word.mjs L29-31 is replaced. Per ruling 1 the rewrite fixes the CONCEPT, not the clause: it states the MECHANICAL fact (--update rewrites the baseline from the current tree and never reads the old ledger, so it moves whichever way the tree moved -- shrinking where the word is gone, EXPANDING where it is new, in one stroke) separately from the POLICY fact (the baseline is shrink-only, so ratcheting down is the author's own remedy while expanding weakens the gate and is a maintainer's call), and closes by saying the marker pins the WORDING, not the act. Comment prose only: git diff contains zero changed non-comment lines; no verdict, population, exit code or message text moves. Nothing PR #10044 settled was touched. NOTE for the PM: the platform appended a SECOND signature footer to the PR body at creation time, so #10065's body ends with the footer twice -- platform behaviour on create, not another actor editing the PR; left alone rather than risk an edit round.",
      "tests": "All at final commit f2ad79dba3, exit codes captured by redirect-then-$? (never through a pipe). Gate set re-derived AFTER the commit with `node scripts/pm/dispatch-gates.mjs` (no paths passed): 1 path vs merge base 20b9a9ce1, 115 families discovered, 3 matched. It named check:cross-package-test-inputs, which the dispatch prompt did not -- ran it. UNION: (1) check:role-word self-test EXIT=0 -> 'OK  self-test: the NEW-use remedy marks baseline expansion as maintainer-only, the predicate rejects an unmarked offer, the ratchet-DOWN remedy stays the author's own, and both success texts report what was READ'; (2) check:role-word run EXIT=0 -> 'check-role-word: OK, no new occurrences of the reserved word. / Scanned: 216 .md/.mdx file(s) read across 2 root(s) -- content/docs 180, skills 36. / Ledger: 43 baselined file(s) still carrying it (129 occurrence(s))'; (3) check:cross-package-test-inputs EXIT=0 -> 'OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; (4) check:nul-bytes EXIT=0 -> 'check-nul-bytes: OK (scanned 6358 text file(s) -- 6358 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).'; (5) check:ratchet-remedy-authority self-test EXIT=0 and run 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. Control corpus: 18 hand-classified scripts, set-equality audited both ways.' REVERSE VERIFICATION -- predicted direction: NO CHANGE, because H4's grep found nothing keying on the sentence. Captured check:ratchet-remedy-authority's output before the edit and diffed it against the after run: byte-identical. That is the measurement that matters for a prose edit to a gate whose CONTROL pins it as 'marked' -- the sweep's classification does not read this sentence, and the prediction held. No build/ablation applies (root .mjs scripts, no dist, no package deps -- the heavy-verify lock was never needed and never taken). Extra corroboration for ruling 4: the LIVE scan now reads 216 files (content/docs 180, skills 36) while the self-test fixture pins 215 (179 + 36). The fixture is closed and synthetic and is working exactly as its L295-297 comment says; NOT refreshed.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    H1 — --update's actual code path (quoted, measured independently)

    Confirmed, and stronger than inherited: --update is not merely also expanding, it is not directional at all.

    const update = process.argv.includes('--update');          // L45
    
    if (update) {                                              // L374
      writeFileSync(BASELINE_PATH, JSON.stringify(current, null, 2) + '\n');   // L375
      console.log(updateSummary(scanned, current));
      process.exit(0);                                         // L377 — leaves here
    }
    
    const baseline = existsSync(BASELINE_PATH)                 // L380 — NEVER REACHED on --update
      ? JSON.parse(readFileSync(BASELINE_PATH, 'utf8'))
      : {};

    Three facts fall out of that ordering:

    1. It is a whole-file overwrite, not a merge, append or targeted delete — writeFileSync of JSON.stringify(current).
    2. current (L364-372) is a pure function of the tree walk: every scanned file whose name+body hits exceed zero.
    3. Decisively, the old baseline is never read on this path — the const baseline = … read sits after the branch's process.exit(0). With no knowledge of the previous contents, the write cannot be "only add" or "only remove" by construction.

    So the ledger moves whichever way the tree moved, both directions in the same stroke. PR #10044's finding is confirmed independently — its premise and this card's premise both stand. The file already said this correctly at L70-73 and L127-128; only L29 disagreed.

    H2 — is the #8435 marker machine-checked? Yes for the wording, no for the act.

    Machine-checked, in two independent places:

    • This file's own self-test, assertions (1)-(4) at L257-286, wired into CI as the first half of check:role-word (package.json:45 → .github/workflows/lint.yml:591). It pins the marker's placement in newUseMessage(), and (3) proves the predicate discriminates rather than approving everything.
    • check:ratchet-remedy-authority (lint.yml:1817), whose CONTROL corpus pins check-role-word.mjs as expect: 'marked' at L588 — "PR fix(devx): mark ratchet-editing remedies as maintainer-only in three more gates (#8519) #8539. Names its ledger by path, the case a dot-excluding gap class loses." — set-equality audited both ways. Presence there, placement here; the gate's header states that split deliberately.

    Not machine-checked: the policy itself. No CODEOWNERS entry covers scripts/role-word-baseline.json (.github/CODEOWNERS names only /scripts/pm/check-governed-merges.mjs), there is no growth guard on the baseline, and --update runs unconditionally at L374 for anyone who passes the flag. The enforcement is entirely textual: the gate is obliged to say the path is a maintainer's; it cannot stop anyone taking it.

    That is exactly why the corrected sentence ends with "Both pin the WORDING, not the act — the flag takes either direction from whoever runs it", satisfying ruling 1's prohibition on implying enforcement that does not exist.

    H3 — sweep of the six MAINTAINER-ONLY gates

    Total 6 · confused 1 · fixed here 1 · others needing a fix 0.

    gate phrasing verdict
    check-engine-double-contract.mjs "That baseline is shrink-only, so an entry weakens a…" (L1786, L1866, L2827) correct
    check-type-check-coverage.mjs "Raising the entry weakens a shrink-only ratchet…" (L2140, L2780) correct
    check-role-word.mjs "expands the baseline, which is the shrink-only direction" (L29) confused — fixed here
    check-durability-degradation-log-level.mjs "That baseline is shrink-only and hand-edited, so an entry weakens a ratchet" (L2163, L4058) correct
    check-driver-conformance.mjs no shrink vocabulary at all — reachable only through the governance limb, exactly as its CONTROL entry records n/a
    check-test-typecheck.mts "which EXPANDS a shrink-only ratchet. The rule saying that is a maintainer's" (L120, L403) correct

    A repo-wide grep for the literal phrase shrink-only direction returns exactly two hits: the defect at check-role-word.mjs:29, and check-published-readme-exports.mjs:719 — "N known instance(s) STILL in <file> carries the shrink-only direction in the word 'still'". That second one is correct and unrelated: it uses "direction" as the ledger's ratchet direction, not as an attribution of expansion to the shrink direction. Nothing to fix there, and it is outside the marked six in any case.

    The five correct gates all use the same shape — the LEDGER is shrink-only (a governance property of the registry) + expanding WEAKENS it + so it is a maintainer's. That shape is what the new sentence adopts.

    H4 — nothing mechanical depends on the sentence

    $ grep -rn "expands the baseline" --include="*.mjs" --include="*.mts" --include="*.ts" \
          --include="*.json" --include="*.md" --include="*.mdx" --include="*.yml" . | grep -v node_modules
    ./scripts/check-role-word.mjs:29:// `--update` expands the baseline, which is the shrink-only direction of this
    ./scripts/check-role-word.mjs:64:// This gate's second remedy is `--update`, which expands the baseline. That is
    

    Two hits, both inside the file itself — L29 the defect, L64 the correct statement. No gate, self-test, fixture or workflow keys on the header's words. Of the files that read check-role-word.mjs at all, only check-ratchet-remedy-authority.mjs reads its source text, and the before/after byte-identical sweep output above is the empirical confirmation that its classification does not depend on this sentence. The fix has no mechanical half.

    Ruling 2 — the sites converged on, and a refinement

    The card named two. Both check out, and they agree with each other (line numbers at origin/main 20b9a9c):

    Refinement rather than a disagreement: the file states it correctly four times, not twice. Two further sites the card did not name —

    All four agree. The header was the sole outlier out of five statements, which strengthens rather than complicates the card.

    Rulings 3-6

    • Ruling 3 (comment prose only) — held. Zero changed non-comment lines; no behaviour change was needed, so no STOP condition arose.
    • Ruling 4 (do not touch what docs(gate): rest check-role-word's comment figures on invariants, not on the ledger's current size #10044 settled) — held. The N generalisation, the cardinality-identity sentence, the legitimate-KINDS rule and the synthetic fixtures are untouched. Corroborating evidence that the 215 pin is doing its job: the live tree now scans 216 (content/docs 180 + skills 36) against the fixture's 215 (179 + 36). The gap is the closed fixture behaving exactly as L295-297 says it must — not drift.
    • Ruling 5 (governed surfaces) — held. One file changed: scripts/check-role-word.mjs.
    • Ruling 6 (no changeset) — held. skip-changeset applied instead, written as the read-then-union set and read back: size/s + skip-changeset. PR left draft; auto-merge not enabled.

    Out-of-scope findings: none. One near-miss checked and dismissed — check-ratchet-remedy-authority reports an 18-entry control corpus while quoted keys number 17; the 18th is the computed [SELF_FILE] key at L666. No discrepancy, nothing to file.

    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