diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index c639e9a7b5f..7fb10228e37 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -77,9 +77,9 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 开轮互斥:fire 开始、首个写动作之前读四个读数,未收班活动任一未满一轮 ⇒ 自退。 - 读数一、二:最近的收班简报,与晚于它的最新开轮标记。 - 开轮标记是轮中会话在座位贴上的唯一痕迹;标记可漏写,认领不可省。 -- 读数三:本车道 `pm:dispatched` ∪ `pm:queue` 卡最新非本 session 的 `Claim:` 评论。 +- 读数三:本车道 `pm:dispatched` ∪ `pm:queue` 卡最新非本 session 同席号的 `Claim:` 评论。 - 或收班简报后卡分支有推送(逐卡 `git ls-remote`):推即认领,欠一条 `Claim:`。 -- 读数四:本车道 `state=CLOSED` 按 `updated_at` 降序第一张卡的 `Claim:` session ID。 +- 读数四:本车道 `state=CLOSED` 按 `updated_at` 降序首张带同席号 `Claim:` 卡的 session ID。 - 评审链与总监席的笔迹是跨席作业不算占席;读数三、四只增加自退、永不清座位。 - 总监席多场并行、无在任者:读数一、二与自退对它不成立;`domain:*` 席互斥照旧。 - 同 session 的 subagent dev 认领不触发自退,甄别靠分支。 @@ -98,7 +98,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 内容写只走 REST 代理,⛔ 无 MCP 写;`user.login` 记令牌不记席位,归属 = 文本里的 session ID。 - GitHub 上一切新内容用英文;中文只留四通道(维护者速读、轮报、裁决引文、四维分析)。 - 裁决引文照抄不译;四维中文只管新记录,存量英文块 ⛔ 不迁移;存量中文 ⛔ 不追溯改写。 -- 一座位一车道双射:域 X 谁管、PM Y 管什么,各恰好一个答案。 +- 一 PM 恰管一车道,一车道由一组座位管,一席一贴;PM Y 管什么恰一个答案。 ## 状态模型 @@ -199,8 +199,8 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - `domain:devx`(工程面)与 `domain:spec`(契约面)跨仓归各自车道。 - 其余(发布库与 apps)归 `domain:ui` 执行席;症状位置不改流向,docs 随所记录的面走。 - 落点不明留分诊首触,⛔ 不猜。 -- 车道可按仓分席(域×仓):忙时一个 domain 车道开多个按仓席位,闲时收归一席。 -- 分席各有独立座位贴与 claim 互斥;分席是名册状态,不改协议文本。 +- 车道可分席(席号 `· seat N`,或域×仓):忙时一车道开多席,闲时收归一席;每席独立座位贴。 +- 认领评论带 `Seat: domain:X#N` 行申报席号(缺省席 1);同席认领互斥,他席认领不触发自退。 - 拆分触发预登记:单轮逼近 fire 周期,或某仓在全序下持续断粮 ⇒ 按仓拆回多席。 - seam 卡单一归属:归修复落地仓的席,双仓皆动 objectstack 侧主导。 - `scripts/pm/**` 等住 objectstack 的全板工具链单写手恒为 objectstack 侧席,他侧上游立卡回链。 @@ -217,7 +217,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - ⛔ pin bump 不做 rider:走专用 bump 脚本连带 override 与 lockfile。 - 规则 3:联动杂事立单,不靠记忆。 - 已验收 PR 的产物流向另一仓,接受该 PR 的执行席立即在消费仓立后续单,带 `Blocked-by:`。 -- 规则 4:纵向拆分,一个分诊 PM + N 个执行 PM,一人一车道双射。 +- 规则 4:纵向拆分,一个分诊 PM + N 个执行 PM;一人恰一车道,一车道可坐多席。 - 分诊座位唯一生产:定级/路由/type/查重/shadow/`duplicate_of`;执行席永不定级或路由裸卡。 - 执行座位信任标签,只在本车道认领;误标 ⛔ 不自行改,挂 `pm:retriage` + 异议评论同笔。 - 突发积压调频率或 `batch`,持续积压按仓拆席走 PR;⛔ 不跨车道借调。 @@ -252,12 +252,11 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 | (无固定归属,按落点分诊) | `packages/apps/*`、`packages/console`(dist 由脚本生成 ⛔ 不手改;UI 缺陷走 `repo:objectui`)、`examples/*`(归它演练的子系统)、`docs/audits/**` | - 表未覆盖的包首次分诊时归类并走 PR 更新本表;新增或退役 `domain:*` 必须同批改本表。 -- 座位在编情况以 `label:pm:seat` 索引为准,每个 `domain:*` 与 `repo:*` 恰一张座位贴。 +- 座位在编以 `label:pm:seat` 索引为准,每车道(`domain:*`/`repo:*`)一组座位贴、每席恰一张。 - `domain:*` 只由分诊席产出,例外仅在飞卡衍生 sub-issue 承父卡域;无标签的卡 ⛔ 不得认领。 - 跨域例外路径:拆不动的跨域单 PR 由分诊席指定一个车道 PM 认领,认领评论申报文件面。 -- 只在跨域例外路径上跑定向在飞检查,范围 = 该单文件面触及的那几个域的在飞单。 -- 定向在飞检查读各在飞单认领评论的文件面申报,要求不相交,相交即让行。 -- 日常同域批次选择不跑全局在飞检查,同域独立性由本域批次选择保证。 +- 派发前在飞检查:读本车道 `pm:dispatched` 卡认领评论申报的文件面,本地求交、不另发查询。 +- 跨域例外路径的范围加上该单文件面触及各域的在飞单;相交 ⇒ 排该 PR 之后串行或让行。 - 跨域相交只经三条越界通道:跨域例外路径、认领席跟到底的越界面、接手无主阻塞项。 - 合并队列是共享串行资源:车道买到的是并行编写,不是并行落地。 - 谁发现 flake 谁修或立单,⛔ 不绕行。 @@ -804,6 +803,7 @@ Session: `session_` Branch: `claude/issue--` Worktree: `-issue-` Domain: `domain:` +Seat: `domain:#` (the seat number this PM sits on; absent = seat 1) File surface: `<预期触碰的目录>` (stop on breach; explain in the report) Container & model: ``, `mode:subagent | mode:cloud`, `model: <档位,引当次 --tier 输出>` Clause-②: yes | no diff --git a/.claude/skills/pm-dispatch/references/core-rules.md b/.claude/skills/pm-dispatch/references/core-rules.md index e7b1cd2e8e8..65daecc0569 100644 --- a/.claude/skills/pm-dispatch/references/core-rules.md +++ b/.claude/skills/pm-dispatch/references/core-rules.md @@ -16,7 +16,7 @@ - 一轮自 fire 唤醒起,到队列清空或用量窗口耗尽止;轮次报告是轮末汇报 ⛔ 不是停止信号。 - 入座自绑 cron Routine 作默认唤醒、只定下次唤醒;fire 到时未清空即接续,⛔ 不重开不重排。 - 分诊席跑普通直连会话、默认判断档,裁决归总监席;`send_later` 只是 ≤55 分钟加速器。 -- 开轮互斥读四个读数:收班简报、开轮标记、本车道最新他会话认领、最新关闭卡的认领。 +- 开轮互斥四读数:收班简报、开轮标记、本车道同席号最新他会话认领与最新关闭卡认领。 - 未收班活动任一未满一轮即自退,总监席除外;有简报即坐席,坐席后核章程再写开轮标记。 ## 全体座位的不变量 - 状态住 GitHub 标签、assignee、正文行与座位贴;⛔ 本地零状态;循环须能从新会话恢复。 @@ -24,7 +24,7 @@ - 该例外须把授权原话引在 PR 正文并由另一座位或维护者复核,⛔ 不得自审自合。 - 用户账号仅三用:assignee、授权批准、维护者亲手;写只走 REST 代理,署名随令牌非席位。 - GitHub 新内容一律英文,中文只留轮次报告、派发令裁决引文与四维分析;引文照抄不译。 -- 存量中文 ⛔ 不追溯改写;一座位一车道双射,每域恰一 PM;与 AGENTS.md 冲突以之为准。 +- 存量中文 ⛔ 不追溯改写;一 PM 一车道、一车道多席、一席一贴;与 AGENTS.md 冲突以之为准。 ## 状态模型 - 可派发等于 open 加队列标签加无 assignee 且不带 `pm:retriage`,已设 assignee 即 ⛔ 永不碰。 - `pm:on-hold` 仅当带机器可读 `Restart-when:` 行才合法,触发文件走 `Restart-touch:` 行。 @@ -65,7 +65,7 @@ - 域由分诊读代码判定,⛔ 绝不从 issue 标题的词汇猜域;说不出修复碰哪个文件就还不可标。 - 域到包家族的对照表住 SKILL.md,增删一个域标签必须同批改那张表。 - 域标签只由分诊席产出,例外仅在飞卡衍生 sub-issue 承父卡域;无标签的卡 ⛔ 不得认领。 -- 跨域单 PR 由分诊席指定一个车道 PM 认领并申报完整文件面,只此路径跑定向在飞检查。 +- 跨域单 PR 由分诊席指定一个车道 PM 认领并申报完整文件面,在飞检查同域跨域皆跑。 - 合并队列是共享串行资源,谁发现 flake 谁修或立单,⛔ 不绕行。 ## 座位贴协议 - 一座位一贴,正文固定四段现值:当前 PM、继承台账、热文件串行队、说明。 diff --git a/.claude/skills/pm-dispatch/references/seat-post-protocol.md b/.claude/skills/pm-dispatch/references/seat-post-protocol.md index 7555c2e80f2..6eb8bda51eb 100644 --- a/.claude/skills/pm-dispatch/references/seat-post-protocol.md +++ b/.claude/skills/pm-dispatch/references/seat-post-protocol.md @@ -6,6 +6,7 @@ - 四段的现值由在任座位在写侧刷新时收缩;⛔ 岗位说明 PR 不代改贴正文。 - 标题即状态板,固定格式 `[PM seat] <座位名> — <状态>`;`label:pm:seat` 列表页即舰队状态板。 +- 多席车道每席一贴,座位名带席号后缀 `domain:X · seat N`;席 1 省略后缀,沿用现贴。 - 状态词表:`🟢 ` 在任、`🟢 Routine`、`⏳ vacant`、`⏸️ paused`。 - assignee = 在任 PM 的 GitHub 账号,退场或回收时摘除;无 assignee = 空缺,与标题互为校验。 - 例外:Routine 座位的 bot 身份通常不可被 assign,以标题为准。 @@ -17,7 +18,7 @@ - 旧账留在上一版 body revision,审计评论带版本指针(哪一版、何时)。 - 超大贴由半状态巡查的 report-only 项浮出,⛔ 不等读取失败。 - 热文件串行队是跨轮跨班次的常设事实:文件 → 有序卡片清单 → 每张卡认领的区域。 -- 新增座位贴(拆域)先查 `pm:seat` 索引再立贴。 +- 新增座位贴(拆域或加席)先查 `pm:seat` 索引再立贴,席号取本车道现有最大席号加一。 ## 活性、写侧刷新与收班 diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index c98dfdaed61..47a8bfc9e86 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -6872,6 +6872,15 @@ export function seatDeclaresWait(markerBody) { * `domain:devx @ objectui` — a lane on a SIBLING board * `repo:cloud` / `skills` / `triage (objectstack-wide)` * + * plus the free-claim multi-seat shape the protocol added afterwards: + * + * `domain:spec · seat 2` — seat NUMBER 2 of a lane several PMs sit on + * + * The `· seat N` suffix is a roster axis, not a lane: it is stripped BEFORE the + * lane test, so a numbered seat reads as its `domain:*` lane exactly as the + * bare post does, and seat 1 keeps the bare title (an absent suffix reads as + * seat 1 — every seat post written before the suffix existed is seat 1). + * * The `foreign` flag is the load-bearing half and it exists for the same reason * H19 refuses to guess at a cross-repo 404: this sweep reads ONE repo. A seat * whose lane lives in a sibling repo has an inventory this patrol cannot see at @@ -6881,19 +6890,27 @@ export function seatDeclaresWait(markerBody) { * changes. `repo:*`-scoped and lane-less seats (`triage`) are foreign for the * same reason: there is no `domain:*` label to count a lane inventory against. * - * @returns {{ lane: string|null, foreign: boolean }} + * @returns {{ lane: string|null, foreign: boolean, seat: number|null }} — + * `seat` is the seat number (1 when the title carries no `· seat N`), or + * null when the title does not parse as a seat post at all. */ export function seatLane(issue) { const m = /^\[PM seat\]\s*(.*?)\s*—\s*(.*)$/u.exec(issue?.title ?? ''); - if (!m) return { lane: null, foreign: true }; - const raw = m[1].trim(); + if (!m) return { lane: null, foreign: true, seat: null }; + let raw = m[1].trim(); + // The `· seat N` suffix (free-claim multi-seat) is a seat NUMBER, wherever + // it sits in the lane half; taken out first so the lane test below sees the + // same text a bare post would. Absent ⇒ seat 1. + const seatM = /\s*·\s*seat\s+(\d+)\b/iu.exec(raw); + const seat = seatM ? Number(seatM[1]) : 1; + if (seatM) raw = (raw.slice(0, seatM.index) + raw.slice(seatM.index + seatM[0].length)).trim(); // An `@ ` suffix names the board the lane lives on. Present ⇒ the lane // is only READABLE there, whatever its `domain:*` spelling says here. const at = /^(.*?)\s*@\s*(\S+)\s*$/u.exec(raw); const lane = (at ? at[1] : raw).trim(); const elsewhere = at ? at[2] !== SWEEP_REPO.repo.split('/')[1] : false; - if (!/^domain:[a-z0-9][a-z0-9._-]*$/i.test(lane)) return { lane: null, foreign: true }; - return { lane, foreign: elsewhere }; + if (!/^domain:[a-z0-9][a-z0-9._-]*$/i.test(lane)) return { lane: null, foreign: true, seat }; + return { lane, foreign: elsewhere, seat }; } /** @@ -8479,7 +8496,38 @@ export function seatPostLastEventMs(seat, commentRows) { } /** - * The newest `Claim:` on a lane, over the dispatched cards this sweep holds. + * The `Seat:` line of a claim body — the seat NUMBER the claiming PM sits on + * (`Seat: domain:#`, free-claim multi-seat), read with the same key-line + * tolerance as `threadReadField` (leading bullet or blockquote, bold or code + * decoration around the key). + * + * Absent ⇒ 1. Every claim written before the line existed belongs to seat 1, + * which is the only seat those lanes had, so the default is backward + * compatible with every existing claim rather than a guess. A line that is + * PRESENT but names no `#` is unreadable and returns null: it matches no + * seat, so its claim is invisible to the seat filter below — the file's + * under-reporting direction on every unrecognised spelling — and ⛔ never + * silently read as seat 1. + */ +const CLAIM_SEAT_KEY_LINE = /^[ \t]*(?:[-*+][ \t]+)?>?[ \t]*(?:\*\*)?`?Seat`?(?:\*\*)?[ \t]*:[ \t]*(.*)$/im; + +/** @returns {number|null} the seat number, 1 when the line is absent, null when it is present but unreadable. */ +export function claimSeatNumber(body) { + const m = CLAIM_SEAT_KEY_LINE.exec(String(body ?? '')); + if (!m) return 1; + const n = /#(\d+)\b/.exec(String(m[1] ?? '')); + return n ? Number(n[1]) : null; +} + +/** + * The newest `Claim:` on a lane, over the dispatched cards this sweep holds, + * restricted to the claims of ONE seat number. + * + * Free-claim multi-seat puts several PMs on one lane, one post each. A post is + * behind only when a claim of ITS OWN seat outran it — another seat's claim is + * that seat's business and would otherwise read every second seat as stale on + * every fire. The seat filter narrows the ROWS handed to the shared claim + * predicate, so what a claim IS stays `latestClaimComment`'s call (below). * * ⚠️ The claim predicate is `latestClaimComment` — REUSED, deliberately not * re-spelled. The filing card is explicit that the literal `Claim:` marker is @@ -8499,9 +8547,10 @@ export function seatPostLastEventMs(seat, commentRows) { * @param {string} lane — a `domain:*` label. * @param {Iterable} issues — open cards this sweep already listed. * @param {Map} commentsByNumber — threads already in the cache. + * @param {number} [seat=1] — the seat number whose claims count (`seatLane(post).seat`). * @returns {{ number: number, at: number }|null} the newest claim, or null. */ -export function newestLaneClaim(lane, issues, commentsByNumber) { +export function newestLaneClaim(lane, issues, commentsByNumber, seat = 1) { let best = null; for (const issue of issues ?? []) { const labels = labelNames(issue ?? {}); @@ -8509,7 +8558,9 @@ export function newestLaneClaim(lane, issues, commentsByNumber) { if (!labels.includes('pm:dispatched') || !labels.includes(lane)) continue; const rows = commentsByNumber?.get?.(issue.number); if (!rows) continue; - const claim = latestClaimComment(rows); + // Only this seat's claims: the filter runs on the rows, the predicate is reused. + const own = (Array.isArray(rows) ? rows : []).filter((row) => claimSeatNumber(row?.body) === seat); + const claim = latestClaimComment(own); if (!claim) continue; const at = Date.parse(claim.createdAt ?? ''); if (!Number.isFinite(at)) continue; @@ -8523,11 +8574,12 @@ export function newestLaneClaim(lane, issues, commentsByNumber) { * * @param {object} seat — the `pm:seat` post. * @param {number|null} seatAt — `T_seat` (`seatPostLastEventMs`). - * @param {{ number: number, at: number }|null} claim — the newest lane claim. + * @param {{ number: number, at: number }|null} claim — the newest lane claim + * OF THIS POST'S SEAT NUMBER (`newestLaneClaim(lane, …, seatLane(seat).seat)`). */ export function h38SeatPostStale(seat, seatAt, claim) { if (!labelNames(seat ?? {}).includes('pm:seat')) return null; - const { lane, foreign } = seatLane(seat ?? {}); + const { lane, foreign, seat: seatNo } = seatLane(seat ?? {}); // A lane this board cannot count has no readable claim population here, so // an absent claim would mean "unreadable", not "none" — H32's `foreign` // reasoning, and the reason this row declines rather than reporting silence. @@ -8542,7 +8594,7 @@ export function h38SeatPostStale(seat, seatAt, claim) { const behindHours = (claim.at - seatAt) / 3_600_000; return ( - `\`pm:seat\` post is STALE — its lane \`${lane}\` carries a \`Claim:\` on #${claim.number} written ` + + `\`pm:seat\` post is STALE — its lane \`${lane}\` (seat ${seatNo}) carries a \`Claim:\` on #${claim.number} written ` + `${behindHours.toFixed(1)}h AFTER this post's last event (claim ${new Date(claim.at).toISOString()}, ` + `post ${new Date(seatAt).toISOString()}). A shift dispatched work and did not record it, so every ` + 'number the post states — 在飞 / 队列 / 决策箱 / the round number — describes a round that has since ' + @@ -21256,12 +21308,13 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // would put an accusation of staleness on a post that correctly says it is // not running a shift. for (const { issue, rows } of seatMarkers.values()) { - const { lane } = seatLane(issue); + const { lane, seat } = seatLane(issue); if (!lane) continue; + // Free-claim multi-seat: the post is compared against ITS seat's claims only. const stale = h38SeatPostStale( issue, seatPostLastEventMs(issue, rows), - newestLaneClaim(lane, seen.values(), commentCache), + newestLaneClaim(lane, seen.values(), commentCache, seat), ); if (stale) findings.push([issue, 'H38', stale]); } @@ -28245,6 +28298,43 @@ async function selfTest() { t('H38 lane: an em-dash claim stays invisible (malformed by ruling)', newestLaneClaim('domain:services', [laneCard(1, ['pm:dispatched', 'domain:services'])], new Map([[1, [{ body: 'Claim — the skills seat', created_at: '2026-08-30T00:00:00Z' }]]])), null); t('H38 lane: an unreadable claim stamp is not an ordering', newestLaneClaim('domain:services', [laneCard(1, ['pm:dispatched', 'domain:services'])], new Map([[1, [{ body: 'Claim: x', created_at: 'nope' }]]])), null); + // Free-claim multi-seat: several PMs on one lane, one post per seat, and a + // post is compared only against claims of ITS seat number. The title suffix + // and the claim's `Seat:` line are the two readings; both default to seat 1. + const seat2Title = '[PM seat] domain:services · seat 2 — 🟢 os-b (session_y)'; + t('H38 seat: a `· seat N` title suffix parses as the LANE, not foreign', seatLane(seat38(seat2Title)).lane, 'domain:services'); + t('H38 seat: …and is not foreign', seatLane(seat38(seat2Title)).foreign, false); + t('H38 seat: …and carries the seat number', seatLane(seat38(seat2Title)).seat, 2); + t('H38 seat: a bare title is seat 1', seatLane(seat38()).seat, 1); + t('H38 seat: a numbered seat on a SIBLING board is still foreign', seatLane(seat38('[PM seat] domain:devx · seat 3 @ objectui — 🟢 os-b')).foreign, true); + t('H38 seat: …and still carries its number', seatLane(seat38('[PM seat] domain:devx · seat 3 @ objectui — 🟢 os-b')).seat, 3); + t('H38 seat: a numbered seat still buys the H32 comment fetch', h32NeedsSeatComments(seat38(seat2Title)), true); + t('H38 seat: an unparseable title has no seat', seatLane(seat38('not a seat title')).seat, null); + t('H38 seat: a claim with no `Seat:` line is seat 1', claimSeatNumber('Claim: x\nBranch: `claude/issue-1-a`'), 1); + t('H38 seat: `Seat: domain:services#2` reads 2', claimSeatNumber('Claim: x\nSeat: domain:services#2'), 2); + t('H38 seat: …backticked too', claimSeatNumber('Claim: x\nSeat: `domain:services#2`'), 2); + t('H38 seat: …and behind a bullet then a blockquote, bold key', claimSeatNumber('- > **Seat**: domain:services#4'), 4); + // ⛔ Blockquote-then-bullet is out, exactly as `claimedBranches` pins for `Branch:` — invisible, so absent, so seat 1. + t('H38 seat: a `> - Seat:` line is not read (measured shape, same as `Branch:`)', claimSeatNumber('> - Seat: domain:services#4'), 1); + t('H38 seat: a `Seat:` line naming no number is unreadable, not seat 1', claimSeatNumber('Claim: x\nSeat: domain:services'), null); + t('H38 seat: prose mentioning a seat is not a `Seat:` line', claimSeatNumber('the seat: domain:services#2 is busy'), 1); + const seatRow = (seatLine, iso) => [{ body: `Claim: PM loop round 1\nSession: \`session_x\`\n${seatLine}`, created_at: iso }]; + const oneCard = [laneCard(1, ['pm:dispatched', 'domain:services'])]; + const seat2Claim = new Map([[1, seatRow('Seat: `domain:services#2`', '2026-08-30T00:00:00Z')]]); + const seat1Claim = new Map([[1, seatRow('Branch: `claude/issue-1-a`', '2026-08-30T00:00:00Z')]]); + t('H38 seat: a SAME-seat newer claim is found (seat 2 post, seat 2 claim)', newestLaneClaim('domain:services', oneCard, seat2Claim, 2).number, 1); + t('H38 seat: ANOTHER seat\'s claim is not counted (seat 1 post, seat 2 claim)', newestLaneClaim('domain:services', oneCard, seat2Claim, 1), null); + t('H38 seat: an absent `Seat:` line is seat 1 — the seat 1 post sees it', newestLaneClaim('domain:services', oneCard, seat1Claim, 1).number, 1); + t('H38 seat: …and the seat 2 post does not', newestLaneClaim('domain:services', oneCard, seat1Claim, 2), null); + t('H38 seat: the default seat argument is 1, so every pre-existing call reads as before', newestLaneClaim('domain:services', laneIssues, laneComments).number, 13398); + 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); + 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')); + // End to end on the post: a seat-2 post is judged against the claim handed to it, and names its seat. + 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'); + t('H38 seat: …and the finding names the seat', h38row(seat38(seat2Title), SEAT_AT_38, claim38(13398, '2026-08-30T07:30:00Z')).includes('seat 2'), true); + t('H38 seat: …while the bare post names seat 1', h38row(seat38(), SEAT_AT_38, claim38(13398, '2026-08-30T07:30:00Z')).includes('seat 1'), true); + t('H38 seat: a seat-2 post with no same-seat claim -> clean', h38SeatPostStale(seat38(seat2Title), SEAT_AT_38, newestLaneClaim('domain:services', oneCard, seat1Claim, 2)), null); + // -- H39 — the closed `pm:*` residue census (#13526, report-only) --------- // // The card's central fence, restated as an assertion rather than as prose: