Skip to content

Commit 498e8f4

Browse files
fix(pm): one reading of the 席位核对 record for fleet-roster and H69 (#26635)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9WXrD8j89X92edJ7iXHVEhk)_ Fixes #26335 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:车队总表(fleet-roster)判断 PR 是否带落地结论时,和 H69 巡查判断有没有席位记录时,现在读同一条 `## 席位核对` 规则;标题行内写 `— ACCEPT` 的记录也算 ACCEPT。 - 为什么改:席位实际发的记录是 `## 席位核对 — ACCEPT · …`,H69 认得,总表却认不得,导致总表把已 ACCEPT 的 PR 报成"无结论"。 - 风险与回滚:只动 PM 工具脚本、不发布任何包;总表与 H69 都是只报告不拦截。回滚 = revert 本 PR。 - 先看:`scripts/pm/seat-record.mjs`(新的共享规则),`scripts/pm/fleet-roster.mjs` 的 `hasLandingVerdict`,`scripts/pm/check-half-states.mjs` 的 `SEAT_RECORD_MARKER` 导入处。 - 两分钟验证:`node scripts/pm/fleet-roster.mjs --self-test && node scripts/pm/check-half-states.mjs --self-test`,预期两行 ✓(78 例 / 5034 例全过)。 - 需要你做的:无,席位落地。 ## What changes - New `scripts/pm/seat-record.mjs`, no imports and no I/O: `SEAT_RECORD_MARKER` (moved verbatim from `check-half-states.mjs`), `SEAT_RECORD_VERDICTS` (`ACCEPT`, `REWORK`, `REJECT`), `seatRecordOf(body)` and `seatRecordAccepts(body)`. The verdict is read inline on the heading line first (`## 席位核对 — ACCEPT · …`), else from the first line below the heading that opens with a verdict word (`**ACCEPT** · …`). Only `ACCEPT` lands a pull. - `fleet-roster.mjs` `hasLandingVerdict`: the 席位核对 arm is now `seatRecordAccepts(body)`. The `## Contract review` PASS arm is unchanged. - `check-half-states.mjs`: `SEAT_RECORD_MARKER` is imported from the shared module and re-exported under its old name. H69's reading is byte-identical: the same regex object source, still read through `latestMarkedComment`. - The module is a sibling of its two readers, not under `scripts/pm/lib/`. A multi-segment import specifier counts as a dispatch-gates watch hint, and gate files that import one lose the hint (measured on an earlier card). The card's hypothesis allowed either place. Behaviour changes in the roster read, all intended: 1. The inline `— ACCEPT` heading counts (the defect). 2. A blockquoted record (`> ## 席位核对`) counts, as H69 already read it. 3. The verdict must sit on the heading or below it. A line-initial `ACCEPT` above the heading no longer counts, and an inline `REWORK` is never overridden by a later `ACCEPT` line. ## Tests - `fleet-roster.mjs --self-test`: 5 new cases (inline ACCEPT, inline REWORK, blockquoted record, ACCEPT above the heading, and the three measured bodies read through the imported marker). 73 → 78 cases, 8 batteries. - `check-half-states.mjs --self-test`: 4 new cases in the H69 battery (the marker reads the inline heading; H69 is clean on an inline record newer than the head; the three measured bodies read record/ACCEPT as `true/true`, `true/true`, `false/false`; inline REWORK and REJECT below are records that never accept). H69 66 → 70 cases (floor 40), 5030 → 5034 total. - No forced test edits: every existing case passes unchanged. ## Acceptance notes - The card asks that "the two recognisers answer the same on the three measured bodies". They ask different questions: H69 asks whether a record is there, and the roster asks whether the pull may land. So the shared question, the seat record and its ACCEPT, is pinned in both self-tests. On the `## Contract review` PASS body both read "no seat record". The roster still answers `true` there through its separate Contract review arm, which is correct. - H69 reads the marker through `markerMatches`, which also undecorates bold or backtick-wrapped lines. The roster tests the raw body. A decorated heading such as `**## 席位核对**` would still read differently. No live record has that shape. It is noted here and not widened, because `markerMatches` lives inside `check-half-states.mjs`. - No pm-dispatch skill text changed: `.claude/skills/pm-dispatch/SKILL.md` is held by another card, so this PR is not on a governed surface. Whether the skill needs a sentence stating the heading spelling is listed in the report's open questions. ## Verification All readings on head `f9a39c24` (the last commit), local. <details><summary>Self-tests and reverse verification</summary> - `node scripts/pm/fleet-roster.mjs --self-test`: `✓ fleet-roster self-test: 78 cases in 8 batteries pass.` (main `7c34e5dc`: 73 cases) - `node scripts/pm/check-half-states.mjs --self-test`: `✓ check-half-states self-test: 5034 cases pass.`, H69 battery `70/40` (main: 5030, `66/40`) - Reverse verification, after the fix was committed: with the old bare-heading regex put back in `hasLandingVerdict` on disk, the roster self-test exits 1 with 4 failures (inline ACCEPT, blockquoted record, ACCEPT above the heading, the three measured bodies). Restored with `git checkout HEAD --`; `git diff HEAD` is empty, and the self-test is green again. </details> <details><summary>dispatch-gates: 47 derived, 47 run, all exit 0</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 47 commands for the three paths. Every one exited 0, captured before any pipe. `--ran` with `COMMAND :: exit CODE` lines: `Run reconciliation — 47 derived, 47 run, 0 NOT-MEASURED, 0 UNRUN.` and `47 of 47 recorded an exit code and none of them is 3`. Among them: `pnpm check:pm-dispatch-gates`, `pnpm check:pm-half-states`, `pnpm check:self-test-wired`, `pnpm check:watch-hint-literal`, `pnpm check:scripts-symbol-anchors`, `pnpm check:nul-bytes`. Families outside the derived total are CI's. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmPquMegy4jNfKiBsAf)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent dbf7e04 commit 498e8f4

3 files changed

Lines changed: 105 additions & 3 deletions

File tree

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,9 @@ import { execFileSync, spawnSync } from 'node:child_process';
983983
import { readFileSync, writeSync } from 'node:fs';
984984
import { fileURLToPath } from 'node:url';
985985
import { isEntrypoint } from '../invoked-as.mjs';
986+
// The seat record's heading and verdict have ONE reading, shared with
987+
// fleet-roster's landing-verdict read; H69 asks it, never re-spells it.
988+
import { SEAT_RECORD_MARKER, seatRecordAccepts, seatRecordOf } from './seat-record.mjs';
986989

987990
/**
988991
* The repo this file sweeps when nothing says otherwise. It is a FALLBACK for a
@@ -11401,8 +11404,10 @@ export function h48GovernedVerdictWithoutBrief(pr, governed, commentRows) {
1140111404
* mirrored properties on the literal `## 席位核对` heading: line-anchored, an
1140211405
* optional leading blockquote, no `g` flag. A `###` sub-heading or a mention
1140311406
* in prose (the landing comment is told ⛔ 不复述 `## 席位核对`) is no record.
11407+
* DEFINED in `seat-record.mjs`, the one reading fleet-roster's landing-verdict
11408+
* read shares, and re-exported here under its old name.
1140411409
*/
11405-
export const SEAT_RECORD_MARKER = /^\s*>?\s*## 席位核对/m;
11410+
export { SEAT_RECORD_MARKER };
1140611411

1140711412
/** Governed globs named inline before the sentence degrades to a count — H43's. */
1140811413
export const H69_SURFACE_LIST_CAP = 4;
@@ -32331,6 +32336,14 @@ Doubles as the fire's **write self-check** (step 0). \`201\` is not the reading.
3233132336
b(BATTERY69, 'record marker: ⛔ a landing comment that cites the record (⛔ 不复述 `## 席位核对`) is not one', SEAT_RECORD_MARKER.test('落地评论 = 两个读数的探针表,⛔ 不复述 `## 席位核对`'), false);
3233232337
b(BATTERY69, 'record marker: a verdict-bearing brief comment is not the record', SEAT_RECORD_MARKER.test('## 维护者速读\n\n**ACCEPT** — lands as ruled'), false);
3233332338
b(BATTERY69, 'record marker: no `g` flag, so two reads of one body agree', [SEAT_RECORD_MARKER.test(RECORD69), SEAT_RECORD_MARKER.test(RECORD69)].join(','), 'true,true');
32339+
// The live inline spelling: the verdict rides the heading line itself.
32340+
const INLINE69 = '## 席位核对 — ACCEPT · domain:skills#2 · session_01x · 2026-10-26T12:30Z\n\n1. 范围 ✓';
32341+
const BARE69 = '## 席位核对\n\n**ACCEPT** · domain:skills seat 2 · head `7cc88889`';
32342+
const CONTRACT69 = '## Contract review\n\nServed-tier: x\n\n**VERDICT: PASS**';
32343+
b(BATTERY69, 'record marker: the inline `— ACCEPT` heading is a record', SEAT_RECORD_MARKER.test(INLINE69), true);
32344+
b(BATTERY69, 'H69: an inline `— ACCEPT` record NEWER than the head -> clean', h69(pr69(), [cm69(INLINE69, NEWER69)], HEAD69), null);
32345+
b(BATTERY69, 'shared reading: the three measured bodies read record + ACCEPT alike (bare, inline, Contract review)', [BARE69, INLINE69, CONTRACT69].map((x) => `${SEAT_RECORD_MARKER.test(x)}/${seatRecordAccepts(x)}`).join(), 'true/true,true/true,false/false');
32346+
b(BATTERY69, 'shared reading: ⛔ an inline REWORK or a REJECT below is a record that never accepts', [seatRecordOf('## 席位核对 — REWORK · 补测试'), seatRecordOf('## 席位核对\n\nREJECT · rework')].map((r) => r?.verdict).join(), 'REWORK,REJECT');
3233432347

3233532348
// The sentence.
3233632349
b(BATTERY69, 'H69 row: the remedy is the record as a COMMENT for the current head', h69row(pr69(), []).includes('post the `## 席位核对` record as a COMMENT for head `7cc8888`'), true);

‎scripts/pm/fleet-roster.mjs‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
* awaiting landing open pulls on a `claude/` head that close an in-flight
4444
* card of the seat (`Fixes|Closes|Resolves #n`) and carry a
4545
* review verdict — a `## 席位核对` comment whose verdict
46-
* line is ACCEPT, or a `## Contract review` with
46+
* is ACCEPT, inline on the heading or on a line below it
47+
* (`seat-record.mjs`), or a `## Contract review` with
4748
* `VERDICT: PASS`. ⛔ `pm:mailbox` pulls never count
4849
* (`LEDGER_EXCLUSION`).
4950
* wall note a line-initial `Wall:` (or `撞墙:`) line in the post
@@ -91,6 +92,10 @@ import { ghApi, PER_PAGE, readAllPages } from './gh-pages.mjs';
9192
// the mailbox opener (#25417); this file reads them, never re-spells them.
9293
import * as mailboxOpen from './mailbox-open.mjs';
9394
import { LEDGER_EXCLUSION, MAILBOX_BRANCH_PREFIX, MAILBOX_LABEL } from './mailbox-open.mjs';
95+
// The seat record's heading and verdict have ONE reading, shared with H69 in
96+
// check-half-states; this file asks it, never re-spells the heading.
97+
import * as seatRecord from './seat-record.mjs';
98+
import { seatRecordAccepts } from './seat-record.mjs';
9499

95100
const SELF_PATH = fileURLToPath(import.meta.url);
96101
const PM_DIR = dirname(SELF_PATH);
@@ -222,7 +227,7 @@ export function closingRefs(body, repo) {
222227
export function hasLandingVerdict(comments) {
223228
return (Array.isArray(comments) ? comments : []).some((c) => {
224229
const body = String(c?.body ?? '');
225-
if (/^##[ \t]*席位核对[ \t]*$/m.test(body) && /^[ \t]*(?:\*\*)?ACCEPT(?:\*\*)?\b/m.test(body)) return true;
230+
if (seatRecordAccepts(body)) return true;
226231
return /^##[ \t]*Contract review\b/m.test(body) && /^[ \t]*(?:\*\*)?VERDICT:[ \t]*PASS\b/im.test(body);
227232
});
228233
}
@@ -590,6 +595,11 @@ async function selfTest() {
590595
t('席位核对 with REJECT is not', hasLandingVerdict([{ body: '## 席位核对\n\nREJECT · rework' }]), false);
591596
t('Contract review VERDICT: PASS is a verdict', hasLandingVerdict([{ body: '## Contract review\n\nServed-tier: x\n\n**VERDICT: PASS**' }]), true);
592597
t('an ACCEPT outside a seat record is not', hasLandingVerdict([{ body: 'ACCEPT this please' }]), false);
598+
t('the live inline heading `## 席位核对 — ACCEPT · …` is a verdict', hasLandingVerdict([{ body: '## 席位核对 — ACCEPT · domain:skills#2 · session_01x · 2026-10-26T12:30Z\n\n1. 范围 ✓' }]), true);
599+
t('⛔ an inline REWORK heading is not, whatever a later line opens with', hasLandingVerdict([{ body: '## 席位核对 — REWORK · 补测试\n\nACCEPT once the test lands' }]), false);
600+
t('a blockquoted record is read as H69 reads it', hasLandingVerdict([{ body: '> ## 席位核对\n>\n> **ACCEPT** · seat 1' }]), true);
601+
t('⛔ an ACCEPT line ABOVE the heading is not the record\'s verdict', hasLandingVerdict([{ body: 'ACCEPT\n\n## 席位核对\n\nREWORK · rework' }]), false);
602+
t('the heading is H69\'s marker, imported not re-spelled: the three measured bodies read record alike', ['## 席位核对\n\n**ACCEPT** · x', '## 席位核对 — ACCEPT · x', '## Contract review\n\n**VERDICT: PASS**'].map((b) => `${seatRecord.SEAT_RECORD_MARKER.test(b)}/${hasLandingVerdict([{ body: b }])}`), ['true/true', 'true/true', 'false/true']);
593603
t('the mailbox constants are the opener\'s own, imported not re-spelled', [MAILBOX_LABEL, LEDGER_EXCLUSION, MAILBOX_BRANCH_PREFIX], [mailboxOpen.MAILBOX_LABEL, mailboxOpen.LEDGER_EXCLUSION, mailboxOpen.MAILBOX_BRANCH_PREFIX]);
594604
t('a mailbox is read by head, then by body line; no label ⇒ none', [mailboxLogin({ labels: ['pm:mailbox'], head: { ref: 'mailbox/os-bill' } }), mailboxLogin({ labels: ['pm:mailbox'], head: { ref: 'claude/x' }, body: 'Mailbox-of: os-sales\n' }), mailboxLogin({ labels: [], head: { ref: 'mailbox/os-bill' } })], ['os-bill', 'os-sales', null]);
595605

‎scripts/pm/seat-record.mjs‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* seat-record — the ONE reading of the seat's `## 席位核对` record on a pull
5+
* request: where the record is, and which verdict it carries.
6+
*
7+
* ## Why it is its own file
8+
*
9+
* Two tools read this one artefact. `check-half-states.mjs` (H69) asks whether
10+
* a record is on the thread at all; `fleet-roster.mjs` asks whether the record
11+
* lets the pull land. Each used to carry its own pattern for the heading, and
12+
* they read the live inline spelling `## 席位核对 — ACCEPT · …` differently: H69
13+
* saw a record, the roster saw no verdict. Neither tool imports the other, so
14+
* the heading and the verdict reading live here and both import them. ⛔ Never
15+
* re-spell either pattern in a reader; a new spelling is added here, with a
16+
* case in BOTH self-tests.
17+
*
18+
* It is a sibling of its two readers, ⛔ not under a `lib/` subdirectory: a
19+
* multi-segment import specifier counts as a dispatch-gates watch hint, and a
20+
* module that inherits one from its gate files loses it when they import here.
21+
*
22+
* ## The record, as it is written
23+
*
24+
* The heading opens a line, optionally inside a blockquote, as `## 席位核对`
25+
* (pm-dispatch SKILL.md ①, `references/execution-duties.md`). The verdict is
26+
* read in this order, and the FIRST verdict word met decides:
27+
*
28+
* inline on the heading line itself, after a separator:
29+
* `## 席位核对 — ACCEPT · domain:skills#2 · …`
30+
* below the first line under the heading that opens with a verdict word,
31+
* bold or bare: `**ACCEPT** · …`, `REWORK · …`
32+
*
33+
* `ACCEPT` is the only verdict that lets a pull land; `REWORK` and `REJECT`
34+
* never do, and a record whose verdict cannot be read carries `null`.
35+
*
36+
* ⛔ No network, no I/O, no imports: a body in, a reading out.
37+
*/
38+
39+
/**
40+
* The seat's record, as a heading line — line-anchored, an optional leading
41+
* blockquote, no `g` flag (so two reads of one body agree). A `###`
42+
* sub-heading or a mention in prose (the landing comment is told ⛔ 不复述
43+
* `## 席位核对`) is no record.
44+
*/
45+
export const SEAT_RECORD_MARKER = /^\s*>?\s*## 席位核对/m;
46+
47+
/** The verdict words a record can carry; only the first lands a pull. */
48+
export const SEAT_RECORD_VERDICTS = Object.freeze(['ACCEPT', 'REWORK', 'REJECT']);
49+
50+
const VERDICT_WORD = SEAT_RECORD_VERDICTS.join('|');
51+
52+
/** The inline arm: the heading's own tail, after an optional separator. */
53+
const INLINE_VERDICT = new RegExp(`^[ \\t]*(?:[—–::·|-][ \\t]*)*(?:\\*\\*)?(${VERDICT_WORD})(?:\\*\\*)?\\b`, 'u');
54+
55+
/** The below arm: a line under the heading that opens with a verdict word. */
56+
const LINE_VERDICT = new RegExp(`^[ \\t]*>?[ \\t]*(?:\\*\\*)?(${VERDICT_WORD})(?:\\*\\*)?\\b`, 'u');
57+
58+
/**
59+
* The record a comment body carries: `null` when there is none, else
60+
* `{ verdict }` with the verdict word, or `null` when none could be read. Pure.
61+
*/
62+
export function seatRecordOf(body) {
63+
const text = String(body ?? '');
64+
const m = SEAT_RECORD_MARKER.exec(text);
65+
if (!m) return null;
66+
const [headingTail, ...below] = text.slice(m.index + m[0].length).split(/\r?\n/);
67+
const inline = INLINE_VERDICT.exec(headingTail);
68+
if (inline) return { verdict: inline[1] };
69+
for (const line of below) {
70+
const v = LINE_VERDICT.exec(line);
71+
if (v) return { verdict: v[1] };
72+
}
73+
return { verdict: null };
74+
}
75+
76+
/** Does this comment body carry a seat record whose verdict is ACCEPT? Pure. */
77+
export function seatRecordAccepts(body) {
78+
return seatRecordOf(body)?.verdict === 'ACCEPT';
79+
}

0 commit comments

Comments
 (0)