Repository navigation
fix(pm): a present-but-unreadable Seat: declaration reads as null, never as seat 1 - #19423
Conversation
…ever as seat 1 `claimSeatNumber` returned 1 whenever the line-initial key regex found nothing, whatever else the body carried. A claim declaring its seat mid-sentence — the same text, inside a code span, inside a subscript tag, or behind a `> - ` prefix — therefore read as a REAL seat, so the misread never surfaced as an error: it surfaced as an accusation, with H38 rowing another seat's post as stale over three claims of one round. A presence probe now runs only where the key line matched nothing: a body that carries the whole declared payload (`Seat:` then `domain:<lane>#<n>`) somewhere the key line cannot see is PRESENT-but-unreadable and returns null, matching no seat and staying invisible to the seat filter — the file's documented under-reporting direction. A body with no declaration at all still returns 1, which is what keeps every claim written before the line existed attributed to the only seat those lanes had. The probe deliberately does NOT widen `CLAIM_SEAT_KEY_LINE`: accepting a malformed declaration would leave the writer no way to find out either. It is case-sensitive and demands the lane-and-number payload so the pinned prose control keeps reading as seat 1. 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 |
…writes the comment (objectstack-ai#19435) Fixes objectstack-ai#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 objectstack-ai#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 — objectstack-ai#13914's control case is «the PM claim comment on objectstack-ai#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 (objectstack-ai#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>
Fixes #19108
Clause-②: no
claimSeatNumberinscripts/pm/check-half-states.mjsreturned 1 whenever its line-initial key regex found nothing, whatever else the body carried. A claim that declares its seat anywhere the key line cannot see — mid-sentence, inside a code span, inside a subscript tag, behind a> -prefix — therefore read as a real seat, so the misread never surfaced as an error: it surfaced as an accusation, and H38 rowed a different seat's post as stale over it.A presence probe now runs only where the key line matched nothing. A body carrying the whole declared payload (
Seat:thendomain:LANE#N) somewhere the key line cannot see is PRESENT-but-unreadable and returnsnull— matching no seat, invisible to the seat filter, which is this file's documented under-reporting direction. A body with no declaration at all still returns1, and the docblock now says why: that default is what keeps every claim written before the line existed attributed to the only seat those lanes had.⛔ The probe deliberately does not widen
CLAIM_SEAT_KEY_LINE. Accepting a malformed declaration would leave the writer no way to find out either — the same defect wearing the other costume — and the file already carries a maintainer ruling that the literalClaim:marker must not be widened. Refusing a malformed declaration at the writing path is a different instrument's job: #19152 is not addressed here.Before / after — the card's measured table, module imported directly
Stored comment bodies fetched through the REST proxy; controls verbatim from the card.
reprobefore the fix on5a5e710, after ona62389c.Seat:lines that the 2026-09-18 repair edit added)nullnull- **Seat**: …)null> - Seat:linenullnullnullSeat:lines the repair added and leaves the opening sentence's inline code span untouched — that sentence is unchanged prose in all three stored bodies, so what is measured is the original declaration shape, not a synthetic one. The card's other population — 「this seat's eight earlier claims of the same shift」 — was NOT MEASURED: enumerating them needs a board scan outside this run's read channel. The three as-stored bodies carry exactly that shape (line-initialSeat: domain:spec#3) and are measured instead, in the same direction.The pin that read
asserted 1. It now asserts
null, and the case name and its comment say so: blockquote-then-bullet is still no line-initial match, but the declaration is present, so the honest answer isnull. That row was itself an instance of the defect this card names — a present declaration silently answered with a real seat — so leaving it at 1 would have fixed the reported spelling and kept a measured sibling of it.PM mechanism assumptions — each verified, two corrected
claimSeatNumberis the only reader of theSeat:line.git grep 'claimSeatNumber'over the tree: 9 occurrences, all inscripts/pm/check-half-states.mjs(1 definition, 1 production caller, 7 self-test pins). Its only caller isnewestLaneClaim(:9257), whose only production call site is the H38 loop (:23996); the two self-test call blocks are the rest. A tree-widegit grep 'Seat'overscripts/and.github/names no second reader —.claude/skills/pm-dispatch/SKILL.mdcarries the template text, not a reader.seatLanereads the seat number off the post title, a different key.Re-read through the fix ⇒⚠️ Refuted: there is no existing H38 /
null, and the line-initial form still reads 3. See the measurement limit declared above on the eight-claim leg.3. ✅ Decided: a
nullseat is simply filtered out — no new H38 sentence. Reasoning below.4.
claimSeatNumberbattery to put cases in. The roster (SELF_TEST_BATTERIES, 8 entries) declares itself PARTIAL in its own header — 「the several hundredt(...)cases above and below are UNBATTERIED and stay that way here」 — and everyclaimSeatNumberpin is an unbatteriedt(...). So the new cases are unbatteriedt(...)beside them,SELF_TEST_BATTERY_FLOORstays 8, and no battery was opened. Every one of the eight batteries still registers above its pin.The H38 decision, on the four axes
The grading allowed either 「
nullor a named row」. Chosen:null, filtered out — H38 gains no sentence.foreignreasoning, quoted inh38SeatPostStale). Withnullthe claim contributes nothing and H38 declines by construction — the card's own shape 3, reached without new prose.Why the probe is case-sensitive and demands the whole payload
Off the line start there is no position left to tell a declaration from prose. The file pins 「the seat: domain:services#2 is busy」 ⇒ 1 as a deliberate control; a case-insensitive bare-key probe would flip it to
nulland start finding declarations in prose. So the probe requires a capitalSeat, a colon, and the lane-and-number payloaddomain:LANE#N. It is pinned to the measured shape, and its docblock says which direction to move it: a variant it misses falls back to 1, which is the defect, so widen on evidence — ⛔ never narrow it.Tests
node scripts/pm/check-half-states.mjs --self-test→ exit 0,5124 cases pass(base5a5e710:5118 cases pass⇒ the 6 new cases are counted; one existing case changed its expectation). All 8 batteries above their pins: 183/172 · 37/34 · 46/42 · 152/142 · 213/184 · 55/52 · 11/10 · 26/24.scripts/ablation-replace.mjsso the mutation is proven on disk: replacing the one changed production line with its oldif (!m) return 1;⇒anchor 1 -> 0, blob 9a8ba4b32ba3 -> 665c53ffe8ee, self-test exit 1,4 of 5124 case(s) failed— the mid-sentence case, the subscript-tag case, the flipped> - Seat:pin and the seat-1 leg of the end-to-end pair. Restore verified by the tool:blob == HEAD (9a8ba4b32ba3)andgit diff HEADempty. The other 3 new cases are controls that must stay green under that ablation (prose ⇒ 1, a lane token with no key ⇒ 1, no declaration ⇒ 1), and they did.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackata62389c(no paths passed — the script derives the change set itself, three-dot vs merge base5a5e710): 41 families, every one run with its exit captured before any pipe, 41/41 exit 0. Reconciled with--ran: 「41 derived, 41 run, 0 NOT-MEASURED, 0 UNRUN」.check:pm-label-desc-cap,check:pm-governed-prose,check-published-list-mirrors,check:pm-settings-deny-roster,check:select-gate-families— all exit 0.node scripts/pm/check-governed-merges.mjs⇒ NOT governed (scripts/pm/**is not on the register, 0 of 1 path hit it) — ordinary queue landing.skip-changeset:scripts/pm/**publishes nothing from any released package.Acceptance notes
Off-path observations, ⛔ not filed and ⛔ not fixed here (os-dev.md rule 3):
noted, not filed:the file's own H38 self-test fixtures are unbatteried, so the roster's eight batteries do not floor this row family at all; opening one is a diff of its own and the roster header already declares that gap deliberately. 承接者: the next card that adds an H38 row.noted, not filed:the sibling boards were never measured for the same misread — the card's own evidence limit. 承接者: none today; it needs a cross-repo sweep nobody is holding.Generated by Claude Code