Skip to content

fix(pm): report claim-shaped comments with a non-canonical separator (H34), without widening the claim marker - #12272

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12090-claim-marker-runbook
Aug 25, 2026
Merged

fix(pm): report claim-shaped comments with a non-canonical separator (H34), without widening the claim marker#12272
os-zhuang merged 1 commit into
mainfrom
claude/issue-12090-claim-marker-runbook

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Closes#12090

Report-only patrol change, scripts/pm/check-half-states.mjs only. Three changes, one PR, per the maintainer's ACCEPT-redirect adjudication on the card.

The constraint this PR honors, verbatim

.claude/skills/pm-dispatch/SKILL.md step 4, recording the maintainer's 2026-08-11 ruling:

首行以字面 Claim: 开头是机器判据(维护者 2026-08-11 裁定;巡查谓词只认这一个拼写且保持严格,修法是全舰队向文档拼写收敛,⛔ 不放宽谓词)

So the accepted set is not widened by one character. CLAIM_COMMENT_MARKER accepts exactly the canonical colon before this PR and after it, and the self-test asserts that on the same specimen that fires the new row — see the four-way acceptance split below. What the card asked for (accept em/en dash) is not taken; what it actually cost is closed a different way.

What was wrong, and what this does about it

A dash-written claim is a malformed claim under that ruling — but it was also invisible. H2 reported the card as claimless (correctly), while governingClaim gates on the same marker before reading Branch:, so H20 could never report a missing remote ref for it and H27 could never report a dead claim. A dispatch abandoned behind a malformed claim sat outside dispatch-liveness entirely, silently, for as long as it was in flight. H33 inherited the same blindness.

1. New report-only row H34 — 「a claim-shaped comment with a non-canonical separator」. Fires on a pm-tracked, assigned card when a comment line begins with Claim/Claimed followed by U+2014, U+2013, U+002D or U+FF1A, the remainder carries the protocol's own content (a session reference, the word "seat", or a protocol-shaped branch anywhere in the comment), and no comment on the thread matches Claim:. The row names the separator by codepoint and its remedy points at the write side and the canonical spelling. Claim: stays the single machine criterion — H2/H20/H27/H33 are untouched byte for byte, and the file never gains a second notion of what a claim is.

Free by construction: judged in the branch that already fetched H2's comment page, on exactly H2's population, so the sweep makes no additional request. Two suppressions are deliberate under-reporting, both stated in the header: a thread that also carries a canonical claim is silent (that card is machine-visible, so the row has no remedy to offer), and a claim-shaped line with none of the protocol's content is silent (Claim - see above in prose is not an attempted claim).

2. The [::] classes collapsed to a single ASCII :. Verified by codepoint: 5b 3a 3a 5d — U+003A written twice, never the fullwidth U+FF1A the shape implied to three separate readers (this file's own H33 note, the filing card's body, and a grading comment all repeated it). Behaviour is byte-identical; the comment no longer lies to the next reader, and U+FF1A now surfaces on the H34 row instead of being silently invisible.

⚠️Declared, because it is one line past the dispatched scope: the same duplicated-U+003A typo sat in claimedBranches's Branch(?:es)?\s*[::] class. Collapsed in the same edit — same file, same defect, same one-character mechanical shape, behaviour-preserving, and pinned by three new self-test cases (branch line: the canonical colon still reads / Branches: still reads / a FULLWIDTH colon does not read, and never did). Fixing one and leaving the other would have left the file carrying the exact defect this PR corrects. No ruling touches the Branch: separator, so nothing is widened there either.

3. The prose contradiction corrected. The latestClaimComment header called em-dash claims 「real claims, correctly formed」, a straight contradiction of the protocol the file enforces. Reworded to the ruling: malformed spelling, write-side repair, and a pointer to H34 as where those claims now surface. The matching self-test comment is corrected the same way.

Acceptance — the four-way split, asserted on one specimen each

Each case asserts both readers on the same input, so no case can go green by quietly redefining a claim:

specimenH34 rowCLAIM_COMMENT_MARKER
Claim — skills seat \session_01RM`. …`firesfalse
Claim: skills seat \session_01RM`+Branch:`silenttrue
the next seat should claim: only after the ruling landssilentfalse
Claim:skills seat \session_01RM`, round R36.`firesfalse (and never matched)

