Commit b71d9e7
fix(pm): refuse a claim's unreadable keyed lines before post-stamped writes the comment (#19435)
Fixes #19152
Clause-②: no
A claim's `Seat:`, `Thread-read:` and `Clause-②:` 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.mjs` is the one writer
every claim passes through, and it said nothing.
It now reads those three lines on the `--comment` path, **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.mjs` prescribed exactly this instrument, in its own
`Seat:` reader's docblock:
> ⛔ The presence probe below therefore does NOT widen
`CLAIM_SEAT_KEY_LINE`: accepting a malformed declaration would leave the
writer no way to find out either, which is the same defect wearing the
other costume. **Refusing it at the writing path is a different
instrument's job.**
## The three owning readers, quoted beside mine
| key | the owner's own reader | what this tool calls a refusal |
|---|---|---|
| `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 else |
| `Thread-read:` | `threadReadField` returns `{present, value}`;
`h50ThreadReadMismatch` then asks `field.present && field.value ===
found.expected`, where `expected` is `commentIdText` (`/^[1-9]\d*$/`) or
the literal `none` | the field reader for the line; a value outside
`/^(?:[1-9]\d*|none)$/` can never equal that `expected`, plus a presence
probe for a declaration off the line start |
| `Clause-②:` | `readClause2Line` (`check-clause2-carriers.mjs`) —
four-valued: `declared` / `malformed` / `near-miss` (`describing`,
`inline-key`, `spelling`) / `null` | anything that is neither `null` nor
`declared`; `--pair` answers exit 4 on those |
No 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 against `check-half-states.mjs`'s own source text (its
`commentIdText` pattern and its `'none'` literal are not exported),
which is the coupling the claim prescribed.
## Before / after — measured, `--dry-run` on each body
| body | before (488f4f5) | after |
|---|---|---|
| `Seat:` declared inside the opening sentence | exit 0, stored | **exit
2, nothing written** |
| `Thread-read:` listing nine ids | exit 0, stored | **exit 2, nothing
written** |
| `` `Clause-②: yes` `` followed by prose (the `describing` near miss) |
exit 0, stored | **exit 2, nothing written** |
| `Clause-②: yes — the ruling states it outright (…)` | exit 0 | exit 0
— deliberately unchanged, see the correction below |
| a well-formed `Claim:` | exit 0 | exit 0 |
| an `os-dev-report` comment | exit 0 | exit 0 |
| a `## Contract review` body quoting a claim's `Seat:` line | exit 0 |
exit 0 |
The refusal, on the first of those:
```text
post-stamped: REFUSED — 1 keyed line(s) in this `Claim:` cannot be read by the checker that owns them. Nothing was written.
1. [Seat] `claimSeatNumber` reads `null` here — the declaration is present and names no seat. The claim lands on NO seat, and H38 rows another seat's post over it.
line: Claim: PM loop round 1 — dispatched by the skills seat. Seat: domain:skills#2 — R1, 2026-09-20T20:31Z.
write: `Seat: domain:LANE#N` at the START of a line, lane and number both — e.g. `Seat: domain:skills#2`
```
## Two dispatch assumptions REFUTED, and corrected here
**① `Clause-②` is NOT "the value and nothing after it".** The dispatch
order described the clause reader as accepting `yes`/`no` "and nothing
after it". Measured on `readClause2Line`: 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 carries
`Clause-②: yes` **with 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) and `malformed` (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 bare `Clause-②: yes — reasoning` is 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 is
`CLAIM_COMMENT_MARKER` through `markerMatches`, which matches a `Claim:`
line anywhere in the body, and `newestLaneClaim`'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
- **实际业务需求** — measured, not supposed: five rows in one shift, three
keys, three detectors, every one found minutes to hours later; and one
of them (#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.md` ranks 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.
- **防 AI 写代码犯错** — the refusal is the contract-first direction: loud at
authoring time, with the exact spelling printed, rather than a lenient
consumer. A warning would be precisely the tolerant seam this repo
forbids — the malformed line would still be stored, and the reader that
cannot parse it would still file its row.
- **创业阶段不扩散** — three keys, the ones the card measured, and no other; no
`--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.exec` and
`readClause2Line`'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.mjs` only, `--comment` only (a claim IS a
comment; a seat POST's body carries a `Seat:` line no claim reader
judges). `check-half-states.mjs` and `check-clause2-carriers.mjs` are
untouched — read, imported, not copied.
- Net **+120 / -0** lines, the claim's budget exactly, self-test cases
included.
- `node scripts/pm/post-stamped.mjs --self-test` :: exit 0 — 530 cases
across 18 batteries, the new battery pinned at a floor of 20.
- The derived union on the merged head (`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 N` reads NOT governed
— `scripts/pm/` is on no register row.
## Acceptance notes
- Noted, not filed: the `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-read`
proof-of-reading flag (the shape `--ack-through` already has on
`--body`) would close that half; nobody is blocked on it today. 承接者: the
next card on this file's `--comment` path.
- Noted, not filed: `commentIdText` and the `'none'` literal in
`check-half-states.mjs` are 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](https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent b929e0a commit b71d9e7
1 file changed
Lines changed: 120 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
361 | 361 | | |
362 | 362 | | |
363 | 363 | | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
364 | 371 | | |
365 | 372 | | |
366 | 373 | | |
| |||
740 | 747 | | |
741 | 748 | | |
742 | 749 | | |
| 750 | + | |
743 | 751 | | |
744 | 752 | | |
745 | 753 | | |
746 | 754 | | |
747 | 755 | | |
| 756 | + | |
748 | 757 | | |
| 758 | + | |
749 | 759 | | |
750 | 760 | | |
751 | 761 | | |
752 | 762 | | |
753 | 763 | | |
| 764 | + | |
754 | 765 | | |
| 766 | + | |
755 | 767 | | |
756 | 768 | | |
757 | 769 | | |
| |||
1601 | 1613 | | |
1602 | 1614 | | |
1603 | 1615 | | |
| 1616 | + | |
| 1617 | + | |
| 1618 | + | |
| 1619 | + | |
| 1620 | + | |
| 1621 | + | |
| 1622 | + | |
| 1623 | + | |
| 1624 | + | |
| 1625 | + | |
| 1626 | + | |
| 1627 | + | |
| 1628 | + | |
| 1629 | + | |
| 1630 | + | |
| 1631 | + | |
| 1632 | + | |
| 1633 | + | |
| 1634 | + | |
| 1635 | + | |
| 1636 | + | |
| 1637 | + | |
| 1638 | + | |
| 1639 | + | |
| 1640 | + | |
| 1641 | + | |
| 1642 | + | |
| 1643 | + | |
| 1644 | + | |
| 1645 | + | |
| 1646 | + | |
| 1647 | + | |
| 1648 | + | |
| 1649 | + | |
| 1650 | + | |
| 1651 | + | |
| 1652 | + | |
| 1653 | + | |
| 1654 | + | |
| 1655 | + | |
| 1656 | + | |
| 1657 | + | |
| 1658 | + | |
| 1659 | + | |
| 1660 | + | |
| 1661 | + | |
| 1662 | + | |
| 1663 | + | |
| 1664 | + | |
| 1665 | + | |
| 1666 | + | |
| 1667 | + | |
| 1668 | + | |
| 1669 | + | |
| 1670 | + | |
| 1671 | + | |
| 1672 | + | |
| 1673 | + | |
| 1674 | + | |
| 1675 | + | |
| 1676 | + | |
| 1677 | + | |
| 1678 | + | |
| 1679 | + | |
| 1680 | + | |
| 1681 | + | |
| 1682 | + | |
| 1683 | + | |
| 1684 | + | |
| 1685 | + | |
1604 | 1686 | | |
1605 | 1687 | | |
1606 | 1688 | | |
| |||
2587 | 2669 | | |
2588 | 2670 | | |
2589 | 2671 | | |
| 2672 | + | |
| 2673 | + | |
2590 | 2674 | | |
2591 | 2675 | | |
2592 | 2676 | | |
| |||
2632 | 2716 | | |
2633 | 2717 | | |
2634 | 2718 | | |
| 2719 | + | |
| 2720 | + | |
| 2721 | + | |
| 2722 | + | |
| 2723 | + | |
| 2724 | + | |
| 2725 | + | |
2635 | 2726 | | |
2636 | 2727 | | |
2637 | 2728 | | |
| |||
2779 | 2870 | | |
2780 | 2871 | | |
2781 | 2872 | | |
| 2873 | + | |
2782 | 2874 | | |
2783 | 2875 | | |
2784 | 2876 | | |
| |||
3696 | 3788 | | |
3697 | 3789 | | |
3698 | 3790 | | |
| 3791 | + | |
| 3792 | + | |
| 3793 | + | |
| 3794 | + | |
| 3795 | + | |
| 3796 | + | |
| 3797 | + | |
| 3798 | + | |
| 3799 | + | |
| 3800 | + | |
| 3801 | + | |
| 3802 | + | |
| 3803 | + | |
| 3804 | + | |
| 3805 | + | |
| 3806 | + | |
| 3807 | + | |
| 3808 | + | |
| 3809 | + | |
| 3810 | + | |
| 3811 | + | |
| 3812 | + | |
| 3813 | + | |
| 3814 | + | |
| 3815 | + | |
| 3816 | + | |
| 3817 | + | |
| 3818 | + | |
3699 | 3819 | | |
3700 | 3820 | | |
3701 | 3821 | | |
| |||
0 commit comments