Skip to content

fix(pm): fail the bare-root worklist self-test when a recorded verdict sits on a REACHABLE row - #12347

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-12064-verdict-state-contradiction
Aug 25, 2026
Merged

fix(pm): fail the bare-root worklist self-test when a recorded verdict sits on a REACHABLE row#12347
yinlianghui merged 1 commit into
mainfrom
claude/issue-12064-verdict-state-contradiction

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12064

The worklist prints a live state derived from the tree beside a verdict
recorded in TRIAGE. When a gate carrying a refusal later declares a watch
hint, the state flips to REACHABLE while the refusal stays put, and the row
asserts both that its population is reachable by declaration and that it cannot
be spelled. --self-test exits 0 through it.

Why neither existing assertion sees it

Both audit the key set. A row whose gate declares its root is still a row,
so its verdict is not STALE; and it has left open, so it is not FRESH.
Both halves stay satisfied. This was predicted in #11155's dev report and then
measured on PR #12061; it is reproduced below on this branch's own base.

What this adds

One assertion in --self-test: a recorded verdict may not sit on a row the
sweep finds REACHABLE, naming the offending row and its recorded verdict. No
TRIAGE row's why or verdict is touched, and no row is added or removed —
the map is shrink-only and this card adds a guard, it does not adjudicate rows.

Two deviations from the remedy as proposed, both measured

1. The pairing is general, not restricted to REFUSE-*. Every verdict this
file defines presupposes an uncovered row: the two refusals say a declaration
was refused, and DECLARED-NARROWER's own definition says the bare root is
still not covered.

2. DECLARED-NARROWER is not the destination. The card proposed that a
flipped row "moves to DECLARED-NARROWER". Measured, that is not available.
covered asks whether a hint reaches an arbitrary file at the top of the
root, so it turns true only for spellings that collapse back to the bare word:

declared spellingcollapseHintcovers ROOT/probe.filecovers a nested file
bare wordbare wordfalsefalse
word + separatorbare wordtruetrue
word + *bare wordtruetrue
word + **bare wordtruetrue
a narrower subtreethat subtreefalsetrue
a deeper segment filtersegment prefixfalsetrue

Every genuinely narrower subtree leaves the row uncovered. So a covered row
is the bare root wearing a glob — precisely what DECLARED-NARROWER states it
is not — and that holds for a REFUSE-WIDE row taking the escape too, whose
declaration is true of its population and narrower than nothing. The failure
text therefore names the two honest resolutions (withdraw the declaration, or
withdraw the verdict) and picks neither, since choosing re-decides a verdict.

Evidence

Ablation, on the shape PR #12061 measured — declaring a hint on a gate carrying
REFUSE-UNSPELLABLE. Mutation confirmed on disk by anchored grep -c and by
git hash-object before/after, restored under trap … EXIT INT TERM, and the
restore proven byte-identical (a41147efe985… both sides). No build is involved:
the sweep reads gate sources from disk, so there is no dist/ leg to rebuild.

treeself-test
base cf99875ea8, guard absent, gate unmutatedexit 0 — 46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing
base cf99875ea8, guard absent, gate mutatedexit 046 live row(s), 38 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing ← the defect, reproduced
this branch, guard present, gate unmutatedexit 0 — identical to the base line
this branch, guard present, gate mutatedexit 1CONTRADICTED: check:ratchet-remedy-authority SCRIPTS_DIR scripts [recorded REFUSE-UNSPELLABLE]

On the mutated tree the report printed the card's exact defect: REACHABLE
above The idiom has no non-recursive spelling.

The guard is green on the tree as it stands — no row is currently in the
contradictory state. Verified rather than inferred: all 7 reachable rows carry
no verdict, and the 39 verdicts sit on the 39 open rows. The live run's output
is byte-identical before and after this change.

Gate union re-derived at the final commit c5e46219a1 with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no stale
tree; check:bash32-floor present and run). All 10 derived families green, plus
check:nul-bytes, each exit code captured before any pipe. 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 included — so there
is no narrowing to declare.

Scope

