Skip to content

Commit a62389c

Browse files
committed
fix(pm): read a present-but-unreadable Seat: declaration as null, never 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>
1 parent 5a5e710 commit a62389c

1 file changed

Lines changed: 49 additions & 14 deletions

File tree

‎scripts/pm/check-half-states.mjs‎

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9194,22 +9194,48 @@ export function seatPostLastEventMs(seat, commentRows) {
91949194
* The `Seat:` line of a claim body — the seat NUMBER the claiming PM sits on
91959195
* (`Seat: domain:<x>#<n>`, free-claim multi-seat), read with the same key-line
91969196
* tolerance as `threadReadField` (leading bullet or blockquote, bold or code
9197-
* decoration around the key).
9198-
*
9199-
* Absent ⇒ 1. Every claim written before the line existed belongs to seat 1,
9200-
* which is the only seat those lanes had, so the default is backward
9201-
* compatible with every existing claim rather than a guess. A line that is
9202-
* PRESENT but names no `#<n>` is unreadable and returns null: it matches no
9203-
* seat, so its claim is invisible to the seat filter below — the file's
9204-
* under-reporting direction on every unrecognised spelling — and ⛔ never
9205-
* silently read as seat 1.
9197+
* decoration around the key). Three answers, and the middle one is the reason
9198+
* this reader is not a one-liner:
9199+
*
9200+
* · ABSENT ⇒ 1. Every claim written before the line existed belongs to seat 1,
9201+
* which is the only seat those lanes had, so the default is backward
9202+
* compatible with every existing claim rather than a guess.
9203+
* · PRESENT but unreadable ⇒ null. It matches no seat, so the claim is
9204+
* invisible to the seat filter below — the file's under-reporting direction
9205+
* on every unrecognised spelling — and ⛔ never silently read as seat 1.
9206+
* TWO spellings reach it: a line-initial key naming no `#<n>`, and a
9207+
* declaration the key line cannot see at all (mid-sentence, inside a code
9208+
* span, inside a subscript tag, behind a `> - ` prefix).
9209+
* · Otherwise the number the line names.
9210+
*
9211+
* ⚠️ The second spelling of the second answer is the repair, and the cost it
9212+
* removes is measured: a wrong answer that is a REAL seat does not surface as
9213+
* an error, it surfaces as an ACCUSATION against whoever sits there — three
9214+
* claims of one round declared their seat inside the opening sentence, were
9215+
* read as seat 1, and H38 rowed another seat's post as stale over it. ⛔ The
9216+
* presence probe below therefore does NOT widen `CLAIM_SEAT_KEY_LINE`:
9217+
* accepting a malformed declaration would leave the writer no way to find out
9218+
* either, which is the same defect wearing the other costume. Refusing it at
9219+
* the writing path is a different instrument's job.
92069220
*/
92079221
const CLAIM_SEAT_KEY_LINE = /^[ \t]*(?:[-*+][ \t]+)?>?[ \t]*(?:\*\*)?`?Seat`?(?:\*\*)?[ \t]*:[ \t]*(.*)$/im;
92089222