Plus: Claimed —, en dash, hyphen-minus and the blockquoted template each pinned by name; multi-comment counting and separator de-duplication; every gate H2 has (untracked, unassigned, unconsulted, unreadable, empty); and the [ \t]*-not-\s* decision pinned twice — once on the exported regex and once on the reader — because the reader splits per line and its case alone survives that mutation (measured, below).

Tests

$ node scripts/pm/check-half-states.mjs --self-test
✓ check-half-states self-test: 1380 cases pass. # 1336 before, +44

Reverse verification — three ablation legs, each proved on disk before it was read

No dist/ is in play: this script is executed from source, so the mutation reaches the run directly. Every leg asserted its anchor's occurrence count before writing (an editor's exit code is not evidence a replacement landed), re-grepped for the injected and removed text after, and the whole script restores under trap … EXIT INT TERM. Predicted direction was recorded before each run.

legmutationpredictedobserved
Awiden CLAIM_COMMENT_MARKER to accept U+2014 (the change the ruling forbids)REDRED — 12 of 1380 failed, incl. ✗ H34: …and CLAIM_COMMENT_MARKER is still false on it (⛔ 不放宽谓词) (got true, want false)
Brelax the near-miss marker's [ \t]* to \s*RED on the regex case onlyRED — 1 of 1380: ✗ H34: the exported marker does not span lines (the class, not the split) (got true, want false); the reader-level case stayed green, which is why both are pinned
restoregreen, byte-identicalbyte-compare vs pre-ablation copy: IDENTICAL · ✓ check-half-states self-test: 1380 cases pass.

Leg C dropped the canonical-claim suppression (if (…CLAIM_COMMENT_MARKER.test…) return nullif (false)): predicted and observed RED — 1 of 1380, ✗ H34: a thread carrying BOTH spellings is silent, proving that suppression case is not vacuously green.

Leg B is why this PR is not one case lighter: the first draft pinned the newline property only at the reader, and leg B ran green against it. The regex-level case exists because a mutation survived.

Live measurement — the new row over the real board

Run with the shipped predicates (not a hand-rolled regex), 2026-08-25T15:24:42Z, over the open pm:dispatched population:

count
population38 cards
canonical Claim:33
near-miss only (the new H34 row)1
neither spelling4
H2 rows5 (unchanged — H34 adds no row H2 was not already firing)
governingClaim coverage24 (unchanged — the marker is untouched)

The one card is #11925, claim comment Claim — \domain:cli` lane execution seat, session `019siH5jDmk5hrayvfyojUqR`, round R36.` — EM DASH (U+2014). It was one of the five cards H2 reports as claimless; that H2 row now has an explanation attached to it, and the card becomes visible to the patrol as a malformed claim rather than a missing one.

Exactness, probed separately and wider than the row itself. A supplementary sweep over the same 38 cards for every line opening with the word, whatever character follows, found exactly two non-canonical hits: #11925 above, and Claiming this card. on #9165 — a paraphrase, not a separator, and correctly declined (H34 reports a wrong separator, not a wrong sentence). So on the live board: 1 detection, 0 false positives, 0 missed near misses.

Context on magnitude, since the card's framing has decayed: the filing census measured 24 dash claims of 54 open cards on 2026-08-25 morning; the previous round measured 1 of 45 that afternoon; this run measures 1 of 38. The maintainer's prescribed repair — fleet-wide convergence to the documented spelling — is measurably working, which is exactly why the read-side widening was not worth spending a ruling on. The row's expected steady state is quiet, in H23's register rather than H30's, and its value is that a seat writing the wrong spelling learns about it while the card is still in flight.

Gates

Derived, not recalled: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack → exit 0, --repo 'objectstack-ai/objectstack' checked against this checkout's 'origin' remote — it holds., change set 1 path(s) vs merge base 107bb4ba4. Every run below, the self-test and both measurements included, is at the final commit 628575e3e (git rev-parse --short HEAD after the last commit, before this body was written). Every exit code was captured before any pipe, and each verdict line below is the gate's own, never a bare $?:

gateexitits own verdict line
check:pm-half-states0✓ check-half-states self-test: 1380 cases pass.
check:agent-test-spelling0✓ check-agent-test-spelling: 0 violations — 378 file(s) · 3812 bare -- token(s) …
check:cross-package-test-inputs0OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
check:entry-guard0✓ check:entry-guard: 162 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 118 export bindings, 116 of them inert on import
check:parse-guard0✓ check:parse-guard: 161 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
check:partof-closing-keyword0✓ check-partof-closing-keyword self-test: 28 cases pass.
check:pnpm-filter-targets0✓ check:pnpm-filter-targets: 136/173 --filter occurrence(s) across 28 file(s) resolve against 78 workspace package(s)
check-ci-filter-parity.mjs0OK: all 96 declared cross-package glob(s) (81 unique) are covered by core or crosspkg …
check-closing-keyword-parity.mjs0check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators …)
check-cross-package-test-inputs.mjs0OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
check-nul-bytes.mjs0check-nul-bytes: OK (scanned 6775 text file(s) … no raw ASCII control bytes).
check-partof-closing-keyword.mjs (PR-body arm)0run with this body in PR_BODY — verdict quoted in the PR comment

node scripts/pm/check-half-states.mjs under half-state-patrol.yml is the live sweep arm of the same script; its offline face is the self-test above, and the live face is the measurement section.

No changeset: this is internal PM patrol tooling under scripts/pm/, published to nobody — labelled skip-changeset (readback confirmed: size/m, skip-changeset).

Out of scope, filed not fixed

Issue #12273 — the self-test's 111 bare .includes() calls on nullable predicate returns, across 18 predicates. Ablation leg A hit it live: a legitimate mutation made a row go null, and the suite died on a TypeError at the first such line instead of reporting named failing cases, so every case after it never ran. Filed unassigned as a finding rather than fixed here — different defect class, and the right shape (per-row wrapper vs. shared helper vs. leave it) is a judgement call, not pinned by existing evidence. The new row's own cases use the local wrapper, which is why leg A's second run reports 12 named failures instead of a stack trace.

⚠️ Review note: a review could not be requested from os-zhuang through the API — all seats share that identity, so GitHub refuses it as "Review cannot be requested from pull request author". This PR stays draft with auto-merge unarmed, and PM review is requested through the report on the card instead.


Generated by Claude Code


Generated by Claude Code

…(H34)
The claim marker requires the literal `Claim:` and the 2026-08-11 maintainer
ruling keeps it that way (⛔ 不放宽谓词). A dash-written claim is therefore
MALFORMED — but it was also invisible: H2 reported the card as claimless while
H20/H27/H33 could not see it at all, so an abandoned dispatch behind a malformed
claim was outside dispatch-liveness entirely.
H34 reports the near miss on its own row without redefining what a claim is.
`Claim:` stays the single machine criterion, byte for byte; the row names the
separator by codepoint and points the remedy at the canonical spelling.
Also: collapse the `[::]` separator classes (two ASCII colons, never the
fullwidth U+FF1A their shape implied) to a single `:` — behaviour-identical —
and correct the H33 prose that called em-dash claims "real claims, correctly
formed" against the protocol this file enforces.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 25, 2026 15:38
@os-zhuang
os-zhuang enabled auto-merge August 25, 2026 15:38
@os-zhuang
os-zhuang added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 48c7c34Aug 25, 2026
41 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12090-claim-marker-runbook branch August 25, 2026 16:23
os-zhuang pushed a commit that referenced this pull request Aug 26, 2026
…ssertion through a row-text wrapper
The self-test's message assertions read `predicate(...).includes(needle)` on
predicates that are three-valued by design (null when clean, a string when the
row fires). The moment a change makes such a predicate go clean, that line
throws `TypeError: Cannot read properties of null (reading 'includes')` while
evaluating t()'s arguments — before t() runs — so the suite ABORTS there,
every later case never runs, and the output names a TypeError instead of a row.
Generalizes the per-predicate wrapper PR #12272 established for H34
(`const h34row = (...args) => String(fn(...args) ?? '')`) to the remaining 18
predicates, plus the two local nullable helpers (`halves`, `dead27`) that carry
the same shape. 145 call sites switched; predicates themselves are unchanged and
their `typeof`/`=== null` assertions deliberately stay un-wrapped.
Self-test output is byte-identical to before: 1380 cases pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

@os-zhuang@claude