Uh oh!
There was an error while loading. Please reload this page.
fix(pm): report claim-shaped comments with a non-canonical separator (H34), without widening the claim marker - #12272
Merged
Conversation
…(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
This was referenced Aug 25, 2026
os-zhuang
marked this pull request as ready for review
August 25, 2026 15:38
os-zhuang
enabled auto-merge
August 25, 2026 15:38
Uh oh!
There was an error while loading. Please reload this page.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes#12090
Report-only patrol change,
scripts/pm/check-half-states.mjsonly. 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.mdstep 4, recording the maintainer's 2026-08-11 ruling:So the accepted set is not widened by one character.
CLAIM_COMMENT_MARKERaccepts 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
governingClaimgates on the same marker before readingBranch:, 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/Claimedfollowed 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 matchesClaim:. 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 abovein 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.claimedBranches'sBranch(?: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 theBranch:separator, so nothing is widened there either.3. The prose contradiction corrected. The
latestClaimCommentheader 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:
CLAIM_COMMENT_MARKERClaim — skills seat \session_01RM`. …`Claim: skills seat \session_01RM`+Branch:`the next seat should claim: only after the ruling landsClaim:skills seat \session_01RM`, round R36.`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
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 undertrap … EXIT INT TERM. Predicted direction was recorded before each run.CLAIM_COMMENT_MARKERto accept U+2014 (the change the ruling forbids)✗ H34: …and CLAIM_COMMENT_MARKER is still false on it (⛔ 不放宽谓词) (got true, want false)[ \t]*to\s*✗ 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 pinnedbyte-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 null→if (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:dispatchedpopulation:Claim:governingClaimcoverageThe 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 set1 path(s) vs merge base 107bb4ba4. Every run below, the self-test and both measurements included, is at the final commit628575e3e(git rev-parse --short HEADafter 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$?:check:pm-half-states✓ check-half-states self-test: 1380 cases pass.check:agent-test-spelling✓ check-agent-test-spelling: 0 violations — 378 file(s) · 3812 bare -- token(s) …check:cross-package-test-inputsOK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check:entry-guard✓ check:entry-guard: 162 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 118 export bindings, 116 of them inert on importcheck:parse-guard✓ check:parse-guard: 161 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.check:partof-closing-keyword✓ check-partof-closing-keyword self-test: 28 cases pass.check:pnpm-filter-targets✓ check:pnpm-filter-targets: 136/173 --filter occurrence(s) across 28 file(s) resolve against 78 workspace package(s)check-ci-filter-parity.mjsOK: all 96 declared cross-package glob(s) (81 unique) are covered by core or crosspkg …check-closing-keyword-parity.mjscheck-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators …)check-cross-package-test-inputs.mjsOK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check-nul-bytes.mjscheck-nul-bytes: OK (scanned 6775 text file(s) … no raw ASCII control bytes).check-partof-closing-keyword.mjs(PR-body arm)PR_BODY— verdict quoted in the PR commentnode scripts/pm/check-half-states.mjsunderhalf-state-patrol.ymlis 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 — labelledskip-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 gonull, and the suite died on aTypeErrorat the first such line instead of reporting named failing cases, so every case after it never ran. Filed unassigned as afindingrather 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.os-zhuangthrough 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