Skip to content

Commit 39ea1d6

Browse files
pm(post-stamped): every seat post opens with a signature line LANE#N · ACCOUNT · SESSION, refused without one; the recognisers skip it (#27263)
Fixes #27113 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:发帖脚本 `post-stamped`、建卡脚本 `issue-create`、中继的改评论/改正文两条通道,现在要求帖子第一行是署名「车道#席位号 · 账户 · 会话」,缺署名就拒发;所有按首行识别记录的脚本先跳过这一行署名再识别。 - 为什么改:所有回复都以同一个机器人账号出现,您看不出是哪个车道、哪个席位、哪个账户写的;按您「缺署名就拒发」的裁决落地。 - 风险与回滚:席位第一次发帖会被拒(拒发信息印出格式,补一行即可);回滚 = revert 本 PR。没有署名的旧帖子,识别结果不变。 - 先看:`scripts/pm/check-half-states.mjs` 的署名读取器(唯一拼写);`scripts/pm/post-stamped.mjs` 的 `main()` 拒发分支;`scripts/pm/fleet-write/dispatch.mjs` 的 `unsignedEditBodies`(含名册豁免)。 - 两分钟验证:`printf 'hello\n' | node scripts/pm/post-stamped.mjs --dry-run --comment=1 --repo=o/r` 预期退出码 2,并印出「opens with no signature line」和格式;第一行加上 `domain:skills#2 · os-justin · session_…` 再跑,预期退出码 0。 - 需要你做的:无,席位落地(Tier S,席位按合同复核落地)。 ## What changes **The signature.** The first non-empty line of a seat-authored body is `LANE#N · ACCOUNT · SESSION`, for example `domain:skills#2 · os-justin · session_01QSeKYmhU5KEj1nukYhp1Tg`. The parts: - LANE: one of `domain:` or `repo:` plus a lowercase name, `epic:#` plus the parent card, `triage`, `director` or `coordinator`. - N: the seat number, from 1. - ACCOUNT: GitHub's login rule. - SESSION: the relay envelope's shape, `session_` plus 6 or more alphanumerics. The separator is exactly one space, U+00B7, one space. The spelling exists once: `SEAT_SIGNATURE_LINE` / `readSignatureLine` / `stripSignatureLine` / `signatureRefusal` / `signatureRefusalText` in `scripts/pm/check-half-states.mjs`. Every writer and reader below imports it. **Write side: refused, nothing inserted.** - `post-stamped.mjs`, `--comment` and `--body` alike: an unsigned body exits `EXIT_REFUSED` (2) before any request, a dry run included. It is judged beside the render, under the same exit, so a body that also breaks the stamp contract gets both refusals. - `issue-create.mjs`: an unsigned card body is a plan error (exit 2), listed beside the stamp-token error. Nothing calls this door from a workflow. - `fleet-write/dispatch.mjs`: a `comment_edit` or `issue_patch` carrying a `body` in a seat's `--actions-file` is refused as usage (2), right after the file is parsed. The check runs before the session, the packer or a dry run. The fleet roster's generated body is exempt by its own `ROSTER_MARKER`, imported. That is the one generated body that reaches these ops through this door; the ruling keeps generated cards out of scope. - ⛔ `pr_create` is out of scope, and so is the library path (`packRequest`). A pull request body's line 1 is `Fixes #n`, a closing keyword the platform reads at that position. The ruling covers posts, and the dev writes PR bodies. The library's callers are the tools above, and each judges its own body first. - `--sign-from` (the ruling's optional half) is not built. The refusal prints the shape, and the seat types one line. **Read side: one leading signature line changes no reading.** I searched `scripts/pm/**` for every first-line idiom: `split(…)[0]`, `find`/`findIndex` on the first non-blank line, anchored markers without the `m` flag, and the named keys. Changed (each now reads through `stripSignatureLine`): | reader | file | what keys on line one | |---|---|---| | `OS_DEV_REPORT_MARKER` | check-half-states | the `os-dev-report` marker; composed from `SEAT_SIGNATURE_SOURCE`, still anchored, still no `m` | | `yieldedClaimIndexes` | check-half-states | a claim's yield line | | `isTriageRulingComment` | check-half-states | triage ruling openings | | `h56StampedReadings` / `h56AmbiguousLines` | check-half-states | the act's OPENING-line stamp, the reader post-stamped's positional refusal imports | | `h65Headline` | check-half-states | round records (round-open marker, round close, stand-down brief) | | `readMailboxLine` | record-recognisers | the seven mailbox doorbell keys (`MAILBOX_KEYS`) | | `firstContentLine` → `classifyRulingComment` | check-prior-rulings | director `Ruling:` comments | | `firstLineKeyText` / `isRecheckComment`, `releaseTextRefusal` | label-write | the owned-24h knock key; the release text's `Release:` line | | `decisionAnalysisSignals` | post-stamped | a decision frame on the plain opening line | Need nothing: - These match at any line start (the `m` flag) or read line by line: `CLAIM_COMMENT_MARKER`, `RELEASE_COMMENT_MARKER` (via `markerMatches`), `CONTRACT_REVIEW_HEADING_MARKER`, `SEAT_RECORD_MARKER` (`## 席位核对`), `readContractReviewHandoff`, `readServedTier`, `readLocalRuns`, `claimSeatNumber`, `H44_ROUND_OPEN_MARKER`, `H44_VERDICT_MARKER`, `ACCEPT_VERDICT_MARKER`, `MAINTAINER_BRIEF_MARKER`, `STAY_BEHIND_LINE`. - H64's head window: a signature carries a session id, so it can only strengthen that reading. - post-stamped's `commentRow` is display only. It prints the signature, which is the useful thing to show. - Generated artefacts: fleet-roster's `writeRefusal`, and sweep-closed-cards' marker (direct REST, not a seat writer). - `Contract-review-claim:` and `Brief-claimed-by:` have no machine reader in `scripts/**`. They are skill prose only, so there is nothing to pin. **Skill.** One line in `.claude/skills/pm-dispatch/references/seat-post-protocol.md` states the signature and points to the refusal. The file stays at 88 of 88 lines; the new line is 118 bytes. It is paid for by deleting the standing-liveness pointer line. That line's content lives at `references/dispatch-runbook.md` line 41 (and `seat-lifecycle.md` line 37). No re-wrap was used to make room. ## Tests Battery counts, base `9ff8154c` to head: | self-test | before | after | |---|---|---| | `post-stamped.mjs` | 724 cases / 25 batteries | 735 / 26 (new: the signature, 11) | | `issue-create.mjs` | 48 / 7 | 56 / 8 (new: the signature, 8) | | `record-recognisers.mjs` | 254 / 14 | 276 / 15 (new: the signature line, 22) | | `fleet-write/dispatch.mjs` | 247 / 22 | 260 / 23 (new: the signature at the seat's door, 13) | | `label-write.mjs` | 134 / 11 | 136 / 11 (release battery pin 30 to 32) | | `check-half-states.mjs` | 5034 | 5034 | | `check-prior-rulings.mjs` | 155 | 155 | | `with-fleet.sh` | 32 | 32 (its issue-create dry-run fixture now signed) | | `check-governed-queue-guard.mjs` | 401 | 401 | The record-recognisers battery covers three things: - The reader's shape, including every lane form and nine near misses. - A pin holding the session half equal to the relay's `SESSION_SHAPE` (`fleet-write/ops.mjs`; mirrored because `check-half-states.mjs` may not import it). - Each recogniser the card names, driven unsigned and signed. Each pin asserts the reading is positive and identical. These cover `## Contract review` (locator, `Served-tier:`, `Local-runs:`), `os-dev-report`, `Contract-review-handoff:`, claim and release lines, the yield line, round records, `## 席位核对`, all seven mailbox keys, triage and director rulings, and the H56 opening stamp. Readers in other modules are imported inside the self-test body, so the module the queue guard loads lazily gains no edge. Three fixtures had to be signed, each because its writer now refuses unsigned bodies: issue-create's `body.md` / `stamped.md` / `b.md`, `with-fleet.sh`'s issue-create dry-run body, and dispatch's CLI end-to-end `issue_patch` (its corrupted-byte offset is now stated as `AT` plus the signature's bytes). Two structural pins moved with the code they pin. post-stamped's refused-render block now matches the combined refusal. Its facet-marker composition pin is unchanged: `code` is still `stripMarkdownCode(String(body ?? ''))`, and only the frame's line list is stripped. Ablation: not run. The diff is scripts plus one doc line, which the role contract excludes from ablation. ## Acceptance notes - PM mechanism hypotheses, measured: - ① Partly falsified, three ways. The reader cannot live in `record-recognisers.mjs`: that file imports `check-half-states.mjs`, which owns the first-line markers, so the reverse edge would close a top-level-await cycle. It lives in `check-half-states.mjs`, the module every writer and reader already imports. The lane class `[a-z]+` would refuse the real `domain:engine-core`. A desktop-only session shape would refuse a `cse_`-derived container session. - ② Confirmed: body mode has no marker-line contract, so the rule applies to both modes. - ③ Refined: the render fails with three different texts, and `renderBody` is shared with label-write's release comment. So the signature is a second block under the same exit, not a row in the stamp-contract list, whose header and footer are about stamps. - ④ Refined: per-recogniser with/without pins rather than doubling every fixture. - Writers outside the card's named set still accept an unsigned body. Examples are a raw `comment` or `issue_create` op handed to the dispatch door (for instance `mailbox-open.mjs`'s mailbox-close comment) and label-write's `Release:` comment. Their readers skip a signature, so a signed body reads correctly; the requirement just is not enforced there. Widening the requirement is the seat's call, so I did not file anything. - `OS_DEV_REPORT_MARKER` keeps its `i` flag (the shipped `OS-DEV-REPORT` dialect), so the composed signature is read case-insensitively in that one marker. Every writer refuses a mis-cased signature before posting, so this matters only on the read side. - H64's `SEAT_SESSION_ID` (the 26-character desktop form) is narrower than the signature's session shape. It is untouched. ## Verification All readings were taken at head `bce074a0`. <details><summary>Gate derivation and reconciliation</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (exit 0): 65 commands, 9 paths vs merge base `9ff8154cf`. All 65 were run locally and recorded as `COMMAND :: exit N`. `--ran` (exit 0): `65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED (a DERIVED zero — all 65 recorded an exit code and none of them is 3)`. `pnpm --filter @objectstack/lint run check:doc-formula-expressions` first exited 3 (PREREQUISITE NOT MET: `@objectstack/formula` / `@objectstack/lint` unbuilt in a fresh worktree). After `turbo run build` of those two under the verify lock (`VERDICT command-exit 0`), it exited 0. `pnpm check:pm-dispatch-gates` exit 0 (fast tier, 1935 cases). Not derived but named by the dispatch: `pnpm check:pm-governed-prose` exit 0, and `check-governed-queue-guard.mjs --self-test` exit 0 (401). Consumers also run: `fleet-roster` 78, `mailbox-open` 45, `check-handover` 46. Control-byte scan over the changed files: no hits. </details> <details><summary>The refusal, end to end (dry runs)</summary> An unsigned comment exits 2 with `post-stamped: REFUSED — the body opens with no signature line. Nothing was written.` plus the shape. An unsigned comment with a bare opening stamp exits 2 and prints both the stamp-contract block and the signature block. A signed body whose claim line (now line 2) carries a bare stamp still gets the positional refusal. A signed body with `{{NOW}}` dry-runs to exit 0 with the signature sent as written. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c082af6 commit 39ea1d6

9 files changed

Lines changed: 459 additions & 36 deletions

File tree

‎.claude/skills/pm-dispatch/references/seat-post-protocol.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525

2626
- 活性惰性半边在接管冲突时评估一次:Routine 座位查调度器 `last_fired` 与 `next_run`。
2727
- 会话座位查最近一条产出评论的时间戳;回收 = 改正文 + 审计评论。
28-
- 常设半边是总监席摘要的座位贴活性巡查,同一判据,细则见 `dispatch-runbook.md`。
2928
- 可跑读法:本车道 `pm:dispatched` 卡按 `updated_at` 降序一搜,或读最近一条认领评论。
3029
- 写侧刷新三时刻:派发、收班、暂停或交接前;时间戳恒取自 `scripts/pm/post-stamped.mjs`。
30+
- 帖与卡正文首行恒为署名 `LANE#N · ACCOUNT · SESSION`;缺即写工具拒发(`post-stamped.mjs` 头注)。
3131
- 刷新前回执敲门;`--body` 拦截:评论晚于末次戳 ⇒ `--ack-through=ID`;空座欠账无卡号 ⇒ 拒写。
3232
- 耐久状态只住正文或以 App 写入;正文守中继上限 60,000 字节,超大贴由半状态巡查浮出。
3333
- 协议升级迁移状态、或巡检发现正文不符时,当场改正文 + 审计评论。

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

