Skip to content

[finding] bare-root-worklist prints REACHABLE beside a REFUSE-* reason when a gate takes the escape, and --self-test stays green — the contradiction lands silently #12064

Description

@yinlianghui

Measured on PR #12061 while implementing #11935. Filed unassigned — out of that card's declared file surface (scripts/check-ratchet-remedy-authority.mjs), and it is a defect of the worklist rather than of the gate.

This is not the same defect as #11277. That one is about a why string stating a wrong measurement, and correctly observes that a prose assertion cannot be mechanised. This one is a state/verdict contradiction, which is mechanisable — the two fields disagree structurally, not in prose.

The shape

scripts/pm/bare-root-worklist.mjs prints, per row, a state derived from the live tree beside a recorded verdict from its TRIAGE map. When a gate that carries a REFUSE-* verdict later takes the watch-hint escape, the state flips to REACHABLE while the recorded refusal reason stays put. The row then asserts both that the population is now reachable and that it is unspellable.

Reproduced by declaring ROOT_DIR_WATCH_HINTS = ['scripts/*'] on scripts/check-ratchet-remedy-authority.mjs (the change on PR #12061):

# unmodified tree (1e79aa4f8)
REFUSE-UNSPELLABLE  check:ratchet-remedy-authority SCRIPTS_DIR scripts
                      reads the TOP LEVEL of the root only, and only two extensions — 115 of 226 (51%).
                      The idiom has no non-recursive spelling: a subtree hint claims every nested directory too

# with the declaration (36c15e682)
REACHABLE           check:ratchet-remedy-authority SCRIPTS_DIR scripts
                      reads the TOP LEVEL of the root only, and only two extensions — 115 of 226 (51%).
                      The idiom has no non-recursive spelling: a subtree hint claims every nested directory too

node scripts/pm/bare-root-worklist.mjs --self-test exits 0 on both trees (OK self-test: 41 live row(s), 34 unreachable as spelled, 35 recorded verdict(s) — none stale, none missing). The production run exits 0 on both as well.

Why the STALE assertion does not fire

The self-test audits TRIAGE keys against all sweep rows in both directions — a key with no row is STALE, a row with no key is missing. A row that is still present but whose state has moved out of the refusal classes satisfies both halves, so nothing fires. The map already has a verdict for a gate that took the escape — DECLARED-NARROWER — so the vocabulary to express the correct post-escape state exists; nothing checks that a row actually moved to it.

This was predicted in #11155's dev report ("its self-test will NOT catch that, because the STALE assertion tests TRIAGE keys against ALL sweep rows, not just uncovered ones, so the contradiction would land silently"). It is now measured rather than predicted, which is why it is worth its own card.

Why it matters

The worklist is a governance record whose value is that its rows are trustworthy — its own triage docblock insists every percentage was measured rather than estimated. A row that reads REACHABLE … has no non-recursive spelling is worse than a wrong measurement: it is self-refuting, and the next reader cannot tell which half is stale. It also silently converts a deliberate, adjudicated refusal into something that reads like an oversight.

The trigger is not hypothetical or specific to this gate: any row whose gate later declares a watch hint lands it, and taking the escape is exactly what the ledger exists to encourage.

Suggested remedy (not applied here)

Assert, in --self-test, that a row's live state and its recorded verdict class are consistent — a REFUSE-* verdict must not sit on a row the sweep now finds REACHABLE; such a row should either move to DECLARED-NARROWER or fail naming itself. That is a mechanical pairing of two fields the tool already computes, and it fails at authoring time rather than landing quietly.

Refs

#11155 (where it was predicted) · #11277 (the adjacent, different why-accuracy defect) · #11935 / PR #12061 (where it was measured) · scripts/pm/bare-root-worklist.mjs TRIAGE

Activity

  1. self-assigned this
    on Aug 25, 2026
  2. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    Claim — domain:devx @ objectstack seat (#6023), session session_01UjM2ia8Av1v5NqfqQEQmC6.

    • Branch: claude/issue-12064-verdict-state-contradiction
    • Worktree: ../objectstack-12064 (dedicated)
    • Model tier: opus (per triage's S–M/opus suggestion)
    • Clause ②: falsify, don't build on, this seat's assumptions.

    Answering triage's fold-or-serial question explicitly, as asked: SERIAL, and the fold is now moot.

    Triage suggested one dispatch with per-member commits, since #12289 lands in the same file. Since that comment was written, #12289 has split in two and neither half is foldable:

    So scripts/pm/bare-root-worklist.mjs is free and this card gets it alone. It is single-writer for this claim.

    ⚠️ One thing to NOT assume, because I checked and it does not hold. It is tempting to think this card's guard would have caught #12328's four rows — same file, same REFUSE-*-versus-reality shape. It would not: #12328's own measurement records that covered() probes hintCovers(h, 'ROOT/probe.file'), which is false for all four spellings, so those rows do not flip to REACHABLE and this guard never fires on them. The two defects rhyme and are genuinely distinct. ⛔ Do not widen this card to reach them, and do not describe it as closing that class.

    Premise stands as filed — predicted in #11155's dev report, then measured on PR #12061 with both trees shown and --self-test exiting 0 on each. That "predicted, then measured" provenance is why it is worth a mechanical guard rather than another prose repair.


    Generated by Claude Code

  3. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor
    {
      "issue": 12064,
      "status": "done",
      "branch": "claude/issue-12064-verdict-state-contradiction",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12347",
      "premise_still_valid": true,
      "summary": "Added one assertion to bare-root-worklist --self-test: a recorded TRIAGE verdict may not sit on a row the sweep finds REACHABLE, naming the offending row and its verdict. Neither existing assertion can see that state because both audit the KEY SET — such a row is still a row (not STALE) and has left `open` (not FRESH). No TRIAGE row's why or verdict was touched and no row added or removed (ZONE 1b/1c held). TWO MEASURED DEVIATIONS from the card's proposed remedy, both in the PR body. (1) The pairing is general rather than restricted to REFUSE-*, because every verdict this file defines presupposes an uncovered row — DECLARED-NARROWER's own definition says the bare root is still not covered. (2) ZONE 2b FALSIFIED: 'such a row moves to DECLARED-NARROWER' is not available. `covered` probes hintCovers(h, `${word}/probe.file`), a file at the TOP of the root, so it turns true only for spellings that collapse back to the bare word (word+'/', word+'*', word+'**'); every genuinely narrower subtree and every deeper segment filter leaves the row uncovered — measured with a hintCovers battery, table in the PR. A covered row is therefore the bare root wearing a glob, which is exactly what DECLARED-NARROWER states it is not, so NO covered row can honestly wear any of the three verdicts. This answers 2b's REFUSE-WIDE question specifically: no — a REFUSE-WIDE declaration is TRUE of its population and narrower than nothing. The failure text names the two honest resolutions (withdraw the declaration, or withdraw the verdict — the shrink this map already permits) and picks neither, since choosing re-decides a verdict on a shrink-only map. ZONE 1e honoured: this does not reach #12328's class and the PR says so; #12328 and #12289 remain open.",
      "tests": "All at final commit c5e46219a1 (worktree from origin/main = cf99875ea8). Exit codes captured to a file BEFORE any pipe throughout.\n\nFOUR VERDICT LINES.\n(1) BEFORE, unmodified tree, --self-test: exit 0 — 'OK  self-test: 46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing.' Matches this seat's most recent control exactly.\n(2) BEFORE, live run: exit 0 — 'bare-root worklist: 46 (family, constant, word) triple(s) across 30 of 173 families, 6841 tracked files. 7 now reachable by declaration; 39 still unreachable as spelled.' / '0 untriaged row(s).'\n(3) AFTER, --self-test: exit 0 — byte-identical text to (1).\n(4) AFTER, live run: exit 0 — output diffed IDENTICAL to (2).\n\nZONE 2A REPRODUCED ON MY OWN BASE, not inherited: with the gate mutated and the worklist rolled back to base cf99875ea8, --self-test exits 0 — '46 live row(s), 38 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing'. The row left `open` (39->38) and nothing fired. That is the defect.\n\nZONE 2C ABLATION (the leg that makes the guard worth anything): injected `const ROOT_DIR_WATCH_HINTS = ['scripts/*'];` into scripts/check-ratchet-remedy-authority.mjs — PR #12061's shape, on a gate carrying REFUSE-UNSPELLABLE. Guard goes RED, exit 1, and NAMES THE ROW: 'x self-test: no recorded verdict sits on a row the sweep now finds REACHABLE — CONTRADICTED: check:ratchet-remedy-authority SCRIPTS_DIR scripts [recorded REFUSE-UNSPELLABLE]. …' The report on that tree printed the card's exact defect: 'REACHABLE  check:ratchet-remedy-authority SCRIPTS_DIR scripts' above 'The idiom has no non-recursive spelling'.\nMUTATION CONFIRMED ON DISK, not by an editor's exit code: anchored `grep -c` on the exact injected line (0 before, 1 after) plus git hash-object a41147efe985->b1f6d5da7442. RESTORE under `trap restore EXIT INT TERM`, proven byte-identical: hash back to a41147efe9858115cfc68289ba1ca9ac32ca9786, marker count 0. NO DIST LEG: the sweep readFileSync-es gate sources, nothing resolves through a package's exports, so ablation-dist-preflight does not apply — stated rather than skipped silently.\n\nGATE UNION re-derived AT the final commit with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` — stderr confirms 'derived from the tree of objectstack-ai/objectstack at commit c5e46219a1', --repo assertion holds against origin, no STALE TREE, change set 1 path from merge base cf99875ea. check:bash32-floor DID join the union and was run, as warned. All 10 derived families exit 0: check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, and bare-root-worklist --self-test; plus check:nul-bytes exit 0. Run under the shared lock: 'os-verify-lock: VERDICT command-exit 0 · held the lock 33s · waited 52s'.\n\nESLINT: the repo-wide `eslint . --no-inline-config` ran the FULL population — 5161 files by eslint's own --format json count, 0 errors, 0 warnings, this file present in the population. So there is NOTHING NARROWED and no warrant to declare. ⚠️ Declared deviation: I ran that repo-wide scan OUTSIDE the shared verify lock (the derived gates were run inside it). It completed in well under the foreground cap, but it is the heavy scan the lock exists for and I should have taken the lock for it.\n\nCONFIG INVARIANCE (recorded even though unused): eslint.config.mjs enables no type-aware linting anywhere — `grep -cE '^\\s*project:'` returns 0, and the config's own comment at line ~328 says so.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes for the PM beyond the JSON

    • ZONE 1d did not trigger. The guard is green on the tree as it stands, and this was verified rather than inferred from the arithmetic: all 7 REACHABLE rows carry no verdict (checked by pairing each report header with its why line by exact indentation), and the 39 verdicts sit on the 39 open rows. ZONE 2e holds.
    • ZONE 2d holds. sweep() already returns each row's covered, and selfTest() already has rows and TRIAGE in one scope. The pairing is three lines and needed no restructuring — no surface question.
    • skip-changeset applied and READ BACK as ["size/s","skip-changeset"]. ⚠️ Declared fallback: the additive POST /issues/12347/labels was refused with HTTP 403 ("GitHub access is not enabled for this session"), so I used read → union → whole-set write. Reasoning rather than assumption: scripts/ is not a workspace member (no scripts/package.json, not matched by any pnpm-workspace.yaml glob) and no published package's files field ships a scripts path, so this PR releases nothing — route 2 of the changeset-check step, which requires a changeset or the label and does not sniff releasable paths.
    • A closing-keyword near-miss worth recording. My first PR body drafted the scope section as "does not close [finding] #12300 retired the double-slash refusal — 4 bare-root-worklist TRIAGE rows still refuse on a mechanism the tree no longer has, and their VERDICTS (not just their prose) are now unsupported #12328's class". GitHub's parser matches close #12328 and ignores the negation, so merging would have silently closed a card that is in the decision box. Caught by auditing the body before posting; rewritten to "does not reach the class reported in [finding] #12300 retired the double-slash refusal — 4 bare-root-worklist TRIAGE rows still refuse on a mechanism the tree no longer has, and their VERDICTS (not just their prose) are now unsupported #12328".
    • Worktree removed cleanly with no --force; tree was clean and pushed first.
    • One process note: during reverse-verification git checkout <commit> -- <file> staged the base version, so my follow-up git checkout -- <file> restored from the index and dropped the guard. Recovered with zero loss because the fix was committed first, exactly as the standing rule requires; final file verified byte-identical to the commit.

    Generated by Claude Code


    Generated by Claude Code

  4. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    os-dev-report

    ⚠️ This supersedes the previous comment, which is not machine-findable. I posted the report with the os-dev-report marker as an HTML comment on its first line; the read-back showed GitHub's sanitizer had stripped it after storage, so that comment now begins with a bare code fence and the PM's scan cannot see it. No comment-edit tool is reachable from this seat, so the report is re-posted here with the marker as literal first-line text. The same sanitizer also ate two short angle-bracket fragments inside the last bullet there, turning a git checkout example into git checkout --; that note is restated below without angle brackets. The JSON is otherwise identical — nothing was re-measured or re-worded between the two.

    {
      "issue": 12064,
      "status": "done",
      "branch": "claude/issue-12064-verdict-state-contradiction",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12347",
      "premise_still_valid": true,
      "summary": "Added one assertion to bare-root-worklist --self-test: a recorded TRIAGE verdict may not sit on a row the sweep finds REACHABLE, naming the offending row and its verdict. Neither existing assertion can see that state because both audit the KEY SET — such a row is still a row (not STALE) and has left `open` (not FRESH). No TRIAGE row's why or verdict was touched and no row added or removed (ZONE 1b/1c held). TWO MEASURED DEVIATIONS from the card's proposed remedy, both in the PR body. (1) The pairing is general rather than restricted to REFUSE-*, because every verdict this file defines presupposes an uncovered row — DECLARED-NARROWER's own definition says the bare root is still not covered. (2) ZONE 2b FALSIFIED: 'such a row moves to DECLARED-NARROWER' is not available. `covered` probes hintCovers(h, word + '/probe.file'), a file at the TOP of the root, so it turns true only for spellings that collapse back to the bare word (word + '/', word + '*', word + '**'); every genuinely narrower subtree and every deeper segment filter leaves the row uncovered — measured with a hintCovers battery, table in the PR. A covered row is therefore the bare root wearing a glob, which is exactly what DECLARED-NARROWER states it is not, so NO covered row can honestly wear any of the three verdicts. This answers 2b's REFUSE-WIDE question specifically: no — a REFUSE-WIDE declaration is TRUE of its population and narrower than nothing. The failure text names the two honest resolutions (withdraw the declaration, or withdraw the verdict — the shrink this map already permits) and picks neither, since choosing re-decides a verdict on a shrink-only map. ZONE 1e honoured: this does not reach #12328's class and the PR says so; #12328 and #12289 remain open.",
      "tests": "All at final commit c5e46219a1 (worktree from origin/main = cf99875ea8). Exit codes captured to a file BEFORE any pipe throughout.\n\nFOUR VERDICT LINES.\n(1) BEFORE, unmodified tree, --self-test: exit 0 — 'OK  self-test: 46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing.' Matches this seat's most recent control exactly.\n(2) BEFORE, live run: exit 0 — 'bare-root worklist: 46 (family, constant, word) triple(s) across 30 of 173 families, 6841 tracked files. 7 now reachable by declaration; 39 still unreachable as spelled.' / '0 untriaged row(s).'\n(3) AFTER, --self-test: exit 0 — byte-identical text to (1).\n(4) AFTER, live run: exit 0 — output diffed IDENTICAL to (2).\n\nZONE 2A REPRODUCED ON MY OWN BASE, not inherited: with the gate mutated and the worklist rolled back to base cf99875ea8, --self-test exits 0 — '46 live row(s), 38 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing'. The row left `open` (39 down to 38) and nothing fired. That is the defect.\n\nZONE 2C ABLATION (the leg that makes the guard worth anything): injected a ROOT_DIR_WATCH_HINTS declaration naming the scripts root with a star into scripts/check-ratchet-remedy-authority.mjs — PR #12061's shape, on a gate carrying REFUSE-UNSPELLABLE. Guard goes RED, exit 1, and NAMES THE ROW: 'x self-test: no recorded verdict sits on a row the sweep now finds REACHABLE — CONTRADICTED: check:ratchet-remedy-authority SCRIPTS_DIR scripts [recorded REFUSE-UNSPELLABLE]. …' The report on that tree printed the card's exact defect: 'REACHABLE  check:ratchet-remedy-authority SCRIPTS_DIR scripts' above 'The idiom has no non-recursive spelling'.\nMUTATION CONFIRMED ON DISK, not by an editor's exit code: anchored `grep -c` on the exact injected line (0 before, 1 after) plus git hash-object a41147efe985 to b1f6d5da7442. RESTORE under `trap restore EXIT INT TERM`, proven byte-identical: hash back to a41147efe9858115cfc68289ba1ca9ac32ca9786, marker count 0. NO DIST LEG: the sweep readFileSync-es gate sources, nothing resolves through a package's exports, so ablation-dist-preflight does not apply — stated rather than skipped silently.\n\nGATE UNION re-derived AT the final commit with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` — stderr confirms 'derived from the tree of objectstack-ai/objectstack at commit c5e46219a1', --repo assertion holds against origin, no STALE TREE, change set 1 path from merge base cf99875ea. check:bash32-floor DID join the union and was run, as warned. All 10 derived families exit 0: check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, and bare-root-worklist --self-test; plus check:nul-bytes exit 0. Run under the shared lock: 'os-verify-lock: VERDICT command-exit 0 · held the lock 33s · waited 52s'.\n\nESLINT: the repo-wide `eslint . --no-inline-config` ran the FULL population — 5161 files by eslint's own --format json count, 0 errors, 0 warnings, this file present in the population. So there is NOTHING NARROWED and no warrant to declare. ⚠️ Declared deviation: I ran that repo-wide scan OUTSIDE the shared verify lock (the derived gates were run inside it). It completed well under the foreground cap, but it is the heavy scan the lock exists for and I should have taken the lock for it.\n\nCONFIG INVARIANCE (recorded even though unused): eslint.config.mjs enables no type-aware linting anywhere — a grep for a project: key returns 0, and the config's own comment at line ~328 says so.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes for the PM beyond the JSON

    • ZONE 1d did not trigger. The guard is green on the tree as it stands, and this was verified rather than inferred from the arithmetic: all 7 REACHABLE rows carry no verdict (checked by pairing each report header with its why line by exact indentation), and the 39 verdicts sit on the 39 open rows. ZONE 2e holds.
    • ZONE 2d holds. sweep() already returns each row's covered, and selfTest() already has rows and TRIAGE in one scope. The pairing is three lines and needed no restructuring — no surface question.
    • skip-changeset applied and READ BACK as ["size/s","skip-changeset"]. ⚠️ Declared fallback: the additive POST /issues/12347/labels was refused with HTTP 403 ("GitHub access is not enabled for this session"), so I used read then union then whole-set write. Reasoning rather than assumption: scripts/ is not a workspace member (no package.json there, not matched by any pnpm-workspace.yaml glob) and no published package's files field ships a scripts path, so this PR releases nothing — route 2 of the changeset-check step, which requires a changeset or the label and does not sniff releasable paths.
    • A closing-keyword near-miss worth recording. My first PR body drafted the scope section as "does not close [finding] #12300 retired the double-slash refusal — 4 bare-root-worklist TRIAGE rows still refuse on a mechanism the tree no longer has, and their VERDICTS (not just their prose) are now unsupported #12328's class". GitHub's parser matches that keyword plus the number and ignores the negation, so merging would have silently closed a card that is in the decision box. Caught by auditing the body before posting; rewritten to "does not reach the class reported in [finding] #12300 retired the double-slash refusal — 4 bare-root-worklist TRIAGE rows still refuse on a mechanism the tree no longer has, and their VERDICTS (not just their prose) are now unsupported #12328".
    • Worktree removed cleanly with no --force; tree was clean and pushed first.
    • One process note, restated without the angle brackets the sanitizer ate: during reverse-verification, checking a file out from a commit (git checkout COMMIT -- PATH) also stages it, so my follow-up git checkout -- PATH restored from the index and dropped the guard. Recovered with zero loss because the fix was committed first, exactly as the standing rule requires; the final file was verified byte-identical to the commit.

    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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions