fix(pm): refuse a claim's unreadable keyed lines before post-stamped writes the comment - #19435
Conversation
…is written `Seat:`, `Thread-read:` and `Clause-②:` are exact-value fields three post-hoc readers enforce after the claim is on the board. `post-stamped --comment` now reads them through those same readers — `claimSeatNumber`, `threadReadField` and `readClause2Line`, imported, never re-spelled — and REFUSES the write (EXIT_REFUSED, before any request) when one of them cannot read a line, naming the key, the offending line and the spelling that reads. Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi Co-authored-by: Claude <noreply@anthropic.com>
…sed-at-write-time Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver levelNone — nothing published; ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19152
Clause-②: no
A claim's
Seat:,Thread-read:andClause-②:lines are exact-value fields. Three post-hoc readers enforce them AFTER the claim is stored; one seat produced five half-state rows off three of them in a single shift, always the same shape — the key right, the value right, an explanation added on the same line.post-stamped.mjsis the one writer every claim passes through, and it said nothing.It now reads those three lines on the
--commentpath, through the very functions that own them, and REFUSES the write before any request when one of them cannot be read — naming the key, quoting the offending line and printing the spelling that reads.check-half-states.mjsprescribed exactly this instrument, in its ownSeat:reader's docblock:The three owning readers, quoted beside mine
Seat:claimSeatNumber(check-half-states.mjs): key line/^[ \t]*(?:[-*+][ \t]+)?>?[ \t]*(?:\*\*)??Seat?(?:\*\*)?[ \t]*:[ \t]*(.*)$/im, then#(\d+); absent ⇒ 1, present-but-unreadable ⇒null(PR #19423)claimSeatNumber(body) === null— the imported call, nothing elseThread-read:threadReadFieldreturns{present, value};h50ThreadReadMismatchthen asksfield.present && field.value === found.expected, whereexpectediscommentIdText(/^[1-9]\d*$/) or the literalnoneClause-②:readClause2Line(check-clause2-carriers.mjs) — four-valued:declared/malformed/near-miss(describing,inline-key,spelling) /nullnullnordeclared;--pairanswers exit 4 on thoseNo second parser: the three readers are imported and called. The one spelling this file adds is the
Thread-read:value shape, pinned in the self-test againstcheck-half-states.mjs's own source text (itscommentIdTextpattern and its'none'literal are not exported), which is the coupling the claim prescribed.Before / after — measured,
--dry-runon each bodySeat:declared inside the opening sentenceThread-read:listing nine ids`Clause-②: yes`followed by prose (thedescribingnear miss)Clause-②: yes — the ruling states it outright (…)Claim:os-dev-reportcomment## Contract reviewbody quoting a claim'sSeat:lineThe refusal, on the first of those:
Two dispatch assumptions REFUTED, and corrected here
①
Clause-②is NOT "the value and nothing after it". The dispatch order described the clause reader as acceptingyes/no"and nothing after it". Measured onreadClause2Line: trailing reasoning on a bare-key line is ACCEPTED, deliberately, and its own docblock records why — #13914's control case is «the PM claim comment on #12297 carriesClause-②: yeswith reasoning» and is recorded there as the shape that is CORRECT, after a stricter reading rejected four real claims on the live board. What that reader refuses is structural:describing(the key twice on the line, or a code span opened before the key, closed later, and the line then continuing),inline-key(the key off the line start) andmalformed(a value it cannot grade). So the measured shape in the card's table is refused here through its quoted-and-continued spelling, and a bareClause-②: yes — reasoningis left alone. Refusing it would have made this tool reject the shape the owning gate prescribes — the second dialect this card exists to avoid.② The scope is NOT "first line opens with
Claim:". The claim and the card both spelled it that way; the fleet's own definition isCLAIM_COMMENT_MARKERthroughmarkerMatches, which matches aClaim:line anywhere in the body, andnewestLaneClaim's header refuses a stricter reading by name: «Defining a stricter first-line-only claim for this row alone would leave the file disagreeing with itself about what a claim IS — H2 calling a card claimed while H38 called the same comment invisible». A first-line rule here would decline to judge bodies H50 reads as claims, which is the hole restated one layer up. The marker is imported.Assumptions ①(the tool reads no keyed line today — zero mentions of the three keys before this diff), ③(reuse
EXIT_REFUSED; a refusal before any request) and ④(extend the existing--self-test, no second entry point) held as stated.REFUSE, not warn — on the four axes
Seat:line is not line-initial is silently read as seat 1 — three claims in one round misattributed, producing a false H38 stale row on another seat post #19108) accused ANOTHER seat's post. A warning on a tool whose stdout is read by scripts is a row nobody clears — this file's own header records the sibling case where a refusal was piped away and the write that followed landed anyway.SKILL.mdranks remedies «(a) 删容许出错的构造 → (b) 让正确形态成唯一拼写 → (c) 加检查», and the card's own reading is that every existing reader is a post-hoc (c). Refusing at the single writer makes the readable spelling the only storable one: a (b) fix at the one chokepoint, with no new checker to keep in step.--force, no flag, no config. The template's other keyed lines (Session:,Branch:,Worktree:,Domain:,File surface:,Container & model:,Serial constraints cleared:) are untouched: they have no exact-value reader to disagree with, so there is nothing to mirror.The two decisions the card asked to be pinned
A key inside a code fence or a code span is judged exactly as in prose. The tool already has a quoted-span mask (
quotedSpans) and it is deliberately NOT reused here. All three owning readers read raw text, so masking would let a body store the exact line the patrol then files — a refusal bought off with backticks. This is the asymmetry this file already states for stamps: quoting changes what is RENDERED, never what was AUTHORED. Two self-test cases pin both directions: backticks around a mid-sentence declaration buy no exemption, while a line-initial declaration inside a fence still reads as the declaration its owner reads there.A key present twice reads the way its owner reads it — the first line-initial declaration decides, and a later one is neither read nor refused. That is not a choice this file makes; it is what
CLAIM_SEAT_KEY_LINE.exec,THREAD_READ_KEY_LINE.execandreadClause2Line's first-declaration-wins loop already do, and a write side that judged the second line would refuse a body its readers call clean. Pinned by a case: a readable declaration followed by a malformed duplicate passes.Scope, budget and verification
scripts/pm/post-stamped.mjsonly,--commentonly (a claim IS a comment; a seat POST's body carries aSeat:line no claim reader judges).check-half-states.mjsandcheck-clause2-carriers.mjsare untouched — read, imported, not copied.node scripts/pm/post-stamped.mjs --self-test:: exit 0 — 530 cases across 18 batteries, the new battery pinned at a floor of 20.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, reconciled with--ran): 30 commands, every exit captured before any pipe. All green.pnpm exec eslint --no-inline-config scripts/pm/post-stamped.mjs:: exit 0. The repo-wide scan is CI's run.skip-changeset:scripts/pm/**publishes nothing from any released package.node scripts/pm/check-governed-merges.mjs --pr Nreads NOT governed —scripts/pm/is on no register row.Acceptance notes
Thread-read:write-time rule can only decide SHAPE — whether the id is the right one is H50's, and it needs the thread this act has not fetched. A future--thread-readproof-of-reading flag (the shape--ack-throughalready has on--body) would close that half; nobody is blocked on it today. 承接者: the next card on this file's--commentpath.commentIdTextand the'none'literal incheck-half-states.mjsare not exported, so this file pins their spelling by reading that file's source. Exporting them would turn the coupling into an import. 承接者: whoever next edits H50's reader.Generated by Claude Code