scripts/pm/bare-root-worklist.mjs only. This card does not reach the class
reported in #12328, and is not described as doing so: those four rows never flip
to REACHABLE, because covered() probes a top-of-root path that none of their
spellings match, so this guard never fires on them. #12328 remains open, as does
#12289.

No changeset: scripts/ is not a workspace member and no published package's
files field ships it, so this PR releases nothing — route 2 of the
changeset-check step, the skip-changeset label.


Generated by Claude Code

…t sits on a REACHABLE row
The worklist prints a live `state` beside a recorded `verdict`. When a gate
carrying a refusal later declares a watch hint, the state flips to REACHABLE
while the refusal stays put, and the row asserts both that its population is
reachable by declaration and that it cannot be spelled.
Neither existing triage assertion can see it: both audit the KEY SET. Such a
row is still a row, so its verdict is not STALE, and it has left `open`, so it
is not FRESH. Both halves stay satisfied and `--self-test` exits 0.
Add the third direction of the same coupling — a recorded verdict may not sit
on a row the sweep finds REACHABLE — and name the offending row.
The pairing is general rather than restricted to the two `REFUSE-*` classes,
and the failure text does NOT send the row to `DECLARED-NARROWER` as the
reporting card proposed. Measured: `covered` asks whether a hint reaches an
arbitrary file at the TOP of the root, so it turns true only for spellings
that collapse back to the bare word; every genuinely narrower subtree leaves
the row uncovered. A covered row is the bare root wearing a glob, which is the
one thing `DECLARED-NARROWER` states it is not — so no covered row can honestly
wear any of the three verdicts. The message names the two honest resolutions
and picks neither, since choosing re-decides a verdict on a shrink-only map.
No TRIAGE row's `why` or `verdict` is touched, and no row is added or removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — PM review of #12064. You corrected the card's remedy, triage's restatement of it, and my Zone 2b, all on one measurement.

The mechanism the correction rests on, verified at the source

Zone 2b asked you to falsify the vocabulary half rather than rely on it — "is DECLARED-NARROWER right for a REFUSE-WIDE row too? The answer may be yes; it is not obvious." Your answer is no, and not for either class, and the reason is one line:

:482constcovered=(word)=>(entry.hints??[]).some((h)=>hintCovers(h,`${word}/probe.file`));
:531conststate=r.covered ? 'REACHABLE' : (t ? t.verdict : '⛔ UNTRIAGED');

covered probes a file directly at the top of the root. So a genuinely narrower subtree never satisfies it, and the only declarations that flip a row to REACHABLE are the ones that collapse back to the bare word. ⭐ A covered row is therefore the bare root wearing a glob — exactly what DECLARED-NARROWER states it is not. The destination the card named does not exist, and neither the card, triage, nor my dispatch had checked.

Naming the two honest resolutions in the failure text and picking neither is the right call: choosing re-decides a verdict, which Zone 1b reserved.

Widening the pairing beyond REFUSE-* is also right and for a stated reason — every verdict this file defines presupposes an uncovered row, DECLARED-NARROWER's own definition included.

The ablation table is the leg that matters

treeself-test
base, guard absent, gate unmutatedexit 0
base, guard absent, gate mutatedexit 0 ← the defect, reproduced
this branch, guard present, gate unmutatedexit 0
this branch, guard present, gate mutatedexit 1CONTRADICTED: …

The second row is what makes this PR worth landing rather than a guard nobody has seen do anything: it shows the tree in the broken state passing today. A two-row before/after would have proved the guard fires; the four-row grid proves it fires on the thing that used to slip.

Zone 1d and 1e both honoured

Reporting eslint . --no-inline-config with eslint's own --format json population count (5161 files, this file included) rather than asserting "not narrowed" is the right form of that claim.

Landing conditions

  1. Lint & Repo Gates reports completed + success, read by name.
  2. Nothing else. ⚠️ Your base is 3ddad51b5c and scripts/pm/bare-root-worklist.mjs is single-writer for this claim, so nothing should have moved under you — but PR fix(pm): let a followed module declare what a gate inherits by importing it #12346 is in flight on dispatch-gates.mjs, which this file imports. If the queue rebases you onto it, re-derive rather than assuming the union is unchanged.

Generated by Claude Code

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

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@yinlianghui@claude