Lines changed: 146 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,135 @@ export const CLAIM_COMMENT_MARKER = /^\s*>?\s*Claim(?:ed)?\s*:/mi;
12551255
*/
12561256
export const RELEASE_COMMENT_MARKER = /^\s*>?\s*Release\s*:/mi;
12571257

1258+
// ---------------------------------------------------------------------------
1259+
// The seat SIGNATURE line — who wrote a seat-authored body, on its first line (#27113).
1260+
//
1261+
// Every seat writes through the fleet relay, so every post lands under one bot
1262+
// login and the platform's author field cannot say which lane, seat or account
1263+
// wrote it. The maintainer's ruling (「让发帖脚本 post-stamped 强制每条帖子第一行带署名:
1264+
// 车道#席位号 · 账户 · 会话,缺署名就拒发」) puts all of it on the FIRST non-empty line:
1265+
//
1266+
// LANE#N · ACCOUNT · SESSION e.g. `domain:skills#2 · os-justin · session_01…`
1267+
//
1268+
// LANE a seat as the role table spells it: `domain:` or `repo:` plus a lowercase
1269+
// name (`domain:engine-core` carries a hyphen, measured on the label set),
1270+
// `epic:#` plus the parent card, `triage`, `director` or `coordinator`
1271+
// N the seat number, from 1
1272+
// ACCOUNT the login a claim's `Account:` line carries — GitHub's own login rule
1273+
// SESSION the full id, in the relay envelope's shape (`SESSION_SHAPE`, fleet-write/ops.mjs,
1274+
// mirrored here because this module may not import that one; the record-recognisers
1275+
// self-test holds the two sources equal) — a container session is `session_` plus
1276+
// the `cse_` tail, so the 26-character desktop form is not the only real one
1277+
//
1278+
// The separator is exactly ` · ` — one space, U+00B7, one space — the doorbell keys' dot.
1279+
//
1280+
// TWO halves read this ONE spelling, and neither restates it:
1281+
// WRITE the seat-authored writers REFUSE a body whose first non-empty line is not one:
1282+
// `post-stamped.mjs` (both modes), `issue-create.mjs`, and the `comment_edit` /
1283+
// `issue_patch` bodies a seat hands `fleet-write/dispatch.mjs`. ⛔ No writer inserts
1284+
// one for the seat: a body without a signature is never posted.
1285+
// READ every recogniser that keys on a body's FIRST line reads it through
1286+
// `stripSignatureLine`, so one leading signature line changes no reading. A marker
1287+
// that already matches at any line start (the `m` flag) needs nothing.
1288+
//
1289+
// ⛔ Exactly ONE line is skipped, and only an exact match. A near miss stays the first line:
1290+
// the write half refuses it by name, and the read half does not quietly read past it.
1291+
// ---------------------------------------------------------------------------
1292+
1293+
/** The four parts' patterns, unanchored, in order — the ONE spelling every reader and writer composes from. */
1294+
const SIGNATURE_LANE_SOURCE = '(?:domain:[a-z][a-z0-9-]*|repo:[a-z][a-z0-9-]*|epic:#[1-9][0-9]*|triage|director|coordinator)';
1295+
const SIGNATURE_ACCOUNT_SOURCE = '[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}';
1296+
/** The relay envelope's session shape without its anchors — `SESSION_SHAPE` in `fleet-write/ops.mjs`. */
1297+
export const SEAT_SIGNATURE_SESSION_SOURCE = 'session_[A-Za-z0-9]{6,}';
1298+
/** The separator, exactly. */
1299+
export const SEAT_SIGNATURE_SEPARATOR = ' · ';
1300+
1301+
/** The signature line's pattern, unanchored and capture-free — what a marker composes when it admits one leading signature. */
1302+
export const SEAT_SIGNATURE_SOURCE =
1303+
`${SIGNATURE_LANE_SOURCE}#[1-9][0-9]*${SEAT_SIGNATURE_SEPARATOR}${SIGNATURE_ACCOUNT_SOURCE}${SEAT_SIGNATURE_SEPARATOR}${SEAT_SIGNATURE_SESSION_SOURCE}`;
1304+
1305+
/** ONE line, whitespace around it trimmed, is a signature — with the four parts captured. ⛔ No `g` or `m` flag. */
1306+
export const SEAT_SIGNATURE_LINE = new RegExp(
1307+
`^(${SIGNATURE_LANE_SOURCE})#([1-9][0-9]*)${SEAT_SIGNATURE_SEPARATOR}(${SIGNATURE_ACCOUNT_SOURCE})${SEAT_SIGNATURE_SEPARATOR}(${SEAT_SIGNATURE_SESSION_SOURCE})$`,
1308+
'u',
1309+
);
1310+
1311+
/** The shape, as a refusal prints it — placeholders only, so it survives a GitHub body unchanged. */
1312+
export const SEAT_SIGNATURE_SHAPE = 'LANE#N · ACCOUNT · SESSION — e.g. `domain:skills#2 · os-justin · session_01…`';
1313+
1314+
/** The index of a line list's first non-blank line, or -1. */
1315+
function firstNonBlankIndex(lines) {
1316+
return lines.findIndex((line) => line.trim() !== '');
1317+
}
1318+
1319+
/**
1320+
* The signature a body opens with, read off its FIRST non-empty line, or null when that line is
1321+
* not one (an empty body included). Pure.
1322+
*
1323+
* @param {string} body
1324+
* @returns {{ lane: string, seat: number, account: string, session: string, line: string } | null}
1325+
*/
1326+
export function readSignatureLine(body) {
1327+
const lines = String(body ?? '').split(/\r?\n/);
1328+
const at = firstNonBlankIndex(lines);
1329+
if (at < 0) return null;
1330+
const line = lines[at].trim();
1331+
const m = SEAT_SIGNATURE_LINE.exec(line);
1332+
return m ? { lane: m[1], seat: Number(m[2]), account: m[3], session: m[4], line } : null;
1333+
}
1334+
1335+
/**
1336+
* The body a first-line recogniser reads: unchanged when its first non-empty line is not a
1337+
* signature, else everything from the next non-empty line on — ONE signature line and the blank
1338+
* lines around it removed, so "the first line" is the same line it was before the rule. Pure.
1339+
*/
1340+
export function stripSignatureLine(body) {
1341+
const text = String(body ?? '');
1342+
if (readSignatureLine(text) === null) return text;
1343+
const lines = text.split(/\r?\n/);
1344+
const rest = lines.slice(firstNonBlankIndex(lines) + 1);
1345+
const from = firstNonBlankIndex(rest);
1346+
return from < 0 ? '' : rest.slice(from).join('\n');
1347+
}
1348+
1349+
/**
1350+
* Why a body about to be written by a seat-authored writer carries no signature, or null when its
1351+
* first non-empty line is one. The row every writer prints, so the refusal reads the same from
1352+
* each door. Pure.
1353+
*
1354+
* @returns {{ line: string, why: string } | null}
1355+
*/
1356+
export function signatureRefusal(body) {
1357+
if (readSignatureLine(body) !== null) return null;
1358+
const lines = String(body ?? '').split(/\r?\n/);
1359+
const at = firstNonBlankIndex(lines);
1360+
const line = at < 0 ? '(the body is empty)' : lines[at].trim();
1361+
// A line naming a seat number AND a session id is a signature typed wrong — named as such, so the fix is the spelling.
1362+
const near = at >= 0 && /#[1-9]/u.test(line) && /\bsession_/u.test(line);
1363+
return {
1364+
line: line.length > 160 ? `${line.slice(0, 160)}…` : line,
1365+
why: near
1366+
? 'the first non-empty line reads like a signature and is not one — check the lane, the `#N` seat number, the login, the full session id, and that each separator is exactly ` · `'
1367+
: 'the first non-empty line is not a signature — the reader cannot tell which lane, seat, account or session wrote this',
1368+
};
1369+
}
1370+
1371+
/**
1372+
* The refusal a seat-authored writer prints, from `signatureRefusal`'s row — ONE text, so the
1373+
* signature reads the same from every door. `tool` names the door; `where` names the body when
1374+
* the door carries more than one (an action of a stroke).
1375+
*/
1376+
export function signatureRefusalText(row, { tool, where = 'the body' } = {}) {
1377+
return (
1378+
`${tool}: REFUSED — ${where} opens with no signature line. Nothing was written.\n` +
1379+
` 1. [signature] ${row.why}\n` +
1380+
` line: ${row.line}\n` +
1381+
` write: ${SEAT_SIGNATURE_SHAPE} — as the FIRST non-empty line, alone\n\n` +
1382+
' Every reply lands under one bot login, so this line is the only place a reader learns which lane, seat,\n' +
1383+
' account and session wrote it. ⛔ No flag turns it off, and no writer inserts it for you.'
1384+
);
1385+
}
1386+
12581387
// ---------------------------------------------------------------------------
12591388
// Reading the two ownership markers — ONE place, decoration included (#18680).
12601389
//
@@ -5584,7 +5713,8 @@ export function yieldedClaimIndexes(commentRows) {
55845713
a.stamp !== null && b.stamp !== null && a.stamp !== b.stamp ? a.stamp < b.stamp : a.index < b.index;
55855714
const yields = [];
55865715
for (const row of rows) {
5587-
const first = row.body.split(/\r?\n/).find((line) => line.trim() !== '') ?? '';
5716+
// One leading signature line is not the yield's line (`stripSignatureLine`, #27113).
5717+
const first = stripSignatureLine(row.body).split(/\r?\n/).find((line) => line.trim() !== '') ?? '';
55885718
if (!markerMatches(CLAIM_YIELD_MARKER, first)) continue;
55895719
const sessions = new Set(first.match(new RegExp(SEAT_SESSION_ID.source, 'g')) ?? []);
55905720
if (sessions.size !== 1) continue;
@@ -7979,7 +8109,8 @@ export const TRIAGE_RULING_ANCHORS = [
79798109
* H4 paid for (#10102) applies verbatim.
79808110
*/
79818111
export function isTriageRulingComment(body) {
7982-
const first = String(body ?? '')
8112+
// One leading signature line is not the ruling's opening (`stripSignatureLine`, #27113).
8113+
const first = stripSignatureLine(body)
79838114
.split('\n')
79848115
.map((l) => l.trim())
79858116
.find((l) => l.length > 0);
@@ -12014,7 +12145,14 @@ export function h50ThreadReadMismatch(issue, commentRows, sinceMs = Date.parse(T
1201412145
* ⛔ No `g` flag — a shared regex carrying `lastIndex` between callers is a
1201512146
* state bug waiting for its second reader.
1201612147
*/
12017-
export const OS_DEV_REPORT_MARKER = /^[\s>]*(?:<!--[ \t]*)?os-dev-report[ \t]*(?:-->)?[ \t]*(?:\r?\n|$)/i;
12148+
// ⭐ One leading signature line (`SEAT_SIGNATURE_SOURCE`, #27113) may stand above the marker: composed from the one
12149+
// spelling, never restated, and still anchored at the body's start — a marker on any OTHER later line stays refused.
12150+
// The `i` the dialect needs folds case on that signature too: a read-side allowance only, since every writer refuses
12151+
// a signature spelt in any other case before it is posted.
12152+
export const OS_DEV_REPORT_MARKER = new RegExp(
12153+
`^(?:\\s*${SEAT_SIGNATURE_SOURCE}[ \\t]*\\r?\\n)?[\\s>]*(?:<!--[ \\t]*)?os-dev-report[ \\t]*(?:-->)?[ \\t]*(?:\\r?\\n|$)`,
12154+
'i',
12155+
);
1201812156

1201912157
/** How many question texts the finding sentence echoes before it degrades to a count. */
1202012158
export const H52_QUESTION_ECHO_CAP = 2;
@@ -12886,7 +13024,8 @@ export function stampDriftMinutes(stamp, createdAt, updatedAt = createdAt) {
1288613024
* than one stamp contributes nothing — see `h56AmbiguousLines`.
1288713025
*/
1288813026
export function h56StampedReadings(text) {
12889-
const lines = String(text ?? '').split('\n');
13027+
// The OPENING line is the act's own line, so one leading signature line above it is skipped (#27113).
13028+
const lines = stripSignatureLine(text).split('\n');
1289013029
const opening = lines.findIndex((line) => line.trim().length > 0);
1289113030
const out = [];
1289213031
for (let i = 0; i < lines.length; i++) {
@@ -12902,7 +13041,7 @@ export function h56StampedReadings(text) {
1290213041

1290313042
/** How many of those positions were HELD OUT for carrying more than one stamp. */
1290413043
export function h56AmbiguousLines(text) {
12905-
const lines = String(text ?? '').split('\n');
13044+
const lines = stripSignatureLine(text).split('\n');
1290613045
const opening = lines.findIndex((line) => line.trim().length > 0);
1290713046
let held = 0;
1290813047
for (let i = 0; i < lines.length; i++) {
@@ -15788,7 +15927,8 @@ export const H65_ROUND_ARTEFACT_SHAPES = Object.freeze([
1578815927
* down (a report ABOUT a round, this card's own body) is not a round artefact.
1578915928
*/
1579015929
export function h65Headline(body) {
15791-
for (const line of h44StripFences(body).split('\n')) {
15930+
// One leading signature line is not the headline (`stripSignatureLine`, #27113).
15931+
for (const line of h44StripFences(stripSignatureLine(body)).split('\n')) {
1579215932
if (line.trim()) return line;
1579315933
}
1579415934
return '';

‎scripts/pm/check-prior-rulings.mjs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ import { isEntrypoint } from '../invoked-as.mjs';
215215
// `check-half-states.mjs`, so this reader and the patrol cannot come to
216216
// disagree about which board is being read or whether this container's fetch
217217
// reaches it.
218-
import { DEFAULT_SWEEP_REPO, resolveSweepRepo, proxyRearmPlan, PROXY_FLAG, PROXY_REARM_GUARD } from './check-half-states.mjs';
218+
import { DEFAULT_SWEEP_REPO, resolveSweepRepo, proxyRearmPlan, PROXY_FLAG, PROXY_REARM_GUARD, stripSignatureLine } from './check-half-states.mjs';
219219

220220
export const EXIT_OK = 0;
221221
export const EXIT_SELF_TEST_FAILED = 1;
@@ -333,7 +333,8 @@ export const RULING_COMMENT_SHAPES = Object.freeze([
333333
* ruling mid-body is about a ruling, not one.
334334
*/
335335
export function firstContentLine(body) {
336-
const line = String(body ?? '').split('\n').find((l) => l.trim() !== '');
336+
// One leading signature line is not the ruling's line (`stripSignatureLine`, #27113).
337+
const line = stripSignatureLine(body).split('\n').find((l) => l.trim() !== '');
337338
if (line === undefined) return '';
338339
return line.replace(/^[\s>]+/, '').replace(/^#{1,6}\s+/, '').replace(/^[*_`\s]+/, '').trim();
339340
}

0 commit comments

Comments
 (0)