9209-
/** @returns {number|null} the seat number, 1 when the line is absent, null when it is present but unreadable. */
9223+
/**
9224+
* PRESENCE only — consulted solely when the key line matched nothing, and it
9225+
* ⛔ never yields a number. Case-SENSITIVE, and it demands the whole declared
9226+
* payload (`domain:<lane>#<n>`), because off the line start there is no
9227+
* position left to tell a declaration from prose: the pinned control
9228+
* 「the seat: domain:services#2 is busy」 has to keep reading as seat 1. Pinned
9229+
* to the measured shape; an unmeasured variant it misses falls back to 1,
9230+
* which is the defect, so widen it on evidence rather than narrowing it.
9231+
*/
9232+
const CLAIM_SEAT_DECLARATION_ANYWHERE = /(?:\*\*)?`?Seat`?(?:\*\*)?[ \t]*:[ \t]*`?domain:[a-z0-9_-]+[ \t]*#\d/;
9233+
9234+
/** @returns {number|null} the seat number, 1 when NO declaration is present at all, null when one is present but unreadable. */
92109235
export function claimSeatNumber(body) {
9211-
const m = CLAIM_SEAT_KEY_LINE.exec(String(body ?? ''));
9212-
if (!m) return 1;
9236+
const text = String(body ?? '');
9237+
const m = CLAIM_SEAT_KEY_LINE.exec(text);
9238+
if (!m) return CLAIM_SEAT_DECLARATION_ANYWHERE.test(text) ? null : 1;
92139239
const n = /#(\d+)\b/.exec(String(m[1] ?? ''));
92149240
return n ? Number(n[1]) : null;
92159241
}
@@ -31527,10 +31553,16 @@ async function selfTest() {
3152731553
t('H38 seat: `Seat: domain:services#2` reads 2', claimSeatNumber('Claim: x\nSeat: domain:services#2'), 2);
3152831554
t('H38 seat: …backticked too', claimSeatNumber('Claim: x\nSeat: `domain:services#2`'), 2);
3152931555
t('H38 seat: …and behind a bullet then a blockquote, bold key', claimSeatNumber('- > **Seat**: domain:services#4'), 4);
31530-
// ⛔ Blockquote-then-bullet is out, exactly as `claimedBranches` pins for `Branch:` — invisible, so absent, so seat 1.
31531-
t('H38 seat: a `> - Seat:` line is not read (measured shape, same as `Branch:`)', claimSeatNumber('> - Seat: domain:services#4'), 1);
31556+
// ⛔ Blockquote-then-bullet is still out, exactly as `claimedBranches` pins for `Branch:` — but it is PRESENT, so unreadable, ⛔ not seat 1.
31557+
t('H38 seat: a `> - Seat:` line is still no line-initial match — now unreadable, ⛔ not seat 1', claimSeatNumber('> - Seat: domain:services#4'), null);
3153231558
t('H38 seat: a `Seat:` line naming no number is unreadable, not seat 1', claimSeatNumber('Claim: x\nSeat: domain:services'), null);
3153331559
t('H38 seat: prose mentioning a seat is not a `Seat:` line', claimSeatNumber('the seat: domain:services#2 is busy'), 1);
31560+
// The measured misread: the declaration carried somewhere the key line cannot see is PRESENT, so null — ⛔ never a real seat.
31561+
t('H38 seat: a declaration mid-sentence in a code span is unreadable, ⛔ not seat 1', claimSeatNumber('**Claim:** dispatched by the `domain:spec` execution seat. `Seat: domain:spec#3` · claimed 2026-09-18T19:31Z'), null);
31562+
t('H38 seat: …and the same declaration inside a subscript tag', claimSeatNumber('<sub>Seat: domain:spec#3</sub>'), null);
31563+
// The two controls that keep the probe from swallowing the default: prose (above) needs the capital key, and a lane token alone is not a declaration.
31564+
t('H38 seat: a lane token with no `Seat:` key is not a declaration, so the absent default holds', claimSeatNumber('Claim: x\nre-graded for `domain:spec#3` work'), 1);
31565+
t('H38 seat: a body with no declaration at all is still seat 1 — every pre-line claim keeps its default', claimSeatNumber('Claim: x\nSession: `session_y`\nWorktree: `objectstack-issue-1`'), 1);
3153431566
const seatRow = (seatLine, iso) => [{ body: `Claim: PM loop round 1\nSession: \`session_x\`\n${seatLine}`, created_at: iso }];
3153531567
const oneCard = [laneCard(1, ['pm:dispatched', 'domain:services'])];
3153631568
const seat2Claim = new Map([[1, seatRow('Seat: `domain:services#2`', '2026-08-30T00:00:00Z')]]);
@@ -31541,6 +31573,9 @@ async function selfTest() {
3154131573
t('H38 seat: …and the seat 2 post does not', newestLaneClaim('domain:services', oneCard, seat1Claim, 2), null);
3154231574
t('H38 seat: the default seat argument is 1, so every pre-existing call reads as before', newestLaneClaim('domain:services', laneIssues, laneComments).number, 13398);
3154331575
t('H38 seat: an unreadable `Seat:` line matches no seat', newestLaneClaim('domain:services', oneCard, new Map([[1, seatRow('Seat: domain:services', '2026-08-30T00:00:00Z')]]), 1), null);
31576+
const midLineClaim = new Map([[1, [{ body: '**Claim:** by the `domain:services` seat. `Seat: domain:services#3` · claimed', created_at: '2026-08-30T00:00:00Z' }]]]);
31577+
t('H38 seat: a mid-line declaration is invisible to the seat it names…', newestLaneClaim('domain:services', oneCard, midLineClaim, 3), null);
31578+
t('H38 seat: …and — the whole point — invisible to seat 1 too, so no post is rowed over it', newestLaneClaim('domain:services', oneCard, midLineClaim, 1), null);
3154431579
t('H38 seat: the newest SAME-seat claim wins over a newer other-seat one', newestLaneClaim('domain:services', oneCard, new Map([[1, [...seatRow('Seat: domain:services#1', '2026-08-29T00:00:00Z'), ...seatRow('Seat: domain:services#2', '2026-08-30T00:00:00Z')]]]), 1).at, at38('2026-08-29T00:00:00Z'));
3154531580
// End to end on the post: a seat-2 post is judged against the claim handed to it, and names its seat.
3154631581
t('H38 seat: a seat-2 post behind a same-seat claim -> finding', typeof h38SeatPostStale(seat38(seat2Title), SEAT_AT_38, claim38(13398, '2026-08-30T07:30:00Z')), 'string');

0 commit comments

Comments
 (0)