From 3785d616fa5def3bb7e81f71217aba9737299132 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 13:23:58 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(pm):=20make=20close-out=20debt=20mecha?= =?UTF-8?q?nical=20=E2=80=94=20same-round=20verdict=20clause,=20H8=20merge?= =?UTF-8?q?d-but-dispatched=20detector,=20seat-title=20liveness=20patrol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three deliverables per the maintainer-pulled card: 1. dispatch-runbook.md gains a same-round verdict-execution clause: a verdict executes in the round that produces it; record-and-walk-away is prohibited; an actor lacking authority names who has it and moves the state to their inbox in the same round. 2. check-half-states.mjs gains H8 (report-only): a card's delivering PR is MERGED while the card still carries pm:dispatched. Exported predicate follows H7's shape and reuses its code-stripped extractors; live mode reads a bounded window of recently merged PRs; self-test extended 79 -> 91 cases. 3. dispatch-runbook.md gains the triage-Routine seat-title liveness patrol (scan green-titled pm:seat posts against the incumbent's own recent activity; silent beyond 24h => demote to vacant with an evidence comment, title + assignee in the same stroke), with the seat-post-protocol liveness bullet amended to name both paths. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018WuTtyckQa1VcXwgd52JpN --- .../references/dispatch-runbook.md | 38 ++++ .../references/seat-post-protocol.md | 10 +- scripts/pm/check-half-states.mjs | 184 +++++++++++++++++- 3 files changed, 222 insertions(+), 10 deletions(-) diff --git a/.claude/skills/pm-dispatch/references/dispatch-runbook.md b/.claude/skills/pm-dispatch/references/dispatch-runbook.md index 84d823b148..eb64c07454 100644 --- a/.claude/skills/pm-dispatch/references/dispatch-runbook.md +++ b/.claude/skills/pm-dispatch/references/dispatch-runbook.md @@ -65,6 +65,25 @@ issue 编号。 一),⛔ 不定级、不改标签 —— 定级单一生产者是分诊席。唯一例外:skills 车道的 finding 由该席自分诊(座位贴协议既有裁定),全仓分诊轮跳过该车道,防双生产者。 +## 座位贴活性巡查(分诊轮常设项;维护者 2026-08-14) + +出处:「结论当轮执行」同批裁定;实测形状是七条座位贴在一日内被逐条手工纠正 —— +停摆/交接写在评论里,标题仍挂 🟢(其中两条纠正本身还是竞态误纠的再纠正),那七次 +手工纠正即本巡查的先例与模板。分诊 Routine 每 fire 附带一次: + +- **扫描面**:`label:pm:seat` 列表页上标题挂 `🟢 ` 的贴(标题即状态 + 板,列表页一眼可得);`🟢 Routine` 座位以调度器读数为准(`last_fired` / + `next_run`),不入本巡查。 +- **活性判据只认在任者自己的产出**:该座位近期的自有评论/认领/贴正文编辑(作者字 + 段与编辑时间戳是平台盖章的硬读数),⛔ 不算别人发给它的跨座位通知与收件评论 —— + 收到消息不是活着的证据。 +- **阈值与既有回收线同一条:>24h 无自有产出即静默超限**,当场降级:标题改 + `⏳ vacant` + 摘 assignee **同笔**(三元同笔纪律),并留证据评论(最后一次自有产 + 出的时间戳与链接、判定时刻)。在飞认领照认领协议由原认领者跟完,⛔ 不代收不代垂。 +- 本巡查是「无心跳,惰性判定」的常设出口补充:惰性判定只在接管冲突时触发,本巡查 + 把「标题挂 🟢 而人已下班」的窗口压到一个班次以内;座位贴协议的三元同笔与回收细 + 则照旧(见 `seat-post-protocol.md`)。 + ## 落卡与裁决记录细则(维护者 2026-08-13) 出处原话(逐字,未译):「还有很多我发现分诊或者决裁后没有改状态,这个也是问题」… @@ -90,6 +109,25 @@ issue 编号。 前提 / 具体问题 / 选项 / 推荐 / 相关单与 PR;标签就是维护者的收件箱,答复后按上 面四件录裁。 +## 结论当轮执行(维护者 2026-08-14) + +出处:维护者问「合并即关账」与「下班即改贴」是否应固化入技能;座位评估两条规则文 +本已在、当日失效全为执行失效;维护者裁(逐字,未译)「现在开工派发」。实测失效形 +状(一日之内):十余张 rc 复测卡带着「建议关闭」评论躺置多日无人执行,一张几十行 +的结论表放置三天未逐行落账;若干张卡的交付 PR 已合并而 `pm:dispatched` 未摘。 + +- **结论产生的那一轮就执行它。** 复测得出「建议关闭」(或任何复测/审计/裁决结论) + 而不在同一轮完成对应的关闭/换标写入,本身就是一个半状态,与半状态家族同列 —— + 记录后离场(record-and-walk-away)⛔ 禁止。结论表同规:产出表的那一轮就是逐行落 + 账的那一轮;表不是交付物,落账才是。 +- **无权执行 ⇒ 点名有权者,并把状态送进对方收件箱。** 座位对某个写入无权(维护者 + 专属、他座位车道)时,结论必须写明由谁执行,并**当轮**完成状态搬运(换标进对方 + 的收件箱视图/决策箱),⛔ 不许只留一条评论等人路过 —— 评论不是任何人的收件箱, + 标签才是。 +- **机械半边**:「交付 PR 已合并而卡仍挂 `pm:dispatched`」的形状由半状态巡查的 + report-only 项浮出(细节以脚本头为权威);巡查旗标是兜底,当轮执行才是修法 —— + 旗标出现即说明有一轮已经欠账。 + ## 云卡(`mode:cloud`)四课 一次性云卡用 `create_session`,⛔ 不用 create_trigger+fire(维护者 2026-08-07 拍 diff --git a/.claude/skills/pm-dispatch/references/seat-post-protocol.md b/.claude/skills/pm-dispatch/references/seat-post-protocol.md index 81a54eec7b..9f347941e1 100644 --- a/.claude/skills/pm-dispatch/references/seat-post-protocol.md +++ b/.claude/skills/pm-dispatch/references/seat-post-protocol.md @@ -33,10 +33,12 @@ - **残余竞态纪律**(唯一剩下的多写手场景是空缺座位争用):动手前重新 fetch 贴正 文;审计评论**时间戳先到先得**;写后回读。新增座位贴(拆域)同样先查 `pm:seat` 索引再立贴。 -- **无心跳,活性惰性判定**:座位不定期报活;只在**接管冲突**时评估一次 —— - Routine 座位查调度器(`last_fired` / `next_run`),会话座位查最近一条产出评论的 - 时间戳,**>24h 无产出即可回收**(改贴正文 + 审计评论)。子树/批次里在飞的认领仍 - 由原认领者跟完。 +- **无心跳,活性判定两条路**:座位不定期报活。惰性半边在**接管冲突**时评估一次 + —— Routine 座位查调度器(`last_fired` / `next_run`),会话座位查最近一条产出评 + 论的时间戳,**>24h 无产出即可回收**(改贴正文 + 审计评论);常设半边是分诊 + Routine 每 fire 附带的座位贴活性巡查,按同一 >24h 判据主动把静默的 🟢 贴降级 + `⏳ vacant`(细则见 `dispatch-runbook.md`)。子树/批次里在飞的认领仍由原认领者 + 跟完。 - **每轮巡检核对自己的座位贴正文**:协议或结构升级会迁移状态,自查一贴成本为零; 发现不符当场改正文 + 审计评论。Routine 座位的收尾简报也落自己的座位贴(它是下一 轮自退守卫的读数)。 diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 326c8435fd..9ab6d9982a 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -67,9 +67,26 @@ * carrying a closing keyword bound to that same `#N` — contradictory by * construction. `Part of` is the protocol saying "merging this must NOT * close the card"; a closing keyword is GitHub being told it must. - * GitHub wins, silently, on merge. This is the only item over PULL + * GitHub wins, silently, on merge. This was the first item over PULL * REQUESTS rather than issues, because the PR body is the surface where * the fact is still fixable — see the next section. + * H8 a card's delivering PR is MERGED while the card still carries + * `pm:dispatched` — the merge happened and its paired write (drop + * `pm:dispatched`, re-grade the remainder) never did (#8683). The + * delivering relation is read from merged PR bodies with H7's own + * code-stripped extractors: `Part of #N` or a closing keyword bound to + * `#N`. The measured shape is the `Part of` one — a partial delivery + * merges, GitHub correctly leaves the card open, and the human half of + * the close-out never lands; a theme-seat pre-work audit found five + * cards in that state at once. The closing-keyword arm is kept because + * an OPEN dispatched card named by a merged PR's closing keyword is a + * half-state no matter which mechanism failed (auto-close raced, card + * reopened without re-grade, keyword edited in after merge) — the sweep + * does not need to know which. Live mode feeds H8 a bounded window of + * recently merged PRs (see `listRecentlyMergedPullRequests`), so it is + * a patrol accelerator, never an exhaustive audit: a delivery that has + * aged out of the window is invisible here, and the finding clears when + * the paired write lands, not when the PR ages out. * * ## The close mechanism, measured (#8293) * @@ -414,6 +431,54 @@ export function h7PartOfWithClosingKeyword(pr) { .join('; '); } +// --------------------------------------------------------------------------- +// H8 — delivering PR merged, card still `pm:dispatched` (#8683). +// +// Pure over the shapes the sweep already consumes: the REST issue (labels + +// number) plus a list of PRs from the same `/pulls` surface H7 reads (`number`, +// `body`, plus `merged_at`, which every `/pulls` row carries). No new API +// layer: the delivering relation is read from the PR BODY with the SAME +// code-stripped extractors H7 pins, so everything measured about GitHub's +// reference parsing (#8293 readings 1–5) covers this predicate too. +// --------------------------------------------------------------------------- + +/** + * H8 — null when clean, else the finding sentence. + * + * A PR "delivers" card N when its body declares `Part of #N` or binds a + * closing keyword to `#N` (both read through `stripMarkdownCode`, so a body + * QUOTING either spelling in backticks does not deliver — the same + * careful-author protection H7 needs). Only a PR with `merged_at` set counts: + * a closed-unmerged PR is an abandoned attempt, not a delivery, and flagging + * it would demand a paired write for work that never landed. + * + * The issue must still carry `pm:dispatched`; the sweep only lists OPEN + * issues, so the closed case never reaches this predicate. Bound per issue + * number exactly like H7 — a merged PR delivering card A says nothing about + * card B. + */ +export function h8MergedPrStillDispatched(issue, mergedPrs) { + if (!labelNames(issue).includes('pm:dispatched')) return null; + const n = String(issue.number); + const delivering = []; + for (const pr of mergedPrs ?? []) { + if (!pr?.merged_at) continue; + const body = pr.body ?? ''; + if (partOfTargets(body).has(n) || closingKeywordTargets(body).has(n)) { + delivering.push(pr); + } + } + if (delivering.length === 0) return null; + const list = delivering + .map((p) => `#${p.number} (merged ${String(p.merged_at).slice(0, 10)})`) + .join(', '); + return ( + `delivering PR ${list} is MERGED but the card still carries \`pm:dispatched\` — ` + + `the merge's paired write never landed. Drop \`pm:dispatched\` and re-grade the ` + + `remainder (re-queue, close, or block the un-delivered half) in the same stroke.` + ); +} + // --------------------------------------------------------------------------- // Transport prerequisite — the classifier (pure) and the probe that feeds it. // @@ -776,10 +841,11 @@ async function sweep() { const findings = []; const seen = new Map(); const seenPrs = new Map(); + const seenMerged = new Map(); try { - await sweepInto(findings, seen, seenPrs); + await sweepInto(findings, seen, seenPrs, seenMerged); } catch (err) { - err.sweptSoFar = seen.size + seenPrs.size; + err.sweptSoFar = seen.size + seenPrs.size + seenMerged.size; throw err; } @@ -788,8 +854,8 @@ async function sweep() { console.log(` ${code} #${issue.number} ${msg}\n ${issue.html_url}`); } console.log( - `check-half-states: swept ${seen.size} open pm-labeled issue(s) and ${seenPrs.size} open PR(s) ` + - `in ${OWNER_REPO} — ${findings.length} half-state(s) found. ` + + `check-half-states: swept ${seen.size} open pm-labeled issue(s), ${seenPrs.size} open PR(s) ` + + `and ${seenMerged.size} recently-merged PR(s) in ${OWNER_REPO} — ${findings.length} half-state(s) found. ` + `Report-only: findings are patrol input, not a gate verdict.`, ); } @@ -804,7 +870,31 @@ async function listOpenPullRequests() { return out; } -async function sweepInto(findings, seen, seenPrs) { +/** + * The merged-PR window H8 reads: the most recently UPDATED closed PRs, merged + * ones only, capped at two pages (≤200 closed rows). The cap is a quota + * decision, and its consequence is H8's stated boundary — a delivery older + * than the window is invisible to the sweep. At this repo's measured pace + * (~18 merges to main per working day) two pages reach back well past the + * longest measured unexecuted-verdict latency (9 days), and a finding stays + * visible every round until the paired write lands, because the card's + * `pm:dispatched` is what clears it, not the PR's age. `sort=updated` rather + * than creation order so a long-lived PR that merges late is still in the + * window when it matters. + */ +async function listRecentlyMergedPullRequests() { + const out = []; + for (let page = 1; page <= 2; page++) { + const batch = await rest( + `/repos/${OWNER_REPO}/pulls?state=closed&sort=updated&direction=desc&per_page=100&page=${page}`, + ); + out.push(...batch.filter((p) => p.merged_at)); + if (batch.length < 100) break; + } + return out; +} + +async function sweepInto(findings, seen, seenPrs, seenMerged) { for (const label of ['pm:dispatched', 'pm:queue', 'pm:blocked', 'pm:seat']) { for (const issue of await listIssues(label)) seen.set(issue.number, issue); } @@ -848,6 +938,17 @@ async function sweepInto(findings, seen, seenPrs) { const contradiction = h7PartOfWithClosingKeyword(pr); if (contradiction) findings.push([pr, 'H7', contradiction]); } + + // H8 — the merged-PR side. One bounded listing (see the helper's window + // note), matched against the still-open `pm:dispatched` cards the label + // pages already collected — no per-card fetch, so the quota cost is the + // two listing pages regardless of board size. + for (const pr of await listRecentlyMergedPullRequests()) seenMerged.set(pr.number, pr); + const mergedWindow = [...seenMerged.values()]; + for (const issue of seen.values()) { + const stale = h8MergedPrStillDispatched(issue, mergedWindow); + if (stale) findings.push([issue, 'H8', stale]); + } } // --------------------------------------------------------------------------- @@ -1011,6 +1112,77 @@ function selfTest() { ); t('H7: a fenced-only keyword is not a finding', h7PartOfWithClosingKeyword(pr('Part of #5\n\n```\nFixes #5\n```')), null); + // -- H8: delivering PR merged, card still `pm:dispatched` (#8683) ---------- + // The measured shape: a `Part of` PR merges, GitHub correctly leaves the + // card open, and the paired write (drop `pm:dispatched`, re-grade the + // remainder) never lands — a theme-seat pre-work audit found five cards in + // that state at once. Fixtures reuse H7's extractor pins, so the stripping + // and per-number-binding measurements carry over rather than being re-proved. + const dispatched = (n) => ({ ...issue(['pm:dispatched'], ['os-help']), number: n }); + const mergedPr = (number, body, merged_at = '2026-08-13T10:00:00Z') => ({ number, body, merged_at }); + + t( + 'H8: merged Part-of PR + still dispatched -> finding', + typeof h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'Part of #4321 — the non-destructive half only.')]), + 'string', + ); + t( + 'H8: …and the finding names the delivering PR', + h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'Part of #4321')]).includes('#4400'), + true, + ); + t( + 'H8: …and prescribes the paired write, not just the fact', + h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'Part of #4321')]).includes('pm:dispatched'), + true, + ); + // The closing-keyword arm: an OPEN dispatched card named by a merged PR's + // closing keyword is a half-state whichever mechanism failed (see header). + t( + 'H8: merged closing-keyword PR + still-open dispatched card -> finding', + typeof h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'Fixes #4321')]), + 'string', + ); + t( + 'H8: card without pm:dispatched is out of scope', + h8MergedPrStillDispatched({ ...issue(['pm:queue'], ['os-help']), number: 4321 }, [mergedPr(4400, 'Part of #4321')]), + null, + ); + // Closed-unmerged is an abandoned attempt, not a delivery: demanding the + // paired write for work that never landed would be a phantom finding. + t( + 'H8: closed-unmerged PR is not a delivery', + h8MergedPrStillDispatched(dispatched(4321), [{ number: 4400, body: 'Part of #4321', merged_at: null }]), + null, + ); + // Bound per issue number, exactly like H7. + t( + 'H8: merged PR delivering a DIFFERENT card -> clean', + h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'Part of #9999\n\nFixes #8888')]), + null, + ); + // Strip reuse: a body QUOTING the spelling in backticks does not deliver — + // the same careful-author protection H7's reading 4 measured. + t( + 'H8: reference inside backticks does not deliver', + h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'the dispatch asked for `Fixes #4321` and `Part of #4321`')]), + null, + ); + // A plain prose mention is neither declaration: only the two protocol + // spellings establish the delivering relation. + t( + 'H8: plain prose mention does not deliver', + h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'follow-up to #4321, measurement only')]), + null, + ); + t( + 'H8: two merged deliverers -> both named', + h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'Part of #4321'), mergedPr(4500, 'Fixes #4321')]).includes('#4500'), + true, + ); + t('H8: empty merged window -> clean', h8MergedPrStillDispatched(dispatched(4321), []), null); + t('H8: missing merged window -> clean', h8MergedPrStillDispatched(dispatched(4321), undefined), null); + // -- transport prerequisite (#7412) --------------------------------------- // The three container classes are REAL measurements, not invented fixtures; // each names where it was taken, so a future transport change can be checked From e75de0a890ced5728dba1f8f7f6a8947656a46dd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 14:41:12 +0000 Subject: [PATCH 2/2] =?UTF-8?q?refactor(pm):=20compress=20prose=20per=20ma?= =?UTF-8?q?intainer=20ruling=20=E3=80=8C8685=20=E5=AD=97=E5=A4=AA=E5=A4=9A?= =?UTF-8?q?=E4=BA=86=E3=80=8D=20=E2=80=94=20halve=20the=20two=20runbook=20?= =?UTF-8?q?sections,=20trim=20H8=20module=20narrative?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Normative bullets, ⛔ marks and load-bearing one-liners kept; provenance narratives cut to one line (detail lives on the card and PR); H8 boundary now stated once in the header and once at the window helper; test cases and their comments untouched (self-test stays 91). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018WuTtyckQa1VcXwgd52JpN --- .../references/dispatch-runbook.md | 54 ++++++------- scripts/pm/check-half-states.mjs | 77 +++++++------------ 2 files changed, 48 insertions(+), 83 deletions(-) diff --git a/.claude/skills/pm-dispatch/references/dispatch-runbook.md b/.claude/skills/pm-dispatch/references/dispatch-runbook.md index eb64c07454..0b76db8a21 100644 --- a/.claude/skills/pm-dispatch/references/dispatch-runbook.md +++ b/.claude/skills/pm-dispatch/references/dispatch-runbook.md @@ -67,22 +67,18 @@ issue 编号。 ## 座位贴活性巡查(分诊轮常设项;维护者 2026-08-14) -出处:「结论当轮执行」同批裁定;实测形状是七条座位贴在一日内被逐条手工纠正 —— -停摆/交接写在评论里,标题仍挂 🟢(其中两条纠正本身还是竞态误纠的再纠正),那七次 -手工纠正即本巡查的先例与模板。分诊 Routine 每 fire 附带一次: - -- **扫描面**:`label:pm:seat` 列表页上标题挂 `🟢 ` 的贴(标题即状态 - 板,列表页一眼可得);`🟢 Routine` 座位以调度器读数为准(`last_fired` / - `next_run`),不入本巡查。 -- **活性判据只认在任者自己的产出**:该座位近期的自有评论/认领/贴正文编辑(作者字 - 段与编辑时间戳是平台盖章的硬读数),⛔ 不算别人发给它的跨座位通知与收件评论 —— - 收到消息不是活着的证据。 -- **阈值与既有回收线同一条:>24h 无自有产出即静默超限**,当场降级:标题改 - `⏳ vacant` + 摘 assignee **同笔**(三元同笔纪律),并留证据评论(最后一次自有产 - 出的时间戳与链接、判定时刻)。在飞认领照认领协议由原认领者跟完,⛔ 不代收不代垂。 -- 本巡查是「无心跳,惰性判定」的常设出口补充:惰性判定只在接管冲突时触发,本巡查 - 把「标题挂 🟢 而人已下班」的窗口压到一个班次以内;座位贴协议的三元同笔与回收细 - 则照旧(见 `seat-post-protocol.md`)。 +出处:「结论当轮执行」同批裁定;先例是七条 🟢 贴一日内被逐条手工纠正。分诊 +Routine 每 fire 附带一次: + +- **扫描面**:`label:pm:seat` 列表页上标题挂 `🟢 ` 的贴; + `🟢 Routine` 座位以调度器读数为准,不入本巡查。 +- **判据只认在任者自己的产出**(自有评论/认领/贴正文编辑 —— 作者与时间戳是平台盖 + 章的硬读数),⛔ 不算别人发给它的跨座位通知 —— 收到消息不是活着的证据。 +- **>24h 无自有产出(与既有回收线同一条)⇒ 当场降级**:标题改 `⏳ vacant` + 摘 + assignee **同笔**,留证据评论(最后自有产出的时间戳与链接);在飞认领照认领协议 + 由原认领者跟完。 +- 惰性判定(接管冲突时)照旧,本巡查是其常设出口,把「标题挂 🟢 而人已下班」的窗 + 口压到一个班次以内(三元同笔与回收细则见 `seat-post-protocol.md`)。 ## 落卡与裁决记录细则(维护者 2026-08-13) @@ -111,22 +107,16 @@ issue 编号。 ## 结论当轮执行(维护者 2026-08-14) -出处:维护者问「合并即关账」与「下班即改贴」是否应固化入技能;座位评估两条规则文 -本已在、当日失效全为执行失效;维护者裁(逐字,未译)「现在开工派发」。实测失效形 -状(一日之内):十余张 rc 复测卡带着「建议关闭」评论躺置多日无人执行,一张几十行 -的结论表放置三天未逐行落账;若干张卡的交付 PR 已合并而 `pm:dispatched` 未摘。 - -- **结论产生的那一轮就执行它。** 复测得出「建议关闭」(或任何复测/审计/裁决结论) - 而不在同一轮完成对应的关闭/换标写入,本身就是一个半状态,与半状态家族同列 —— - 记录后离场(record-and-walk-away)⛔ 禁止。结论表同规:产出表的那一轮就是逐行落 - 账的那一轮;表不是交付物,落账才是。 -- **无权执行 ⇒ 点名有权者,并把状态送进对方收件箱。** 座位对某个写入无权(维护者 - 专属、他座位车道)时,结论必须写明由谁执行,并**当轮**完成状态搬运(换标进对方 - 的收件箱视图/决策箱),⛔ 不许只留一条评论等人路过 —— 评论不是任何人的收件箱, - 标签才是。 -- **机械半边**:「交付 PR 已合并而卡仍挂 `pm:dispatched`」的形状由半状态巡查的 - report-only 项浮出(细节以脚本头为权威);巡查旗标是兜底,当轮执行才是修法 —— - 旗标出现即说明有一轮已经欠账。 +出处:维护者裁(逐字,未译)「现在开工派发」;当日实测是成批「建议关闭」复测结论 +与结论表躺置多日无人执行。 + +- **结论产生的那一轮就执行它。** 复测/审计/裁决得出结论而不在同一轮完成对应的关 + 闭/换标写入,本身就是半状态,与半状态家族同列 —— 记录后离场 + (record-and-walk-away)⛔ 禁止;结论表同规,表不是交付物,落账才是。 +- **无权执行 ⇒ 点名有权者,并当轮把状态搬进对方收件箱**(换标进对方的收件箱视图/ + 决策箱),⛔ 不许只留一条评论等人路过 —— 评论不是任何人的收件箱,标签才是。 +- **机械半边**:「交付 PR 已合并而卡仍挂 `pm:dispatched`」由半状态巡查的 + report-only 项浮出(细节以脚本头为权威);旗标是兜底,当轮执行才是修法。 ## 云卡(`mode:cloud`)四课 diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 9ab6d9982a..9ce9f79e7f 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -71,22 +71,15 @@ * REQUESTS rather than issues, because the PR body is the surface where * the fact is still fixable — see the next section. * H8 a card's delivering PR is MERGED while the card still carries - * `pm:dispatched` — the merge happened and its paired write (drop - * `pm:dispatched`, re-grade the remainder) never did (#8683). The - * delivering relation is read from merged PR bodies with H7's own - * code-stripped extractors: `Part of #N` or a closing keyword bound to - * `#N`. The measured shape is the `Part of` one — a partial delivery - * merges, GitHub correctly leaves the card open, and the human half of - * the close-out never lands; a theme-seat pre-work audit found five - * cards in that state at once. The closing-keyword arm is kept because - * an OPEN dispatched card named by a merged PR's closing keyword is a - * half-state no matter which mechanism failed (auto-close raced, card - * reopened without re-grade, keyword edited in after merge) — the sweep - * does not need to know which. Live mode feeds H8 a bounded window of - * recently merged PRs (see `listRecentlyMergedPullRequests`), so it is - * a patrol accelerator, never an exhaustive audit: a delivery that has - * aged out of the window is invisible here, and the finding clears when - * the paired write lands, not when the PR ages out. + * `pm:dispatched` — the merge's paired write (drop the label, re-grade + * the remainder) never landed (#8683). Delivery is read from merged PR + * bodies with H7's code-stripped extractors (`Part of #N`, or a closing + * keyword bound to `#N` — either way an OPEN dispatched card named by a + * merged PR is a half-state, whichever mechanism failed). Live mode + * feeds H8 a bounded window of recently merged PRs, so it is a patrol + * accelerator, never an exhaustive audit: a delivery older than the + * window is invisible, and the finding clears when the paired write + * lands, not when the PR ages out. * * ## The close mechanism, measured (#8293) * @@ -434,28 +427,19 @@ export function h7PartOfWithClosingKeyword(pr) { // --------------------------------------------------------------------------- // H8 — delivering PR merged, card still `pm:dispatched` (#8683). // -// Pure over the shapes the sweep already consumes: the REST issue (labels + -// number) plus a list of PRs from the same `/pulls` surface H7 reads (`number`, -// `body`, plus `merged_at`, which every `/pulls` row carries). No new API -// layer: the delivering relation is read from the PR BODY with the SAME -// code-stripped extractors H7 pins, so everything measured about GitHub's -// reference parsing (#8293 readings 1–5) covers this predicate too. +// Pure over the shapes the sweep already consumes (REST issue + `/pulls` +// rows), reusing H7's code-stripped extractors so the measured reference- +// parser behavior (#8293) carries over. No new API layer. // --------------------------------------------------------------------------- /** * H8 — null when clean, else the finding sentence. * * A PR "delivers" card N when its body declares `Part of #N` or binds a - * closing keyword to `#N` (both read through `stripMarkdownCode`, so a body - * QUOTING either spelling in backticks does not deliver — the same - * careful-author protection H7 needs). Only a PR with `merged_at` set counts: - * a closed-unmerged PR is an abandoned attempt, not a delivery, and flagging - * it would demand a paired write for work that never landed. - * - * The issue must still carry `pm:dispatched`; the sweep only lists OPEN - * issues, so the closed case never reaches this predicate. Bound per issue - * number exactly like H7 — a merged PR delivering card A says nothing about - * card B. + * closing keyword to `#N`, read through `stripMarkdownCode` (a body QUOTING + * either spelling in backticks does not deliver). Only `merged_at`-set PRs + * count — closed-unmerged is an abandoned attempt, not a delivery. Bound per + * issue number exactly like H7. */ export function h8MergedPrStillDispatched(issue, mergedPrs) { if (!labelNames(issue).includes('pm:dispatched')) return null; @@ -871,16 +855,12 @@ async function listOpenPullRequests() { } /** - * The merged-PR window H8 reads: the most recently UPDATED closed PRs, merged - * ones only, capped at two pages (≤200 closed rows). The cap is a quota - * decision, and its consequence is H8's stated boundary — a delivery older - * than the window is invisible to the sweep. At this repo's measured pace - * (~18 merges to main per working day) two pages reach back well past the - * longest measured unexecuted-verdict latency (9 days), and a finding stays - * visible every round until the paired write lands, because the card's - * `pm:dispatched` is what clears it, not the PR's age. `sort=updated` rather - * than creation order so a long-lived PR that merges late is still in the - * window when it matters. + * The merged-PR window H8 reads: most recently UPDATED closed PRs, merged + * ones only, capped at two pages — a quota decision whose consequence is + * H8's stated boundary (a delivery older than the window is invisible). At + * ~18 merges/day two pages reach well past the longest measured + * unexecuted-verdict latency; `sort=updated` so a long-lived PR that merges + * late is still in the window when it matters. */ async function listRecentlyMergedPullRequests() { const out = []; @@ -939,10 +919,8 @@ async function sweepInto(findings, seen, seenPrs, seenMerged) { if (contradiction) findings.push([pr, 'H7', contradiction]); } - // H8 — the merged-PR side. One bounded listing (see the helper's window - // note), matched against the still-open `pm:dispatched` cards the label - // pages already collected — no per-card fetch, so the quota cost is the - // two listing pages regardless of board size. + // H8 — one bounded merged-PR listing (window note at the helper), matched + // against the already-collected open `pm:dispatched` cards; no per-card fetch. for (const pr of await listRecentlyMergedPullRequests()) seenMerged.set(pr.number, pr); const mergedWindow = [...seenMerged.values()]; for (const issue of seen.values()) { @@ -1113,11 +1091,8 @@ function selfTest() { t('H7: a fenced-only keyword is not a finding', h7PartOfWithClosingKeyword(pr('Part of #5\n\n```\nFixes #5\n```')), null); // -- H8: delivering PR merged, card still `pm:dispatched` (#8683) ---------- - // The measured shape: a `Part of` PR merges, GitHub correctly leaves the - // card open, and the paired write (drop `pm:dispatched`, re-grade the - // remainder) never lands — a theme-seat pre-work audit found five cards in - // that state at once. Fixtures reuse H7's extractor pins, so the stripping - // and per-number-binding measurements carry over rather than being re-proved. + // Fixtures reuse H7's extractor pins, so the stripping and per-number- + // binding measurements carry over rather than being re-proved. const dispatched = (n) => ({ ...issue(['pm:dispatched'], ['os-help']), number: n }); const mergedPr = (number, body, merged_at = '2026-08-13T10:00:00Z') => ({ number, body, merged_at });