diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 35ca430bfee..99640972109 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -983,6 +983,9 @@ import { execFileSync, spawnSync } from 'node:child_process'; import { readFileSync, writeSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { isEntrypoint } from '../invoked-as.mjs'; +// The seat record's heading and verdict have ONE reading, shared with +// fleet-roster's landing-verdict read; H69 asks it, never re-spells it. +import { SEAT_RECORD_MARKER, seatRecordAccepts, seatRecordOf } from './seat-record.mjs'; /** * 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) { * mirrored properties on the literal `## 席位核对` heading: line-anchored, an * optional leading blockquote, no `g` flag. A `###` sub-heading or a mention * in prose (the landing comment is told ⛔ 不复述 `## 席位核对`) is no record. + * DEFINED in `seat-record.mjs`, the one reading fleet-roster's landing-verdict + * read shares, and re-exported here under its old name. */ -export const SEAT_RECORD_MARKER = /^\s*>?\s*## 席位核对/m; +export { SEAT_RECORD_MARKER }; /** Governed globs named inline before the sentence degrades to a count — H43's. */ 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. b(BATTERY69, 'record marker: ⛔ a landing comment that cites the record (⛔ 不复述 `## 席位核对`) is not one', SEAT_RECORD_MARKER.test('落地评论 = 两个读数的探针表,⛔ 不复述 `## 席位核对`'), false); b(BATTERY69, 'record marker: a verdict-bearing brief comment is not the record', SEAT_RECORD_MARKER.test('## 维护者速读\n\n**ACCEPT** — lands as ruled'), false); 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'); + // The live inline spelling: the verdict rides the heading line itself. + const INLINE69 = '## 席位核对 — ACCEPT · domain:skills#2 · session_01x · 2026-10-26T12:30Z\n\n1. 范围 ✓'; + const BARE69 = '## 席位核对\n\n**ACCEPT** · domain:skills seat 2 · head `7cc88889`'; + const CONTRACT69 = '## Contract review\n\nServed-tier: x\n\n**VERDICT: PASS**'; + b(BATTERY69, 'record marker: the inline `— ACCEPT` heading is a record', SEAT_RECORD_MARKER.test(INLINE69), true); + b(BATTERY69, 'H69: an inline `— ACCEPT` record NEWER than the head -> clean', h69(pr69(), [cm69(INLINE69, NEWER69)], HEAD69), null); + 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'); + 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'); // The sentence. 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); diff --git a/scripts/pm/fleet-roster.mjs b/scripts/pm/fleet-roster.mjs index 2c58b4c459f..db8d104f664 100644 --- a/scripts/pm/fleet-roster.mjs +++ b/scripts/pm/fleet-roster.mjs @@ -43,7 +43,8 @@ * awaiting landing open pulls on a `claude/` head that close an in-flight * card of the seat (`Fixes|Closes|Resolves #n`) and carry a * review verdict — a `## 席位核对` comment whose verdict - * line is ACCEPT, or a `## Contract review` with + * is ACCEPT, inline on the heading or on a line below it + * (`seat-record.mjs`), or a `## Contract review` with * `VERDICT: PASS`. ⛔ `pm:mailbox` pulls never count * (`LEDGER_EXCLUSION`). * wall note a line-initial `Wall:` (or `撞墙:`) line in the post @@ -91,6 +92,10 @@ import { ghApi, PER_PAGE, readAllPages } from './gh-pages.mjs'; // the mailbox opener (#25417); this file reads them, never re-spells them. import * as mailboxOpen from './mailbox-open.mjs'; import { LEDGER_EXCLUSION, MAILBOX_BRANCH_PREFIX, MAILBOX_LABEL } from './mailbox-open.mjs'; +// The seat record's heading and verdict have ONE reading, shared with H69 in +// check-half-states; this file asks it, never re-spells the heading. +import * as seatRecord from './seat-record.mjs'; +import { seatRecordAccepts } from './seat-record.mjs'; const SELF_PATH = fileURLToPath(import.meta.url); const PM_DIR = dirname(SELF_PATH); @@ -222,7 +227,7 @@ export function closingRefs(body, repo) { export function hasLandingVerdict(comments) { return (Array.isArray(comments) ? comments : []).some((c) => { const body = String(c?.body ?? ''); - if (/^##[ \t]*席位核对[ \t]*$/m.test(body) && /^[ \t]*(?:\*\*)?ACCEPT(?:\*\*)?\b/m.test(body)) return true; + if (seatRecordAccepts(body)) return true; return /^##[ \t]*Contract review\b/m.test(body) && /^[ \t]*(?:\*\*)?VERDICT:[ \t]*PASS\b/im.test(body); }); } @@ -590,6 +595,11 @@ async function selfTest() { t('席位核对 with REJECT is not', hasLandingVerdict([{ body: '## 席位核对\n\nREJECT · rework' }]), false); t('Contract review VERDICT: PASS is a verdict', hasLandingVerdict([{ body: '## Contract review\n\nServed-tier: x\n\n**VERDICT: PASS**' }]), true); t('an ACCEPT outside a seat record is not', hasLandingVerdict([{ body: 'ACCEPT this please' }]), false); + t('the live inline heading `## 席位核对 — ACCEPT · …` is a verdict', hasLandingVerdict([{ body: '## 席位核对 — ACCEPT · domain:skills#2 · session_01x · 2026-10-26T12:30Z\n\n1. 范围 ✓' }]), true); + t('⛔ an inline REWORK heading is not, whatever a later line opens with', hasLandingVerdict([{ body: '## 席位核对 — REWORK · 补测试\n\nACCEPT once the test lands' }]), false); + t('a blockquoted record is read as H69 reads it', hasLandingVerdict([{ body: '> ## 席位核对\n>\n> **ACCEPT** · seat 1' }]), true); + t('⛔ an ACCEPT line ABOVE the heading is not the record\'s verdict', hasLandingVerdict([{ body: 'ACCEPT\n\n## 席位核对\n\nREWORK · rework' }]), false); + 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']); 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]); 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]); diff --git a/scripts/pm/seat-record.mjs b/scripts/pm/seat-record.mjs new file mode 100644 index 00000000000..632783c00ec --- /dev/null +++ b/scripts/pm/seat-record.mjs @@ -0,0 +1,79 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * seat-record — the ONE reading of the seat's `## 席位核对` record on a pull + * request: where the record is, and which verdict it carries. + * + * ## Why it is its own file + * + * Two tools read this one artefact. `check-half-states.mjs` (H69) asks whether + * a record is on the thread at all; `fleet-roster.mjs` asks whether the record + * lets the pull land. Each used to carry its own pattern for the heading, and + * they read the live inline spelling `## 席位核对 — ACCEPT · …` differently: H69 + * saw a record, the roster saw no verdict. Neither tool imports the other, so + * the heading and the verdict reading live here and both import them. ⛔ Never + * re-spell either pattern in a reader; a new spelling is added here, with a + * case in BOTH self-tests. + * + * It is a sibling of its two readers, ⛔ not under a `lib/` subdirectory: a + * multi-segment import specifier counts as a dispatch-gates watch hint, and a + * module that inherits one from its gate files loses it when they import here. + * + * ## The record, as it is written + * + * The heading opens a line, optionally inside a blockquote, as `## 席位核对` + * (pm-dispatch SKILL.md ①, `references/execution-duties.md`). The verdict is + * read in this order, and the FIRST verdict word met decides: + * + * inline on the heading line itself, after a separator: + * `## 席位核对 — ACCEPT · domain:skills#2 · …` + * below the first line under the heading that opens with a verdict word, + * bold or bare: `**ACCEPT** · …`, `REWORK · …` + * + * `ACCEPT` is the only verdict that lets a pull land; `REWORK` and `REJECT` + * never do, and a record whose verdict cannot be read carries `null`. + * + * ⛔ No network, no I/O, no imports: a body in, a reading out. + */ + +/** + * The seat's record, as a heading line — line-anchored, an optional leading + * blockquote, no `g` flag (so two reads of one body agree). A `###` + * sub-heading or a mention in prose (the landing comment is told ⛔ 不复述 + * `## 席位核对`) is no record. + */ +export const SEAT_RECORD_MARKER = /^\s*>?\s*## 席位核对/m; + +/** The verdict words a record can carry; only the first lands a pull. */ +export const SEAT_RECORD_VERDICTS = Object.freeze(['ACCEPT', 'REWORK', 'REJECT']); + +const VERDICT_WORD = SEAT_RECORD_VERDICTS.join('|'); + +/** The inline arm: the heading's own tail, after an optional separator. */ +const INLINE_VERDICT = new RegExp(`^[ \\t]*(?:[—–::·|-][ \\t]*)*(?:\\*\\*)?(${VERDICT_WORD})(?:\\*\\*)?\\b`, 'u'); + +/** The below arm: a line under the heading that opens with a verdict word. */ +const LINE_VERDICT = new RegExp(`^[ \\t]*>?[ \\t]*(?:\\*\\*)?(${VERDICT_WORD})(?:\\*\\*)?\\b`, 'u'); + +/** + * The record a comment body carries: `null` when there is none, else + * `{ verdict }` with the verdict word, or `null` when none could be read. Pure. + */ +export function seatRecordOf(body) { + const text = String(body ?? ''); + const m = SEAT_RECORD_MARKER.exec(text); + if (!m) return null; + const [headingTail, ...below] = text.slice(m.index + m[0].length).split(/\r?\n/); + const inline = INLINE_VERDICT.exec(headingTail); + if (inline) return { verdict: inline[1] }; + for (const line of below) { + const v = LINE_VERDICT.exec(line); + if (v) return { verdict: v[1] }; + } + return { verdict: null }; +} + +/** Does this comment body carry a seat record whose verdict is ACCEPT? Pure. */ +export function seatRecordAccepts(body) { + return seatRecordOf(body)?.verdict === 'ACCEPT'; +}