diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index 9533924d50d..4fa3e220100 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -34,7 +34,7 @@ model: opus - 同批 agents 共用一个 scratchpad 目录,自然命名的文件会被彼此静默覆盖。 2. **卡 assignee 归 PM。** 派发原子对已把它设好;共享身份下该字段答不了是谁。 - 你的身份位是认领评论里的分支;仓 CLAUDE.md 的 claim-first 已由 PM 的认领满足。 - - 恒不写卡的 assignee(空也开工,报 `summary`);PR assignee = 卡的:`pr_create` 带 `assignees: [LOGIN]`。 + - 恒不写卡的 assignee(空也开工,报 `summary`);PR ⛔ 不带 `assignees`,人经席位交棒才见 PR。 - 发现与他人在途工作重复,停下报 `blocked`。 - PR 上不是你设置的状态属于另一个 actor:⛔ 永不去纠正;疑问进报告,挡住报 blocked。 - 回退他人的操作(尤其 ready 翻转)永不轮到你;把意外写进 `summary`。 @@ -57,7 +57,7 @@ model: opus - 写工具自读所需变量;⛔ 永不打印环境变量的值,转录是 AGENTS.md 禁落令牌的日志行。 - 拼法不限于 `env`、`printenv`、`set`、`export -p`、`echo $VAR`、`cat /proc/*/environ`。 - 判存在只用不印值形式 `[ -n "${X+x}" ] && echo set`;已印出 ⇒ `deviations` 只点名变量名。 - - 写预算三笔:`git push`、`pr_create`(draft,含 assignee 与 labels)、`os-dev-report` 评论。 + - 写预算三笔:`git push`、`pr_create`(draft,含 labels、⛔ 无 assignees)、`os-dev-report` 评论。 - 卡与线程只走 payload 档(公开仓单卡网页内嵌 JSON,拼写住 platform-readings)或单卡 REST 读。 - 三类发现进报告交席位代立,不 `POST /issues`、⛔ 不静默弃报;PM 去重读数当既有事实用。 - PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 `PATCH`;事后要改的报告点名改法,席位代写。 diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index cfd3db0d57c..0d4178a98b9 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -195,10 +195,10 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - PR 上的标签 = 待维护者审阅,不入六态;与请审同为等人批清单,随获批或撤回判决离开。 - 批准判定单源 = 队列守卫常量 `GOVERNED_APPROVERS`:授权账户 APPROVED 即算,⛔ 不卡 `commit_id`。 - 批准后再推亦不过期;席位落地 = 过落地前检、清标、ready、auto-merge,踢出/变基同法。 -- ③ 中继 `pr_create`/`pr_ready` 命中 Tier H 即向 `GOVERNED_APPROVERS` 中未请/派/审者请审。 -- 作者是审批人 ⇒ 指派它、请其余(请它必 422),否则请全员;速读只点非作者审批人去批。 -- 脱中继开的 PR 无 run 行:交速读前席位恒自读 `requested_reviewers`/`assignees`,⛔ 不只看 run 行。 -- 缺口经中继 `pr_request_reviewers`/`assign` 补,⛔ 不碰 draft 位;ACCEPT 与轮报点名脱中继作者。 +- ③ 请审 = 席位交棒:ACCEPT 与现 head 同形 PASS 俱在案后,中继 `pr_request_reviewers` 请审批人。 +- 作者是审批人 ⇒ `assign` 它、请其余(请它必 422);⛔ 永不派/请席位自身账号;Tier S 不交人。 +- `pr_create` 不请不派;`pr_ready` 只补交棒漏;PASS 后 head 移 ⇒ 回等复核;脱中继 PR 自读请/派。 +- 速读「需要你做的」求批点名 PASS 评论 id+`Head-sha`;无 PASS ⇒ 写 contract review owed 不求批。 - ④ 轮次报告单列 awaiting a human merge。 - skills 车道自有 PR:纯代码面如 `scripts/pm/` 由本席按达档自审(清单不减)后落地。 - 受管面两层:Tier S = `.claude/**` 全树,余皆 Tier H 等人批;S 经达档复核 PASS 后 ready → 入队。 diff --git a/.claude/skills/pm-dispatch/references/contract-review.md b/.claude/skills/pm-dispatch/references/contract-review.md index 3ac662e2809..60fb3d4c5ad 100644 --- a/.claude/skills/pm-dispatch/references/contract-review.md +++ b/.claude/skills/pm-dispatch/references/contract-review.md @@ -31,8 +31,7 @@ 独立性对、PASS/FAIL。 - 同形含首行 `Served-tier:`:值写该面所定档的常量名 `CONTRACT_REVIEW_TIER` 或 `TIER_DEFAULT`; 无此行不成裁决;模板 `node scripts/pm/record-recognisers.mjs --template`,申报面加 `--face declaration`。 -- 同形另带 `Local-runs:` 行:`none`,或 `probe — REASON` 具名唯一本地探针; - 有行必校,缺行旧记录不改判。 +- 同形带 `Local-runs:` 行:`none` 或 `probe — REASON` 具名唯一探针;有行必校,缺行旧记录不改判。 - ① derived judgments 逐项:diff 引出的接受集与公开面变化逐条点名判对错。 - ② semver 定级与 changeset 声明一致;③ 边界旗:dev 挂旗与 `open_questions` 逐旗答复或升级。 - ④ 设计判断欠 schema、governed 规则文本、收窄三面,spec、skills、ADR 皆已在内 ⛔ 不另立面: @@ -40,17 +39,18 @@ 被引代码读而不审;①–④ 合称完整复核,余面同形无 ④,⛔ 无第二条记录。 - 实质 head 各欠一轮完整复核;只落发现的 commit 由同一复核者 delta 复核;无 must-fix 即止。 - 另记作者:PR `user.login` 非 `objectstack-fleet[bot]` ⇒ ③ 注点名该 login(脱中继写),落地前读。 -- 简报从 `node scripts/pm/record-recognisers.mjs --brief-template` 起稿: - 只读条款与输入集内建,⛔ 不删 ⛔ 行。 +- 简报从 `record-recognisers.mjs --brief-template` 起稿:只读条款与输入集内建,⛔ 不删 ⛔ 行。 - 隔离复核子代理暂存全写按所审 PR 命名的 `/pr-/`,⛔ 不读非本轮自写的暂存。 - 独立性对:`Implemented-by:`/`Reviewed-by:` 取值同见 `--template`;子代理记采纳它的席位。 - ④ 恒由起草者之外的 agent 达档跑,起草已达档不免;喂入同上,⛔ 不喂起草者的结论。 - 轮次报告设复审清单专节,与呈裁清单同为强制审计。 -- 落地前检两条,过则 Tier S 入队、Tier H 等人批:① 达档条款②复核 PASS 在案(同形记录)。 +- 请审/求批与落地前检两条,过则 Tier S 入队、Tier H 等人批:① 现 head 同形达档 PASS 在案。 - 例外:纯重生成或只动上列散文面的 head 后移原记录继续管;判据机读已提交树; PR 落 `Regen-provenance:` 行。 - ② PR check 全绿,⛔ 非 required 子集;例外:merge-base 同签名的红不计、按设计而红见 SKILL.md。 - 签名 = 失败步 + 首错行,读 base check runs 的 API ⛔ 不凭口述;主干红止血立单不变。 +- 落地时仍欠复核的 PR 在落地 head 复审,车道席或分诊按 (a)(b)(c) 接;FAIL ⛔ 不修已关 PR。 +- 每条 must-fix 引该记录立新卡、按发现定级;回退仅限主干红(p0,main-red 约定①)或安全边界。 ## 等复核标记 `needs:contract-review` diff --git a/.claude/skills/pm-dispatch/references/dispatch-runbook.md b/.claude/skills/pm-dispatch/references/dispatch-runbook.md index 0286f5b7de5..8b74be4cbaa 100644 --- a/.claude/skills/pm-dispatch/references/dispatch-runbook.md +++ b/.claude/skills/pm-dispatch/references/dispatch-runbook.md @@ -133,11 +133,11 @@ - 披露纪律卡(私密细节不得离开席位会话)也留席内,认领写明;卡线程照计 `batch`。 - 项目的 coordinator 跨席位统一调度卡线程;每席一个线程跑 PM 循环,卡线程即 dev。 - 席位随时把候选卡交 coordinator,附优先级、派发令全文与建议 effort,⛔ 不等自己的槽位。 -- coordinator 按优先级开线程,p1 先;全项目上限由维护者按项目定(`coordinator.md`),满即排队。 - 卡线程交回即释放槽位,席位当即告知 coordinator;审核期开着不占槽,REWORK 补丁轮再占一槽。 - 档位与 effort 席位自推导 ⛔ 不等维护者原话:`--tier` 过路径,MANDATORY 即绑;effort 按难度。 - 派发令写两者与推导(`--tier` 行或难度读数),协调者照令开线程;常设原话有则作 `reason:`。 - 席位留认领文本/派发令/契约复核/ACCEPT/落地;线程实施、开 draft PR、驱动 CI 与可合并。 +- 线程 ⛔ 不做 harness 开 PR 后指派/请审:「审核通过才会推送给我审核」,请审是席位交棒。 - 派发令由 coordinator 写进线程开场消息,`Writes:` 行逐字在内,⛔ 不落共享目录;卡上留指针。 - coordinator 开线程即读线程分支交席位(读不到则线程自报);⛔ 线程不认领、不写 assignee。 - 席位写 assignee+`pm:dispatched`+带该 `Branch:` 的认领;线程核对最新认领指向本分支方开工。 diff --git a/.claude/skills/pm-dispatch/references/execution-duties.md b/.claude/skills/pm-dispatch/references/execution-duties.md index 4663a766fe1..b203241f809 100644 --- a/.claude/skills/pm-dispatch/references/execution-duties.md +++ b/.claude/skills/pm-dispatch/references/execution-duties.md @@ -60,7 +60,7 @@ ## 认领(先认领后动工) - 归属 = session ID,⛔ 不认作者字段;`Account:` 派生自 assignee,不等即半态、持卡席下笔补正。 -- 卡 assignee 归 PM,dev 恒不写;PR assignee = 卡的,接管 ② 同笔换;读者 `is:pr is:open assignee:LOGIN`。 +- 卡 assignee 归 PM,dev 恒不写;PR ⛔ 不派席位账号;在飞 PR 读 `head:claude/` 开 PR 正文点名的卡。 - 部分落地(PR 带 `Refs #N (item k)`,⛔ 不 `Fixes`)即释放:合入同笔回 `pm:queue` + 清 assignee。 - 同笔 `Release:` 行点名已落项与余项;余项整树归他席 ⇒ 点名该席改标,拆分加 `pm:retriage`。 - 派发前按序执行原子对:① Assign @me,并把 `pm:dispatched` 与摘 `pm:queue` 放进同一次标签写入。 diff --git a/.claude/skills/pm-dispatch/references/landing-operations.md b/.claude/skills/pm-dispatch/references/landing-operations.md index 389d713ce7a..cd2c2fe7873 100644 --- a/.claude/skills/pm-dispatch/references/landing-operations.md +++ b/.claude/skills/pm-dispatch/references/landing-operations.md @@ -9,7 +9,8 @@ - 路径肢 = diff 触及契约面 `packages/spec/src/**` 非测试,含 error-code-ledger、`*.zod.ts` 契约 schema。 - 声明肢 = 认领评论声明 `Clause-②: yes`,与路径无关;错误的 `no` 是可审计的假申报。 - 子代理起不来 ⇒ 复核缺席,PR 带 `needs:contract-review` 留 draft 队列外;旁路仅维护者逐次亲审 -- PASS ⇒ ready、auto-merge;FAIL ⇒ 补丁轮;⛔ 免复核不放行。 +- PASS ⇒ Tier S ready、auto-merge;Tier H 经 SKILL.md ③ 交棒请审;FAIL ⇒ 补丁轮;⛔ 免复核不放行。 +- 落地后才见欠复核 ⇒ 按 `contract-review.md` 复审落地 head、发现立新卡,⛔ 不修已关 PR。 - DELIBERATE CORRECTION 红(`check-empty-changeset`)⛔ 不等维护者;确认件皆须点名每个被改 note: - diff 只改 release 文字或注释者:派发席 ACCEPT 确认,逐句引改前与改后,免第二次同 head 复核。 - 其余(含改代码)照全套:同 head 达档复核 PASS 记录即确认,记录逐句判改写句。 @@ -51,7 +52,6 @@ - undraft 撞 429 ⇒ 落地入等 `resets at` 态:该刻记卡,到点一次重试;⛔ 不报阻塞、不自定节奏。 - 落地遭分类器拒 ⇒ 停手、报维护者、卡上记命令与拒因;⛔ 不换拼法、不退回 ccr 绕过。 - 落地评论 = 两个读数的探针表 + 一行状态变更(摘标/关卡/收口),⛔ 不复述 `## 席位核对`。 -- 契约复核 PASS 落地的 PR 到窗口时已 ready 且 auto-merge 在挂,见 `contract-review.md`。 - 转 ready/入队前跑 `check-governed-merges.mjs --pr N`:受管面照两层;>3000 行(含生成物)照 Tier H。 - ⛔ 两层不由席位批准;清标即落地同受此闸,漏判会被队列守卫在 merge group 里拒收。 - 再读 `mergeable_state`:`dirty` ⇒ 先 merge `origin/main` 再挂;生成物在面上按 A 的固定序。 diff --git a/.claude/skills/pm-dispatch/references/lanes/director.md b/.claude/skills/pm-dispatch/references/lanes/director.md index f0c95e6558d..af2a0f9d569 100644 --- a/.claude/skills/pm-dispatch/references/lanes/director.md +++ b/.claude/skills/pm-dispatch/references/lanes/director.md @@ -54,7 +54,7 @@ - 入箱勤务:校验/补全四棱块、速读与 `Prior rulings on this card:` 行;入箱同笔现读前提。 - 线程有 `Ruling:`/RULED 的卡再入箱,该行逐 id 各一句变化;缺行 ⇒ 拒入,`pm:retriage` 回立卡者。 - 一次只开一批:维护者对批 N 回字(一字即可)后,新到才合成批 N+1;续批至清零才收工。 -- 受管 PR 待人批前核四件:席位记录、维护者速读、中继 `pr_request_reviewers` 请审、绿 head。 +- 受管 PR 待人批前核五件:席位记录、速读、现 head PASS、`pr_request_reviewers` 请审、绿 head。 - 缺件即补;⛔ 不入批、不求聊天「同意」;批准与合并恒是维护者的点击,⛔ 不代。 - 呈报前逐卡逐仓重跑 `git grep -n -iE 'TERMS' origin/main -- AGENTS.md docs/adr packages/spec/src`。 - 以协议为准:消费面对齐协议 ⇒ 荐对齐呈批,协议没有的能力荐删;⛔ 不自裁。 diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 200c0e79475..f5c5c23fd58 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -984,7 +984,7 @@ 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. +// fleet-roster's landing-verdict read; H43 and H69 ask it, never re-spell it. import { SEAT_RECORD_MARKER, seatRecordAccepts, seatRecordOf } from './seat-record.mjs'; /** @@ -9883,8 +9883,9 @@ export function renderDanglingReferences(report, { markdown = false } = {}) { } // --------------------------------------------------------------------------- -// H43 — an open GOVERNED-SURFACE PR that has asked NEITHER approver to look -// (#14967). +// H43 — an open GOVERNED-SURFACE PR past the seat's hand-off point (its +// `## 席位核对` ACCEPT and a same-head `## Contract review` PASS are on the +// thread) that has asked NEITHER approver to look (#14967; the timing below). // // ## The state nothing could read, and the question that found it // @@ -9895,7 +9896,8 @@ export function renderDanglingReferences(report, { markdown = false } = {}) { // asked nobody at all. Two of them were ADR records. They had waited two days. // // ⭐ The protocol TEXT is not the defect and this row does not touch it: the -// dispatch skill's ACCEPT path already says to request BOTH accounts, and a +// dispatch skill's hand-off (ACCEPT 之后的路径分叉 ③) says the seat requests +// BOTH accounts once its ACCEPT and a same-head PASS are on the thread, and a // request to `os-zhuang` succeeded during that same sweep. What was missing is // a READING. The three instruments that look at governed PRs each answer a // different question: the queue guard asks whether an AUTHORIZED APPROVAL @@ -9929,6 +9931,38 @@ export function renderDanglingReferences(report, { markdown = false } = {}) { // submitted review by `os-zhuang`, and the two-field // union alone reported it as unasked. // +// ## The timing — a half state only once the hand-off is DUE +// +// The maintainer, verbatim: 「这个是技能的bug,应该是开发完了你审核通过才会推送给我 +// 审核」 and 「没有完成复审就推送给我审核了」. A human sees a PR only after the +// owning seat's `## 席位核对` ACCEPT and, where clause ② applies — on every +// governed path it does, the register's rows are the review's `rules` face — a +// `## Contract review` PASS naming the PR's CURRENT head are both on its +// thread; THEN the request is the seat's hand-off (relay `pr_request_reviewers`, +// `assign` for an author-approver). So this row has three input states, never +// two (#4690): +// +// thread unread (undefined/null) UNJUDGED — quiet, and the coverage +// clause says the thread was not read +// thread read, ACCEPT ∧ same-head PASS DUE — the request is owed +// thread read, either missing BEFORE — nobody is owed a request +// yet; asking would put an unreviewed +// PR in front of a human, the very +// defect the filing cards measured +// +// Only DUE can fire, and only DUE buys the submitted-review page. The two +// readings reuse the file's own recognisers: `SEAT_RECORD_MARKER` + +// `seatRecordAccepts` (seat-record.mjs, H69's reading) for the ACCEPT, and +// `latestContractReviewOnHead` (the record reader `record-recognisers.mjs` +// imports too) for the PASS, widened with the verdict WORD +// (`contractReviewVerdictOf`) because a FAIL on the current head owes a patch +// round, not a request. Two bounds, stated: the PR thread is the one thread +// read — a PASS that sits on the card alone (the queue guard's second thread) +// reads here as BEFORE, so this row can only under-report, never ask early; +// and the ACCEPT's freshness against the head is H69's question, not this +// row's. A FAIL, a PASS on an older head, a REWORK, or a verdict word the +// reader cannot make out are all BEFORE. +// // ## Report-only, and pointedly so // // ⛔ Nothing can ship wrong through this gap: on Tier H the queue guard still @@ -9979,8 +10013,12 @@ export function renderDanglingReferences(report, { markdown = false } = {}) { // // ZERO extra changed-file requests: the pages are H36's, already fetched for // the population this row needs (`h36NeedsFiles` is wider than governed). The -// one request this row buys is the submitted-review leg, and only for a -// governed PR whose request∪assignment union is ALREADY short — on the live +// thread it reads for the hand-off is H48's fetch class (`prCommentCache`, one +// issue-comment thread per governed open PR; H48 and H69 then pay nothing for +// it), bought only for a Tier H PR the cheap union already leaves short. The +// one request of its own this row buys is the submitted-review leg, and only +// for a governed PR whose request∪assignment union is ALREADY short AND whose +// hand-off is DUE — on the live // board this landed against, 30 open PRs, 29 file pages already in hand, 8 // governed, 5 review probes. Both bounds are honest about what they hide: a // candidate past `H43_REVIEW_PROBE_CAP` (oldest-first, because the oldest @@ -10129,33 +10167,76 @@ export function h43CheapCoverage(pr) { return logins; } +/** The three hand-off states (header, "The timing"): unread is UNJUDGED, before is quiet by design, due can fire. */ +export const H43_HANDOFF_STATES = Object.freeze(['unread', 'before', 'due']); + /** - * The gathering policy, H16's idiom: answerable from the LIST row plus the - * file page already in hand, and never NARROWER than the population it feeds - * — nor WIDER: it reads the same matched slice as the row, so a Tier S PR - * (which the row stands down on) buys no review page either, and cannot crowd - * a Tier H candidate out of the oldest-first probe cap. A governed PR whose two - * cheap fields already cover every approver is DECIDED — no row can fire — so - * its review page is a request nobody needs. + * The hand-off reading (header, "The timing"): is this PR past the point at + * which the seat owes the approver request? `commentRows` is the PR's own + * issue-comment thread — H48's fetch class — in its three states (undefined + * never consulted, null unreadable, rows judged); `headSha` is the PR's CURRENT + * head. ACCEPT is the NEWEST `## 席位核对` record's verdict (seat-record.mjs, + * the one reading); PASS is the newest `## Contract review` naming this head + * whose verdict word reads PASS. Pure. + * + * @returns {{ state: 'unread'|'before'|'due', accepted: boolean, passed: boolean, + * record: { createdAt: string|null, stamp: number|null, index: number }|null, + * review: { id: number|null, sha: string|null, verdict: string|null, createdAt: string|null }|null }} */ -export function h43NeedsReviewProbe(pr, governed, approvers) { +export function h43HandoffReading(commentRows, headSha) { + if (commentRows === undefined || commentRows === null) { + return { state: 'unread', accepted: false, passed: false, record: null, review: null }; + } + const rows = Array.isArray(commentRows) ? commentRows : []; + const record = latestMarkedComment(rows, SEAT_RECORD_MARKER); + const accepted = record !== null && seatRecordAccepts(rows[record.index]?.body); + const review = latestContractReviewOnHead(rows, headSha); + const passed = review !== null && review.verdict === 'PASS'; + return { state: accepted && passed ? 'due' : 'before', accepted, passed, record, review }; +} + +/** + * The FIRST gathering policy — which PRs buy the thread read: answerable from + * the LIST row plus the file page already in hand, and never NARROWER than the + * population it feeds — nor WIDER: it reads the same matched slice as the row, + * so a Tier S PR (which the row stands down on) buys nothing, and a governed PR + * whose two cheap fields already cover every approver is DECIDED — no row can + * fire — so neither its thread nor its review page is a request anybody needs. + */ +export function h43NeedsThreadRead(pr, governed, approvers) { const surfaces = Array.isArray(governed) ? governed : []; if (surfaces.length === 0 || governedLandsOnRecord(surfaces)) return false; const cheap = h43CheapCoverage(pr); return (approvers ?? []).some((a) => !cheap.has(a)); } +/** + * The SECOND gathering policy, H16's idiom — which PRs buy the submitted-review + * page: the thread-read population narrowed to a hand-off that is DUE. Before + * the seat's ACCEPT and a same-head PASS nobody is owed a request, so no page + * is bought and no row can fire; an unread thread buys nothing either. + */ +export function h43NeedsReviewProbe(pr, governed, approvers, handoff = undefined) { + if (!h43NeedsThreadRead(pr, governed, approvers)) return false; + return handoff?.state === 'due'; +} + /** * The row. `governed` is the matched slice from the imported matcher (empty ⇒ * out of scope; a slice that lands on a record — Tier S — ⇒ out of scope too, * header above); `reviewed` is the logins that have SUBMITTED a review, or * `null` when that leg was not read — three input states, never two (#4690): - * covered, uncovered, and uncovered-on-the-cheap-union-alone, which says so. + * covered, uncovered, and uncovered-on-the-cheap-union-alone, which says so; + * `handoff` is `h43HandoffReading`'s answer — undefined or `unread` is + * UNJUDGED, `before` is not a half state, only `due` can fire (header, "The + * timing"). */ -export function h43GovernedReviewRequestGap(pr, governed, approvers, reviewed = null) { +export function h43GovernedReviewRequestGap(pr, governed, approvers, reviewed = null, handoff = undefined) { const surfaces = governed ?? []; const list = approvers ?? []; if (surfaces.length === 0 || list.length === 0 || governedLandsOnRecord(surfaces)) return null; + // The timing: before the seat's ACCEPT and a same-head PASS, nobody is owed a request — quiet by design. + if (handoff?.state !== 'due') return null; const cheap = h43CheapCoverage(pr); const submitted = reviewed == null ? null : new Set([...reviewed].map((l) => String(l))); const missing = list.filter((a) => !cheap.has(a) && !(submitted?.has(a) ?? false)); @@ -10186,16 +10267,24 @@ export function h43GovernedReviewRequestGap(pr, governed, approvers, reviewed = 'this row stands on the request∪assignment union alone — an approver who has already SUBMITTED ' + 'a review would clear it, and that reading is missing rather than negative.' : ''; + const recordWhen = handoff.record?.createdAt ? ` (${handoff.record.createdAt})` : ''; + const passHead = handoff.review?.sha ? `\`${handoff.review.sha}\`` : 'this head'; + const passWhere = handoff.review?.id != null ? ` (comment ${handoff.review.id})` : ''; return ( - `open and GOVERNED on Tier H (${hits} changed file(s) on the register: ${named}${more}), and ${asked}. ` + + `open and GOVERNED on Tier H (${hits} changed file(s) on the register: ${named}${more}), past the seat's ` + + `hand-off point — its thread carries a \`## 席位核对\` ACCEPT${recordWhen} and a \`## Contract review\` PASS ` + + `naming head ${passHead}${passWhere} — and ${asked}. A human is asked only after the seat's review and a PASS ` + + 'on that very head (「审核通过才会推送给我审核」), so until both were on the thread this PR was not a half ' + + 'state; now that they are, the request is the seat\'s hand-off and it has not been made. ' + 'Being a DRAFT is neither why this fires nor why it would go quiet — a governed PR waiting as a ' + 'draft for the human merge is the CORRECT terminal state of this regime, and it is still a state ' + 'in which nobody has been asked to look.' + authorClause + ' Report-only patrol INPUT, not a verdict and not a gate: nothing can ship through this gap (the ' + 'queue guard still refuses an unapproved Tier H enqueue), so what is missing is only that the ' + - 'person the regime protects was never asked. Remedy is the request itself, or the assign fallback ' + - 'above — ⛔ never a label, and ⛔ never an approving review from a seat.' + + 'person the regime protects was never asked. Remedy is the seat\'s hand-off stroke — relay ' + + '`pr_request_reviewers` naming them, or the assign fallback above — ⛔ never a label, and ⛔ never ' + + 'an approving review from a seat.' + legClause ); } @@ -11480,10 +11569,13 @@ export function h48GovernedVerdictWithoutBrief(pr, governed, commentRows) { // the maintainer being asked to look: before `pr_ready` on Tier H and before // a Tier S PR is queued, that record exists on the PR for the head the // maintainer would read (pm-dispatch SKILL.md, ACCEPT 之后的路径分叉 ①). The -// relay requests `GOVERNED_APPROVERS` at `pr_create` as a standing summons and -// again at `pr_ready`, so a Tier H PR carries a pending request from its -// first minute — which is exactly why the request alone says nothing about -// whether the person asked has anything to read. Measured on the filing day: +// request itself is the seat's HAND-OFF, owed only after that record and a +// same-head `## Contract review` PASS (路径分叉 ③; the relay's draft open +// summons nobody), so a pending request with no record newer than the head is +// a hand-off made out of order, or an off-relay actor's request — and either +// way the request alone says nothing about whether the person asked has +// anything to read. Measured on the filing day, under the regime that still +// summoned at `pr_create`: // four of six governed drafts had an approver requested and no seat record at // all, so the maintainer opened the dev's draft with 席位意见 empty. // @@ -11619,9 +11711,10 @@ export function h69SeatRecordBehindHead(pr, governed, approvers, commentRows, he return ( `open and GOVERNED on Tier H (${hits} changed file(s) on the register: ${named}${more}), with ${asked} ` + `requested for review and ${missing}. The seat's record — the 维护者速读 with 席位意见 filled, then ` + - '`## 席位核对` with the six-item check — is what the request is supposed to FOLLOW, not precede: the relay ' + - 'summons the approvers at `pr_create` and again at `pr_ready`, so a pending request says the maintainer was ' + - 'asked, and only the record says there is something on this head for them to read. Remedy: post the ' + + '`## 席位核对` with the six-item check — is what the request is supposed to FOLLOW, not precede: the request ' + + 'is the seat\'s HAND-OFF, owed only after that record and a same-head `## Contract review` PASS, so a pending ' + + 'request says the maintainer was asked, and only the record says there is something on this head for them to ' + + 'read. Remedy: post the ' + `\`## 席位核对\` record as a COMMENT for head \`${sha7}\`${staleClause}. Report-only patrol INPUT, not a ` + 'verdict and not a gate: nothing ships through this gap (the queue guard still refuses an unapproved Tier H ' + 'enqueue), ⛔ no label is written from here, and ⛔ no seat may substitute an approving review for the record.' @@ -12499,7 +12592,51 @@ export function latestContractReviewOnHead(commentRows, headSha) { const newest = latestMarkedComment(onHead, CONTRACT_REVIEW_HEADING_MARKER); if (!newest) return null; const row = onHead[newest.index]; - return { ...newest, id: row?.id ?? null, sha: contractReviewHeadMatch(row?.body, headSha) }; + // The verdict WORD rides beside id and sha for H43's hand-off reading; existing readers ignore it. + return { ...newest, id: row?.id ?? null, sha: contractReviewHeadMatch(row?.body, headSha), verdict: contractReviewVerdictOf(row?.body) }; +} + +/** + * The verdict WORD a contract-review record carries — `PASS`, `FAIL`, or + * `null` when none can be read. + * + * ⚠️ This is the ONE reader of the word, and it is a PATROL's reader: + * `record-recognisers.mjs` deliberately reads existence and provenance only + * (whether a record reads PASS is the seat's precondition ①, and the queue + * guard stands on the record's existence). H43 needs the word because the + * hand-off it patrols is owed only after a PASS — a FAIL on the current head + * owes a patch round, not a request — and a word it cannot read is `null`, + * which H43 reads as NOT yet due (a quiet row), never as PASS. + * + * Three measured dialects, consulted in this order: + * explicit a line opening `**VERDICT: PASS**` (the `--template` form), + * consulted FIRST wherever it sits, because the template puts it + * LAST, under ①–④ prose whose lines may open with either word; + * inline the heading's own tail, `## Contract review (clause ②) — **PASS** · head …`; + * bare the first line under the heading that opens with the word + * (the queue guard's own fixture shape). + * No `g` flag anywhere, so two reads of one body agree. + */ +export const CONTRACT_REVIEW_VERDICTS = Object.freeze(['PASS', 'FAIL']); +const CONTRACT_REVIEW_VERDICT_EXPLICIT = /^[ \t]*>?[ \t]*\*{0,2}VERDICT:[ \t]*\*{0,2}(PASS|FAIL)\b/; +const CONTRACT_REVIEW_VERDICT_INLINE = /(?:^|[\s—–::·|(-])\*{0,2}(PASS|FAIL)\*{0,2}(?=$|[\s·—–,.。;;)])/; +const CONTRACT_REVIEW_VERDICT_BARE = /^[ \t]*>?[ \t]*\*{0,2}(PASS|FAIL)\*{0,2}\b/; +export function contractReviewVerdictOf(body) { + const text = String(body ?? ''); + const m = CONTRACT_REVIEW_HEADING_MARKER.exec(text); + if (!m) return null; + const [headingTail, ...below] = text.slice(m.index + m[0].length).split(/\r?\n/); + for (const line of below) { + const explicit = CONTRACT_REVIEW_VERDICT_EXPLICIT.exec(line); + if (explicit) return explicit[1]; + } + const inline = CONTRACT_REVIEW_VERDICT_INLINE.exec(headingTail); + if (inline) return inline[1]; + for (const line of below) { + const bare = CONTRACT_REVIEW_VERDICT_BARE.exec(line); + if (bare) return bare[1]; + } + return null; } // --------------------------------------------------------------------------- @@ -17400,6 +17537,9 @@ export const SWEEP_COUNT_KEYS = [ 'governedPrs', 'governedReviewCandidates', 'governedReviewProbed', + // H43's hand-off reading: threads read, and how many PRs are past the point where the request is owed. + 'governedHandoffRead', + 'governedHandoffDue', 'governedRegisterReason', 'liveFolds', 'memberReadCandidates', @@ -17987,12 +18127,15 @@ export function summaryLine(counts, findingCount) { counts.governedRegisterReason ? `NOT MEASURED — ${counts.governedRegisterReason}` : `${counts.governedPrs ?? 0} of the open PR(s) whose changed-file page was read hit the ` + - `governed register, and the submitted-review leg answered on ${counts.governedReviewProbed ?? 0} ` + - `of ${counts.governedReviewCandidates ?? 0} that the request/assignment union already left ` + - `short (Tier H only — a Tier S PR lands on a record, is asked nobody, and is out of this item; ` + - `oldest-first, ${H43_REVIEW_PROBE_CAP} per run). An unread review leg leaves its row ` + - 'standing on the two cheap fields and the row says so; a PR whose file page went unread is ' + - 'not judged by this item at all' + 'governed register; among the Tier H ones the request/assignment union left short, the thread was ' + + `read on ${counts.governedHandoffRead ?? 0} of ${counts.governedReviewCandidates ?? 0} and ` + + `${counts.governedHandoffDue ?? 0} are past the seat's hand-off point (a \`## 席位核对\` ACCEPT and a ` + + 'same-head `## Contract review` PASS on the thread — before both, nobody is owed a request and the row ' + + 'is quiet; an unread thread is UNJUDGED), and the submitted-review leg answered on ' + + `${counts.governedReviewProbed ?? 0} of those ${counts.governedHandoffDue ?? 0} (Tier H only — a Tier S ` + + `PR lands on a record, is asked nobody, and is out of this item; oldest-first, ${H43_REVIEW_PROBE_CAP} ` + + 'per run). An unread review leg leaves its row standing on the two cheap fields and the row says so; a ' + + 'PR whose file page went unread is not judged by this item at all' }. ` + `Family folds (H37): ${counts.liveFolds ?? 0} live shared branch(es) claimed by more than their ` + `own chain head, and a member comment page read on ${counts.memberReadProbed ?? 0} of ` + @@ -22386,12 +22529,13 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // than a widening of the one above on purpose: `commentCache` is keyed by card // number and fed by listings that all filter `!i.pull_request`, so a PR // number colliding with a card number in one map would hand a card's thread to - // a PR row. H48 is its only reader today; a future PR-comment reader finds a - // thread this row already bought here and pays nothing for it. + // a PR row. Three readers: H43 (the hand-off reading, first in the sweep), + // H48 and H69 — whichever asks first buys the thread, the others pay nothing. // - // ⛔ Bought ONLY for the PRs H48 speaks about (governed and open), never for - // the open-PR listing at large — the caller enforces that, and this helper - // never fetches a number it was not handed. + // ⛔ Bought ONLY for governed open PRs (H48's population; H43's Tier H + // short-of-coverage slice is inside it), never for the open-PR listing at + // large — the callers enforce that, and this helper never fetches a number + // it was not handed. // // Returns REST ROWS, or `null` for a thread that could not be read — a failed // request, or a walk still full at the page ceiling. Both are UNJUDGED @@ -23016,10 +23160,13 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // H43 (#14967) — the governed-surface review-request gap, read off the SAME // changed-file pages H36 just fetched: this row buys ZERO extra file - // requests. Its own request is the submitted-review leg, and only for a - // governed PR whose request/assignment union is already short — without that - // leg a PR an approver has ALREADY reviewed fires forever, because GitHub - // drops the pending request the moment the review lands. + // requests. It reads the PR thread (H48's fetch class, cached for H48 and + // H69) for the hand-off — the seat's `## 席位核对` ACCEPT and a same-head + // `## Contract review` PASS, before which nobody is owed a request — and its + // own request is the submitted-review leg, bought only for a governed PR whose + // hand-off is due and whose request/assignment union is already short — + // without that leg a PR an approver has ALREADY reviewed fires forever, + // because GitHub drops the pending request the moment the review lands. // // A register that would not load skips the pass and NAMES itself on the // summary line (#4690): a sibling-repo copy of this sweeper carries no @@ -23040,14 +23187,31 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // Oldest-first, because the PR nobody has asked about for longest is the // one the cap must not be the reason nobody hears about. The policy reads // the matched slice, so a Tier S PR — which the row stands down on — buys - // no review page and takes no slot under the cap. + // no thread and no review page, and takes no slot under the cap. const shortOfCoverage = [...seenPrs.values()] - .filter((pr) => h43NeedsReviewProbe(pr, governedByPr.get(pr.number) ?? [], GOVERNED_REGISTER.approvers)) + .filter((pr) => h43NeedsThreadRead(pr, governedByPr.get(pr.number) ?? [], GOVERNED_REGISTER.approvers)) .sort((a, b) => String(a.created_at ?? '').localeCompare(String(b.created_at ?? ''))); stats.governedReviewCandidates = shortOfCoverage.length; + // The hand-off reading (the row's header, "The timing"): ONE issue-comment + // thread per short-of-coverage Tier H PR — H48's fetch class, landing in + // `prCommentCache` so H48 and H69 pay nothing for it afterwards. Before + // the seat's `## 席位核对` ACCEPT and a same-head `## Contract review` PASS + // are both on the thread, nobody is owed a request, so such a PR buys no + // review page and fires no row; an unread thread is UNJUDGED. + const handoffByPr = new Map(); + for (const pr of shortOfCoverage) { + const rows = await prCommentRowsFor(pr.number); + if (rows !== null) stats.governedHandoffRead = (stats.governedHandoffRead ?? 0) + 1; + handoffByPr.set(pr.number, h43HandoffReading(rows, pr.head?.sha)); + } + const handoffDue = shortOfCoverage.filter((pr) => + h43NeedsReviewProbe(pr, governedByPr.get(pr.number) ?? [], GOVERNED_REGISTER.approvers, handoffByPr.get(pr.number)), + ); + stats.governedHandoffDue = handoffDue.length; + const reviewedByPr = new Map(); - for (const pr of shortOfCoverage.slice(0, H43_REVIEW_PROBE_CAP)) { + for (const pr of handoffDue.slice(0, H43_REVIEW_PROBE_CAP)) { let reviews; try { reviews = await rest(`/repos/${OWNER_REPO}/pulls/${pr.number}/reviews?per_page=${H43_REVIEWS_PAGE_SIZE}`); @@ -23073,6 +23237,7 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen governed, GOVERNED_REGISTER.approvers, reviewedByPr.get(pr.number) ?? null, + handoffByPr.get(pr.number), ); if (gap) findings.push([pr, 'H43', gap]); } @@ -24408,6 +24573,15 @@ export const SELF_TEST_BATTERIES = Object.freeze({ // the non-approver and assign-only requests that must not read as a // summons, and the Tier S slice the row stands down on. 'H69 seat record before governed review': 40, + // Registered with the hand-off timing: 89 cases (57 legacy coverage cases now + // driven with the hand-off DUE, 32 timing cases), the pin just under. What this + // battery floors is a NARROWING of when the row fires — only once the seat's + // `## 席位核对` ACCEPT and a same-head `## Contract review` PASS are on the + // thread — so the controls that keep it a narrowing and not a silencer live + // inside it: the DUE case that must still fire, the FAIL / older-head / REWORK + // cases that must stay quiet, the unread thread that must stay UNJUDGED, and + // the three verdict-word dialects. + 'H43 hand-off timing': 80, }); /** The floor on the ROSTER itself — how many batteries must be declared at all. */ @@ -31399,78 +31573,96 @@ async function selfTest() { }); const APP43 = ['os-zhuang', 'hotlong']; const GOV43 = [{ id: 'adr', glob: 'docs/adr/**', what: 'architecture decision records', files: ['docs/adr/0121-x.md'] }]; - const h43row = (...args) => String(h43GovernedReviewRequestGap(...args) ?? ''); + // The hand-off timing (the maintainer's 「审核通过才会推送给我审核」): every + // legacy case below that expects a FINDING is driven with the hand-off DUE — + // the seat's `## 席位核对` ACCEPT and a `## Contract review` PASS naming the + // head both on the thread — because before both, the row is quiet by design; + // the timing itself has its own battery after the legacy cases. + const BATTERY43 = 'H43 hand-off timing'; + const t43 = (name, actual, expected) => b(BATTERY43, name, actual, expected); + const HEAD43 = '7cc88889e2294032fbb467107499c8f04d9a77da'; + const ACCEPT43 = '## 席位核对\n\nACCEPT · domain:skills seat 2 · head `7cc88889`'; + const REWORK43 = '## 席位核对\n\nREWORK · 补测试'; + const PASS43 = '## Contract review\n\nServed-tier: `CONTRACT_REVIEW_TIER`\nHead-sha: `7cc88889e229`\nReviewed-by: `session_01x`\n\n**VERDICT: PASS**'; + const FAIL43 = PASS43.replace('**VERDICT: PASS**', '**VERDICT: FAIL**'); + const OLD_PASS43 = PASS43.replace('`7cc88889e229`', '`0123456789ab`'); + const rows43 = (...bodies) => bodies.map((body, i) => ({ id: 900 + i, body, created_at: `2026-10-28T1${i}:00:00Z` })); + const DUE43 = h43HandoffReading(rows43(ACCEPT43, PASS43), HEAD43); + const BEFORE43 = h43HandoffReading(rows43(ACCEPT43), HEAD43); + const h43Gap = (pr, governed, approvers, reviewed, handoff = DUE43) => h43GovernedReviewRequestGap(pr, governed, approvers, reviewed, handoff); + const h43Probe = (pr, governed, approvers, handoff = DUE43) => h43NeedsReviewProbe(pr, governed, approvers, handoff); + const h43row = (...args) => String(h43Gap(...args) ?? ''); // ① The healthy governed PR: both accounts asked. This is the case the row // must stay quiet on forever, or the anchor grows a permanent row. - t('H43: both approvers requested -> clean', h43GovernedReviewRequestGap(h43pr(1, { requested: APP43 }), GOV43, APP43, []), null); - t('H43: …assignment covers just as well as a request', h43GovernedReviewRequestGap(h43pr(1, { assigned: APP43 }), GOV43, APP43, []), null); - t('H43: …and a mix of the two fields covers', h43GovernedReviewRequestGap(h43pr(1, { requested: ['os-zhuang'], assigned: ['hotlong'] }), GOV43, APP43, []), null); + t43('H43: both approvers requested -> clean', h43Gap(h43pr(1, { requested: APP43 }), GOV43, APP43, []), null); + t43('H43: …assignment covers just as well as a request', h43Gap(h43pr(1, { assigned: APP43 }), GOV43, APP43, []), null); + t43('H43: …and a mix of the two fields covers', h43Gap(h43pr(1, { requested: ['os-zhuang'], assigned: ['hotlong'] }), GOV43, APP43, []), null); // ② One account asked — the measured majority shape (7 of 10 governed PRs at // the director sweep). The row must NAME the account nobody asked. - t('H43: one approver requested -> finding', typeof h43GovernedReviewRequestGap(h43pr(2, { requested: ['hotlong'] }), GOV43, APP43, []), 'string'); - t('H43: …naming the account that was never asked', h43row(h43pr(2, { requested: ['hotlong'] }), GOV43, APP43, []).includes('`os-zhuang`'), true); - t('H43: …and NOT accusing the one that was', h43row(h43pr(2, { requested: ['hotlong'] }), GOV43, APP43, []).includes('`hotlong`'), false); - t('H43: …and naming the register hit that makes it governed', h43row(h43pr(2, { requested: ['hotlong'] }), GOV43, APP43, []).includes('`docs/adr/**`×1'), true); + t43('H43: one approver requested -> finding', typeof h43Gap(h43pr(2, { requested: ['hotlong'] }), GOV43, APP43, []), 'string'); + t43('H43: …naming the account that was never asked', h43row(h43pr(2, { requested: ['hotlong'] }), GOV43, APP43, []).includes('`os-zhuang`'), true); + t43('H43: …and NOT accusing the one that was', h43row(h43pr(2, { requested: ['hotlong'] }), GOV43, APP43, []).includes('`hotlong`'), false); + t43('H43: …and naming the register hit that makes it governed', h43row(h43pr(2, { requested: ['hotlong'] }), GOV43, APP43, []).includes('`docs/adr/**`×1'), true); // ③ Nobody asked at all — two of the ten, both ADR records. - t('H43: nobody requested -> finding', typeof h43GovernedReviewRequestGap(h43pr(3), GOV43, APP43, []), 'string'); - t('H43: …and it says NOBODY rather than listing a partial gap', h43row(h43pr(3), GOV43, APP43, []).includes('has asked NOBODY'), true); - t('H43: …still naming BOTH accounts, so the remedy needs no lookup', /`os-zhuang`[\s\S]*`hotlong`/.test(h43row(h43pr(3), GOV43, APP43, [])), true); + t43('H43: nobody requested -> finding', typeof h43Gap(h43pr(3), GOV43, APP43, []), 'string'); + t43('H43: …and it says NOBODY rather than listing a partial gap', h43row(h43pr(3), GOV43, APP43, []).includes('has asked NOBODY'), true); + t43('H43: …still naming BOTH accounts, so the remedy needs no lookup', /`os-zhuang`[\s\S]*`hotlong`/.test(h43row(h43pr(3), GOV43, APP43, [])), true); // ④ + ⑤ The author-identity case, which is the one that silently produces // ZERO coverage: a request naming the PR's own author is a 422, so the // documented fallback is to ASSIGN that account and request the other. - t('H43: approver-authored WITH the assign fallback taken -> clean', h43GovernedReviewRequestGap(h43pr(4, { author: 'hotlong', assigned: ['hotlong'], requested: ['os-zhuang'] }), GOV43, APP43, []), null); - t('H43: approver-authored WITHOUT it -> finding', typeof h43GovernedReviewRequestGap(h43pr(5, { author: 'hotlong' }), GOV43, APP43, []), 'string'); - t('H43: …⛔ authorship alone is never coverage', h43row(h43pr(5, { author: 'hotlong' }), GOV43, APP43, []).includes('`hotlong`'), true); - t('H43: …and the sentence says the request is impossible, not forgotten', h43row(h43pr(5, { author: 'hotlong' }), GOV43, APP43, []).includes('422'), true); - t('H43: …naming the fallback that WAS documented and was not taken', h43row(h43pr(5, { author: 'hotlong' }), GOV43, APP43, []).includes('ASSIGN'), true); + t43('H43: approver-authored WITH the assign fallback taken -> clean', h43Gap(h43pr(4, { author: 'hotlong', assigned: ['hotlong'], requested: ['os-zhuang'] }), GOV43, APP43, []), null); + t43('H43: approver-authored WITHOUT it -> finding', typeof h43Gap(h43pr(5, { author: 'hotlong' }), GOV43, APP43, []), 'string'); + t43('H43: …⛔ authorship alone is never coverage', h43row(h43pr(5, { author: 'hotlong' }), GOV43, APP43, []).includes('`hotlong`'), true); + t43('H43: …and the sentence says the request is impossible, not forgotten', h43row(h43pr(5, { author: 'hotlong' }), GOV43, APP43, []).includes('422'), true); + t43('H43: …naming the fallback that WAS documented and was not taken', h43row(h43pr(5, { author: 'hotlong' }), GOV43, APP43, []).includes('ASSIGN'), true); // The other half of the same PR: the non-author approver is an ordinary miss // and must not inherit the author sentence. - t('H43: a non-author approver gets no 422 clause', h43row(h43pr(6, { author: 'someone-else', requested: ['hotlong'] }), GOV43, APP43, []).includes('422'), false); + t43('H43: a non-author approver gets no 422 clause', h43row(h43pr(6, { author: 'someone-else', requested: ['hotlong'] }), GOV43, APP43, []).includes('422'), false); // ⑥ A SUBMITTED review counts as coverage — and it must, because GitHub // removes the pending request the moment the review lands. Measured live // while this row was written: an open governed PR requesting `hotlong` with a // submitted review by `os-zhuang` reads as fully covered, and the two-field // union alone called it unasked. - t('H43: a submitted review covers its author\'s approver slot', h43GovernedReviewRequestGap(h43pr(7, { requested: ['hotlong'] }), GOV43, APP43, ['os-zhuang']), null); - t('H43: …and an unrelated reviewer covers nothing', typeof h43GovernedReviewRequestGap(h43pr(7, { requested: ['hotlong'] }), GOV43, APP43, ['some-dev']), 'string'); + t43('H43: a submitted review covers its author\'s approver slot', h43Gap(h43pr(7, { requested: ['hotlong'] }), GOV43, APP43, ['os-zhuang']), null); + t43('H43: …and an unrelated reviewer covers nothing', typeof h43Gap(h43pr(7, { requested: ['hotlong'] }), GOV43, APP43, ['some-dev']), 'string'); // ⑦ Not governed: out of scope entirely, however empty the request list is. - t('H43: a non-governed PR with nobody requested -> clean', h43GovernedReviewRequestGap(h43pr(8), [], APP43, []), null); + t43('H43: a non-governed PR with nobody requested -> clean', h43Gap(h43pr(8), [], APP43, []), null); // Three input states, never two (#4690): the review leg can be MISSING, and // a row that stands on the cheap union alone says so rather than asserting. - t('H43: an unread review leg is declared, not assumed negative', h43row(h43pr(9), GOV43, APP43, null).includes('NOT read'), true); - t('H43: …and a read one makes no such claim', h43row(h43pr(9), GOV43, APP43, []).includes('NOT read'), false); + t43('H43: an unread review leg is declared, not assumed negative', h43row(h43pr(9), GOV43, APP43, null).includes('NOT read'), true); + t43('H43: …and a read one makes no such claim', h43row(h43pr(9), GOV43, APP43, []).includes('NOT read'), false); // Draft status decides nothing in either direction — a governed PR waiting as // a draft for the human merge is the regime's CORRECT terminal state. - t('H43: a ready governed PR fires the same as a draft', h43row(h43pr(10, { draft: false }), GOV43, APP43, []), h43row(h43pr(10, { draft: true }), GOV43, APP43, [])); - t('H43: …and the row says being a draft is not the point', h43row(h43pr(10), GOV43, APP43, []).includes('DRAFT'), true); + t43('H43: a ready governed PR fires the same as a draft', h43row(h43pr(10, { draft: false }), GOV43, APP43, []), h43row(h43pr(10, { draft: true }), GOV43, APP43, [])); + t43('H43: …and the row says being a draft is not the point', h43row(h43pr(10), GOV43, APP43, []).includes('DRAFT'), true); // ⛔ No name and no surface is hardcoded: an entirely different approver set // drives the row, which is what "imported, never restated" has to mean. - t('H43: the approver set is INJECTED, never baked in', h43GovernedReviewRequestGap(h43pr(11, { requested: ['some-other-account'] }), GOV43, ['some-other-account'], []), null); - t('H43: an empty approver set can never fire', h43GovernedReviewRequestGap(h43pr(11), GOV43, [], []), null); + t43('H43: the approver set is INJECTED, never baked in', h43Gap(h43pr(11, { requested: ['some-other-account'] }), GOV43, ['some-other-account'], []), null); + t43('H43: an empty approver set can never fire', h43Gap(h43pr(11), GOV43, [], []), null); // The gathering policy: a request is bought only for a governed PR the cheap // fields already leave short. Never narrower than the population it feeds. - t('H43 policy: a non-governed PR buys no review page', h43NeedsReviewProbe(h43pr(12), [], APP43), false); - t('H43 policy: a governed PR already covered buys none either', h43NeedsReviewProbe(h43pr(12, { requested: APP43 }), GOV43, APP43), false); - t('H43 policy: a governed PR short of coverage does', h43NeedsReviewProbe(h43pr(12, { requested: ['hotlong'] }), GOV43, APP43), true); - t('H43 policy: …and one with nobody asked, obviously', h43NeedsReviewProbe(h43pr(12), GOV43, APP43), true); - t('H43: the cheap union reads BOTH list fields', [...h43CheapCoverage(h43pr(13, { requested: ['a'], assigned: ['b'] }))].sort().join(','), 'a,b'); + t43('H43 policy: a non-governed PR buys no review page', h43Probe(h43pr(12), [], APP43), false); + t43('H43 policy: a governed PR already covered buys none either', h43Probe(h43pr(12, { requested: APP43 }), GOV43, APP43), false); + t43('H43 policy: a governed PR short of coverage does', h43Probe(h43pr(12, { requested: ['hotlong'] }), GOV43, APP43), true); + t43('H43 policy: …and one with nobody asked, obviously', h43Probe(h43pr(12), GOV43, APP43), true); + t43('H43: the cheap union reads BOTH list fields', [...h43CheapCoverage(h43pr(13, { requested: ['a'], assigned: ['b'] }))].sort().join(','), 'a,b'); // The row is a `stall`, not a gate: nothing ships wrong through this gap — // the queue guard still refuses an unapproved governed enqueue — but the PR // is stopped and no other sweep moves it. - t('H43 band: registered as a stall', familyBand('H43'), 'stall'); - t('H43 band: …and the sweep really pushes it, so the registry sees it', familyRegistryCoverage().emitted.includes('H43'), true); - t('H43: the row states its report-only posture', h43row(h43pr(14), GOV43, APP43, []).includes('not a verdict and not a gate'), true); + t43('H43 band: registered as a stall', familyBand('H43'), 'stall'); + t43('H43 band: …and the sweep really pushes it, so the registry sees it', familyRegistryCoverage().emitted.includes('H43'), true); + t43('H43: the row states its report-only posture', h43row(h43pr(14), GOV43, APP43, []).includes('not a verdict and not a gate'), true); // The REGISTER, pinned against the real imported modules rather than a // fabricated pair. ⚠️ This is the positive control that a rename in either @@ -31480,11 +31672,11 @@ async function selfTest() { const register43 = GOVERNED_REGISTER.available ? `${GOVERNED_REGISTER.matcher(['docs/adr/0121-x.md']).length > 0}/${GOVERNED_REGISTER.matcher(['packages/core/src/x.ts']).length > 0}/${GOVERNED_REGISTER.approvers.length >= 2}` : `NOT MEASURED — ${GOVERNED_REGISTER.reason}`; - t('H43 register: the imported matcher answers governed and clear, and the approver set is non-empty', register43, 'true/false/true'); - t('H43 register: the load is three-state — resolved, or a NAMED reason, never silently absent', GOVERNED_REGISTER.available || typeof GOVERNED_REGISTER.reason === 'string', true); - t('H43 register: a missing sibling names the two files to copy', governedRegisterFailure({ code: 'ERR_MODULE_NOT_FOUND' }).includes('check-governed-queue-guard.mjs'), true); - t('H43 register: …and says the rest of the sweep is unaffected', governedRegisterFailure({ code: 'ERR_MODULE_NOT_FOUND' }).includes('every other row is unaffected'), true); - t('H43 register: any other load failure carries its own message', governedRegisterFailure(new Error('boom')).includes('boom'), true); + t43('H43 register: the imported matcher answers governed and clear, and the approver set is non-empty', register43, 'true/false/true'); + t43('H43 register: the load is three-state — resolved, or a NAMED reason, never silently absent', GOVERNED_REGISTER.available || typeof GOVERNED_REGISTER.reason === 'string', true); + t43('H43 register: a missing sibling names the two files to copy', governedRegisterFailure({ code: 'ERR_MODULE_NOT_FOUND' }).includes('check-governed-queue-guard.mjs'), true); + t43('H43 register: …and says the rest of the sweep is unaffected', governedRegisterFailure({ code: 'ERR_MODULE_NOT_FOUND' }).includes('every other row is unaffected'), true); + t43('H43 register: any other load failure carries its own message', governedRegisterFailure(new Error('boom')).includes('boom'), true); // The per-row scope (ruling B2, 2026-10-13): the register's question carries // the swept board, bound ONCE at load. Driven through the real modules with @@ -31494,12 +31686,12 @@ async function selfTest() { const boundCloud = await loadGovernedRegister(realLoad, 'objectstack-ai/cloud'); const boundOs = await loadGovernedRegister(realLoad, 'objectstack-ai/objectos'); const boundForeign = await loadGovernedRegister(realLoad, 'someone/fork'); - t('H43 register: the loader binds the swept board\'s repository id, and this run\'s is the board it sweeps', + t43('H43 register: the loader binds the swept board\'s repository id, and this run\'s is the board it sweeps', GOVERNED_REGISTER.available ? GOVERNED_REGISTER.repo : 'NOT MEASURED', GOVERNED_REGISTER.available ? (await realLoad('./check-governed-merges.mjs')).repoIdForSlug(OWNER_REPO) : 'NOT MEASURED'); - t('H43 register: a workflow path is governed on a cloud board (the scoped row names cloud)', boundCloud.available && boundCloud.repo === 'cloud' && boundCloud.matcher(['.github/workflows/ci.yml']).length === 1, true); - t('H43 register: …and NOT on an objectos board, while the default rows still govern it', boundOs.available && boundOs.repo === 'objectos' && boundOs.matcher(['.github/workflows/ci.yml']).length === 0 && boundOs.matcher(['AGENTS.md']).length === 1, true); - t('H43 register: a board outside GOVERNED_REPOS is NOT MEASURED with the board named, never judged against another repository\'s rows', !boundForeign.available && boundForeign.matcher === null && boundForeign.repo === null && /someone\/fork/.test(boundForeign.reason) && /NOT MEASURED on this board/.test(boundForeign.reason), true); + t43('H43 register: a workflow path is governed on a cloud board (the scoped row names cloud)', boundCloud.available && boundCloud.repo === 'cloud' && boundCloud.matcher(['.github/workflows/ci.yml']).length === 1, true); + t43('H43 register: …and NOT on an objectos board, while the default rows still govern it', boundOs.available && boundOs.repo === 'objectos' && boundOs.matcher(['.github/workflows/ci.yml']).length === 0 && boundOs.matcher(['AGENTS.md']).length === 1, true); + t43('H43 register: a board outside GOVERNED_REPOS is NOT MEASURED with the board named, never judged against another repository\'s rows', !boundForeign.available && boundForeign.matcher === null && boundForeign.repo === null && /someone\/fork/.test(boundForeign.reason) && /NOT MEASURED on this board/.test(boundForeign.reason), true); // The TIER, read off the REAL register's own slices rather than a fixture // that restates which surfaces are which: a PR whose governed paths all lie @@ -31510,16 +31702,16 @@ async function selfTest() { // closed) — every case above it is a Tier H case by construction. const TIER_S_43 = GOVERNED_REGISTER.available ? GOVERNED_REGISTER.matcher(['.claude/agents/os-dev.md']) : []; const MIXED_43 = GOVERNED_REGISTER.available ? GOVERNED_REGISTER.matcher(['.claude/agents/os-dev.md', 'docs/adr/0121-x.md']) : []; - t('H43 register: …and it exports the landing-tier reader beside the matcher, answering its own record tier on the record row', GOVERNED_REGISTER.available && typeof GOVERNED_REGISTER.tierOf === 'function' && GOVERNED_REGISTER.tierOf(TIER_S_43) === GOVERNED_REGISTER.recordTier, true); - t('H43 tier: a PR whose governed paths ALL land on a record (Tier S) is asked nobody and is CLEAN, however empty the request list', h43GovernedReviewRequestGap(h43pr(15), TIER_S_43, APP43, []), null); - t('H43 tier: …and a MIXED slice (one Tier H row) is Tier H, so the same PR still fires', typeof h43GovernedReviewRequestGap(h43pr(15), MIXED_43, APP43, []), 'string'); - t('H43 tier: …naming the tier it speaks about', h43row(h43pr(15), MIXED_43, APP43, []).includes('GOVERNED on Tier H'), true); - t('H43 tier: the Tier S PR buys no review page either, so it takes no slot under the probe cap', h43NeedsReviewProbe(h43pr(15), TIER_S_43, APP43), false); - t('H43 tier: …while the mixed one does', h43NeedsReviewProbe(h43pr(15), MIXED_43, APP43), true); - t('H43 tier: the answer is the register\'s, on the slice — S for the record row, H for mixed, H for a tier-less fixture, H for nothing', [governedLandsOnRecord(TIER_S_43), governedLandsOnRecord(MIXED_43), governedLandsOnRecord(GOV43), governedLandsOnRecord([])].join(), 'true,false,false,false'); - t('H43 tier: ⛔ a register that did not load stands NOTHING down', governedLandsOnRecord(TIER_S_43, { available: false, tierOf: null, recordTier: null }), false); - t('H43 tier: …nor does one that loaded without the tier reader', governedLandsOnRecord(TIER_S_43, { available: true, tierOf: null, recordTier: 'S' }), false); - t('H43 summary: the review leg is stated as Tier H only', saidBy('h43GovernedReviews', summaryLine({ governedPrs: 8, governedReviewCandidates: 5, governedReviewProbed: 5 }, 0)).includes('Tier H only'), true); + t43('H43 register: …and it exports the landing-tier reader beside the matcher, answering its own record tier on the record row', GOVERNED_REGISTER.available && typeof GOVERNED_REGISTER.tierOf === 'function' && GOVERNED_REGISTER.tierOf(TIER_S_43) === GOVERNED_REGISTER.recordTier, true); + t43('H43 tier: a PR whose governed paths ALL land on a record (Tier S) is asked nobody and is CLEAN, however empty the request list', h43Gap(h43pr(15), TIER_S_43, APP43, []), null); + t43('H43 tier: …and a MIXED slice (one Tier H row) is Tier H, so the same PR still fires', typeof h43Gap(h43pr(15), MIXED_43, APP43, []), 'string'); + t43('H43 tier: …naming the tier it speaks about', h43row(h43pr(15), MIXED_43, APP43, []).includes('GOVERNED on Tier H'), true); + t43('H43 tier: the Tier S PR buys no review page either, so it takes no slot under the probe cap', h43Probe(h43pr(15), TIER_S_43, APP43), false); + t43('H43 tier: …while the mixed one does', h43Probe(h43pr(15), MIXED_43, APP43), true); + t43('H43 tier: the answer is the register\'s, on the slice — S for the record row, H for mixed, H for a tier-less fixture, H for nothing', [governedLandsOnRecord(TIER_S_43), governedLandsOnRecord(MIXED_43), governedLandsOnRecord(GOV43), governedLandsOnRecord([])].join(), 'true,false,false,false'); + t43('H43 tier: ⛔ a register that did not load stands NOTHING down', governedLandsOnRecord(TIER_S_43, { available: false, tierOf: null, recordTier: null }), false); + t43('H43 tier: …nor does one that loaded without the tier reader', governedLandsOnRecord(TIER_S_43, { available: true, tierOf: null, recordTier: 'S' }), false); + t43('H43 summary: the review leg is stated as Tier H only', saidBy('h43GovernedReviews', summaryLine({ governedPrs: 8, governedReviewCandidates: 5, governedReviewProbed: 5 }, 0)).includes('Tier H only'), true); // The coverage clause, both branches, cut out of the summary line so only // H43's own words can answer for H43. @@ -31529,12 +31721,57 @@ async function selfTest() { // copied into `counts`. That is the documented failure this enumeration was // built to end — `counts.x ?? 0` renders a missing key and a real zero // identically — and it reappears the moment a new row forgets the list. - t('H43: every count key rides the enumerated forwarding contract', ['governedPrs', 'governedReviewCandidates', 'governedReviewProbed', 'governedRegisterReason'].every((k) => SWEEP_COUNT_KEYS.includes(k)), true); - t('H43 summary: the triple is reported', saidBy('h43GovernedReviews', summaryLine({ governedPrs: 8, governedReviewCandidates: 5, governedReviewProbed: 5 }, 0)).includes('8 of the open PR(s)'), true); - t('H43 summary: …with the review leg named as a subset', saidBy('h43GovernedReviews', summaryLine({ governedPrs: 8, governedReviewCandidates: 5, governedReviewProbed: 5 }, 0)).includes('answered on 5 of 5'), true); - t('H43 summary: an unloadable register reports NOT MEASURED, never 0 of 0', saidBy('h43GovernedReviews', summaryLine({ governedRegisterReason: 'the pair is not installed' }, 0)).includes('NOT MEASURED — the pair is not installed'), true); - t('H43 summary: …and then claims no governed population at all', saidBy('h43GovernedReviews', summaryLine({ governedRegisterReason: 'the pair is not installed' }, 0)).includes('hit the governed register'), false); + t43('H43: every count key rides the enumerated forwarding contract', ['governedPrs', 'governedReviewCandidates', 'governedReviewProbed', 'governedRegisterReason'].every((k) => SWEEP_COUNT_KEYS.includes(k)), true); + t43('H43 summary: the triple is reported', saidBy('h43GovernedReviews', summaryLine({ governedPrs: 8, governedReviewCandidates: 5, governedReviewProbed: 5 }, 0)).includes('8 of the open PR(s)'), true); + t43('H43 summary: …with the review leg named as a subset of the PRs whose hand-off is due', saidBy('h43GovernedReviews', summaryLine({ governedPrs: 8, governedReviewCandidates: 5, governedHandoffRead: 5, governedHandoffDue: 5, governedReviewProbed: 5 }, 0)).includes('answered on 5 of those 5'), true); + t43('H43 summary: an unloadable register reports NOT MEASURED, never 0 of 0', saidBy('h43GovernedReviews', summaryLine({ governedRegisterReason: 'the pair is not installed' }, 0)).includes('NOT MEASURED — the pair is not installed'), true); + t43('H43 summary: …and then claims no governed population at all', saidBy('h43GovernedReviews', summaryLine({ governedRegisterReason: 'the pair is not installed' }, 0)).includes('hit the governed register'), false); + + // -- The hand-off timing (the maintainer's 「审核通过才会推送给我审核」) ------ + // + // A Tier H PR is a half state here only once the seat's hand-off is DUE: its + // `## 席位核对` ACCEPT and a `## Contract review` PASS naming the CURRENT head + // are both on the thread. Before both, nobody is owed a request and asking + // would put an unreviewed PR in front of a human — the very defect the + // filing cards measured — so the row is QUIET, and an unread thread is + // UNJUDGED rather than quiet-by-verdict. + const reading = (rows, head = HEAD43) => h43HandoffReading(rows, head); + const states = (...pairs) => pairs.map(([rows, head]) => reading(rows, head).state).join(); + t43('H43 hand-off: an unread thread (never consulted, or unreadable) is UNREAD — never before, never due', states([undefined], [null]), 'unread,unread'); + t43('H43 hand-off: an empty thread is BEFORE', reading([]).state, 'before'); + t43('H43 hand-off: ACCEPT alone is BEFORE — the PASS is still owed', [BEFORE43.state, BEFORE43.accepted, BEFORE43.passed].join(), 'before,true,false'); + t43('H43 hand-off: a PASS alone is BEFORE — the seat has not accepted', [reading(rows43(PASS43)).state, reading(rows43(PASS43)).passed].join(), 'before,true'); + t43('H43 hand-off: ACCEPT + a same-head PASS is DUE', [DUE43.state, DUE43.accepted, DUE43.passed, DUE43.review?.verdict].join(), 'due,true,true,PASS'); + t43('H43 hand-off: …in either thread order', reading(rows43(PASS43, ACCEPT43)).state, 'due'); + t43('H43 hand-off: ⛔ a FAIL on the current head is BEFORE — a patch round is owed, not a request', [reading(rows43(ACCEPT43, FAIL43)).state, reading(rows43(ACCEPT43, FAIL43)).review?.verdict].join(), 'before,FAIL'); + t43('H43 hand-off: ⛔ a PASS naming an OLDER head is BEFORE — the head moved after the PASS, so it is back to waiting', [reading(rows43(ACCEPT43, OLD_PASS43)).state, reading(rows43(ACCEPT43, OLD_PASS43)).review].join(), 'before,'); + t43('H43 hand-off: ⛔ a REWORK record beside a PASS is BEFORE — only ACCEPT hands off', reading(rows43(REWORK43, PASS43)).state, 'before'); + t43('H43 hand-off: the NEWEST seat record decides — ACCEPT then REWORK is BEFORE, REWORK then ACCEPT is DUE', states([rows43(ACCEPT43, REWORK43, PASS43)], [rows43(REWORK43, ACCEPT43, PASS43)]), 'before,due'); + t43('H43 hand-off: the newest PASS on the head decides, and a FAIL after it withdraws the hand-off', states([rows43(ACCEPT43, PASS43, FAIL43)], [rows43(ACCEPT43, FAIL43, PASS43)]), 'before,due'); + t43('H43 hand-off: a head too short to match is never DUE (the PASS cannot be bound to it)', reading(rows43(ACCEPT43, PASS43), '7cc8').state, 'before'); + // The verdict WORD — the one reader of it, three measured dialects. + t43('H43 verdict: the template line `**VERDICT: PASS**` reads PASS, and FAIL reads FAIL', [contractReviewVerdictOf(PASS43), contractReviewVerdictOf(FAIL43)].join(), 'PASS,FAIL'); + t43('H43 verdict: the heading-inline dialect `## Contract review (clause ②) — **PASS** · head …` reads PASS', contractReviewVerdictOf('## Contract review (clause ②) — **PASS** · head `7cc88889`\n\n### ① Derived judgments\n- none'), 'PASS'); + t43("H43 verdict: the queue guard's bare-line dialect (a line that is just `PASS`) reads PASS", contractReviewVerdictOf('## Contract review\n\nHead: `7cc88889e229`\nReviewed-by: `session_01SEAT`\n\nPASS'), 'PASS'); + t43('H43 verdict: the explicit VERDICT line wins over prose that happens to open with a verdict word', contractReviewVerdictOf('## Contract review\n\nPASS is what ① would have said, but ②:\n\n**VERDICT: FAIL**'), 'FAIL'); + t43('H43 verdict: a record with no verdict word, a body with no heading, and a mention in prose all read null', [contractReviewVerdictOf('## Contract review\n\nServed-tier: `CONTRACT_REVIEW_TIER`'), contractReviewVerdictOf('**VERDICT: PASS**'), contractReviewVerdictOf('## Contract review\n\nthe seat will post PASS later')].join(), ',,'); + t43('H43 verdict: the roster of words is PASS and FAIL, nothing else', CONTRACT_REVIEW_VERDICTS.join(), 'PASS,FAIL'); + t43('H43 verdict: latestContractReviewOnHead now carries the verdict beside id and sha', [latestContractReviewOnHead(rows43(ACCEPT43, PASS43), HEAD43)?.verdict, latestContractReviewOnHead(rows43(FAIL43), HEAD43)?.verdict, latestContractReviewOnHead(rows43(PASS43), '0123456789abcdef')].join(), 'PASS,FAIL,'); + // The row and the probe, gated on the reading. + t43('H43 row: ⛔ BEFORE the hand-off, nobody asked is NOT a half state — quiet', h43GovernedReviewRequestGap(h43pr(16), GOV43, APP43, [], BEFORE43), null); + t43('H43 row: ⛔ an UNREAD thread is UNJUDGED — quiet, never a finding', [h43GovernedReviewRequestGap(h43pr(16), GOV43, APP43, [], reading(undefined)), h43GovernedReviewRequestGap(h43pr(16), GOV43, APP43, [], reading(null)), h43GovernedReviewRequestGap(h43pr(16), GOV43, APP43, [], undefined)].join(), ',,'); + t43('H43 row: DUE and nobody asked -> finding', typeof h43GovernedReviewRequestGap(h43pr(16), GOV43, APP43, [], DUE43), 'string'); + t43('H43 row: …DUE and both covered -> clean, exactly as before', h43GovernedReviewRequestGap(h43pr(16, { requested: APP43 }), GOV43, APP43, [], DUE43), null); + t43('H43 row: …and the sentence names the two records that make the hand-off due, with the PASS comment id and the head it names', [h43row(h43pr(16), GOV43, APP43, []).includes('`## 席位核对` ACCEPT'), h43row(h43pr(16), GOV43, APP43, []).includes('`## Contract review` PASS naming head `7cc88889e229` (comment 901)')].join(), 'true,true'); + t43("H43 row: …and the remedy is the seat's hand-off stroke, `pr_request_reviewers`", h43row(h43pr(16), GOV43, APP43, []).includes("the seat's hand-off stroke — relay `pr_request_reviewers`"), true); + t43('H43 row: …quoting the ruling that set the timing', h43row(h43pr(16), GOV43, APP43, []).includes('审核通过才会推送给我审核'), true); + t43('H43 policy: the THREAD is read for every Tier H PR the cheap union leaves short — and for no other', [h43NeedsThreadRead(h43pr(17), GOV43, APP43), h43NeedsThreadRead(h43pr(17, { requested: APP43 }), GOV43, APP43), h43NeedsThreadRead(h43pr(17), [], APP43), h43NeedsThreadRead(h43pr(17), TIER_S_43, APP43)].join(), 'true,false,false,false'); + t43('H43 policy: the REVIEW page is bought only once the hand-off is DUE — before it, and on an unread thread, nothing is bought', [h43NeedsReviewProbe(h43pr(17), GOV43, APP43, DUE43), h43NeedsReviewProbe(h43pr(17), GOV43, APP43, BEFORE43), h43NeedsReviewProbe(h43pr(17), GOV43, APP43, reading(null)), h43NeedsReviewProbe(h43pr(17), GOV43, APP43)].join(), 'true,false,false,false'); + t43('H43 states: the roster of hand-off states is unread, before, due', H43_HANDOFF_STATES.join(), 'unread,before,due'); + t43('H43 summary: the hand-off reading is disclosed — threads read, how many are due, and that BEFORE is quiet by design', [saidBy('h43GovernedReviews', summaryLine({ governedPrs: 8, governedReviewCandidates: 5, governedHandoffRead: 4, governedHandoffDue: 2, governedReviewProbed: 2 }, 0)).includes('thread was read on 4 of 5'), saidBy('h43GovernedReviews', summaryLine({ governedPrs: 8, governedReviewCandidates: 5, governedHandoffRead: 4, governedHandoffDue: 2, governedReviewProbed: 2 }, 0)).includes("2 are past the seat's hand-off point"), saidBy('h43GovernedReviews', summaryLine({}, 0)).includes('nobody is owed a request and the row is quiet')].join(), 'true,true,true'); + t43('H43 summary: the two new count keys ride the enumerated forwarding contract', ['governedHandoffRead', 'governedHandoffDue'].every((k) => SWEEP_COUNT_KEYS.includes(k)), true); + t43('H43 floor: this battery is DECLARED on the frozen roster with a positive pin', Object.hasOwn(SELF_TEST_BATTERIES, BATTERY43) && SELF_TEST_BATTERIES[BATTERY43] > 0 && Object.isFrozen(SELF_TEST_BATTERIES), true); // -- H44 — a reading with no 取数时刻 (#14929, report-only) ----------------- // @@ -32491,7 +32728,7 @@ Doubles as the fire's **write self-check** (step 0). \`201\` is not the reading. b(BATTERY69, 'H69 row: …and refuses the label and the seat review', h69row(pr69(), []).includes('no label is written from here') && h69row(pr69(), []).includes('no seat may substitute an approving review'), true); b(BATTERY69, 'H69 row: names the approver asked', h69row(pr69(), []).includes('with `hotlong` requested for review'), true); b(BATTERY69, 'H69 row: names the governed surfaces it matched, from the matcher', h69row(pr69(), []).includes('`docs/adr/**`×1'), true); - b(BATTERY69, 'H69 row: says the request is a SUMMONS the record must precede', h69row(pr69(), []).includes('summons the approvers at `pr_create` and again at `pr_ready`'), true); + b(BATTERY69, 'H69 row: says the request is the seat\'s HAND-OFF the record must precede', h69row(pr69(), []).includes('is the seat\'s HAND-OFF, owed only after that record and a same-head'), true); const many69 = Array.from({ length: H69_SURFACE_LIST_CAP + 2 }, (_, i) => ({ glob: `g${i}/**`, files: ['f'] })); b(BATTERY69, 'H69 row: past the surface cap it counts the rest rather than growing', h69row(pr69(), [], HEAD69, many69).includes('+2 more'), true); b(BATTERY69, 'H69 row: a PR without a head sha still renders, naming the gap', h69row(pr69({ fields: { head: {} } }), []).includes('for head `unknown`'), true); diff --git a/scripts/pm/fleet-write/execute.mjs b/scripts/pm/fleet-write/execute.mjs index 59de60bfe26..ddd1bfaae27 100644 --- a/scripts/pm/fleet-write/execute.mjs +++ b/scripts/pm/fleet-write/execute.mjs @@ -212,12 +212,15 @@ * platform lists them (`resultOf`). Both legs are paced like every other * write verb. * - * ## The governed-review follow-up — after `pr_create` and after `pr_ready` + * ## The governed-review follow-up — after `pr_ready`, ⛔ never after `pr_create` * * Once a `governedReview` row's own requests have landed (`GOVERNED_REVIEW_OPS` - * in `ops.mjs`, its header section of this name: `pr_create` at the number the - * CREATE answered — ⛔ never one the request named — and `pr_ready` at the pull - * the action names), `requestGovernedReview` reads the pull's file list in + * in `ops.mjs`, its header section of this name: `pr_ready` alone, at the pull + * the action names — the draft open summons nobody, because a human is asked + * only at the seat's hand-off, `pr_request_reviewers` after its `## 席位核对` + * ACCEPT and a same-head `## Contract review` PASS, so a `pr_create` is the + * create plus the legs the caller spelled and nothing reads the pull's files + * there), `requestGovernedReview` reads the pull's file list in * full (`readPullFiles`: every page, a cap that reds as a truncated subset) and * hands the paths to `governedReviewPlan`, a pure function over the register: * `governedPathsIn` scoped to `payload.repo` through `repoIdForSlug`, then @@ -227,7 +230,7 @@ * reads the pull (its author, pending `requested_reviewers`, `assignees`) and * its submitted reviews, and the plan is the `GOVERNED_APPROVERS` accounts * NOT in that union — requested, assigned or reviewed, the coverage the H43 - * patrol reads — so a re-run, a `pr_ready` after a `pr_create`, or the + * patrol reads — so a re-run, a `pr_ready` after the seat's hand-off, or the * platform's own CODEOWNERS request landing first costs no write. The pull's * own author is never requested (GitHub answers 422) — a missing approver * who IS the author is ASSIGNED instead, the documented fallback. The writes @@ -1127,8 +1130,9 @@ export async function executeFleetWrite({ payload: raw, sender, token, api = DEF log(` ⚠ action ${i + 1} ${action.op}: no annotation — ${scrub(why, [token])}; a seat reading this run finds the card on the board instead.`); } } - // The governed-review follow-up (header): after the row's own requests landed, at the number the CREATE - // answered for pr_create — never one the request named — and at the pull the action names for pr_ready. + // The governed-review follow-up (header): after the row's own requests landed, at the pull the action names + // (`pr_ready`); a row without `pull` would fall back to the opener's ANSWERED number — never one the request + // named — and no such row carries `governedReview` today: the draft open summons nobody. if (stoppedAt === null && GOVERNED_REVIEW_OPS.includes(action.op)) { const pull = Number.isInteger(action.pull) ? action.pull : opener?.json?.number; let followed; @@ -1177,7 +1181,8 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'the workflow_dispatch: ONE POST to the dispatch endpoint of the allowlisted file with { ref: main } under the App token, a bodiless 204 lands as accepted, a 422 or 403 is a failed action, no other actions/* request leaves, no annotation': 7, 'the release.yml refresh: ONE POST to its dispatch endpoint carrying { ref: main, inputs: { refresh_version_pr: "true" } } under the App token, landing as accepted; force, false, absent, a string or any other input refused with ZERO requests, exit 2': 9, "the batch gap on the runner: a stroke carrying DEFAULT_BATCH_THRESHOLD or more content-creating actions is announced as ONE rule ⑤ batch before its first write — on an injected clock every write through the last content-creating one leaves a full DEFAULT_BATCH_GAP_MS after the one before, and the pacing is exactly what the seat budgets; below the threshold, after the batch and on a landing stroke rule ①'s gap; a stroke the job cannot hold refused with ZERO requests; an announcement the log cannot record refused before the first write": 10, - "the governed-review follow-up: after pr_create (at the number the create answered) and after pr_ready, the pull's file list is read in full; a Tier H hit requests review from every GOVERNED_APPROVERS account not already requested, assigned or reviewed — the pull's own author assigned instead, never requested; a Tier S or ungoverned diff sends nothing and says so; an unreadable or truncated list, an unreadable pull or review list, or a refused write is a FAILED action": 29, + // 29 → 30 as the follow-up left the draft open: three pr_create NEGATIVE pins (no read, no request, no assignee) replace the create-side runs. + "the governed-review follow-up: after pr_ready alone — a pr_create summons NOBODY and never reads the pull's files, because a human is asked only at the seat's hand-off (pr_request_reviewers after ACCEPT + a same-head PASS); after pr_ready the pull's file list is read in full, a Tier H hit requests review from every GOVERNED_APPROVERS account not already requested, assigned or reviewed — the pull's own author assigned instead, never requested; a Tier S or ungoverned diff sends nothing and says so; an unreadable or truncated list, an unreadable pull or review list, or a refused write is a FAILED action": 30, // 17 → 21 with the head fence: a stale head, the embedded copy never trusted, a run naming no pull, a pull unread. "the run_rerun_failed: the five fences judged HERE on the run and its pull, read immediately before ONE bodiless POST — the pull's current head by its own GET, never the run's embedded copy, and a run naming no pull refused by name; each refused with the POST not sent and accepted on the measured run; a run, pull, baseline or fence that fails is zero writes; the 201 confirmed only by reading attempt 2 back with its failed jobs queued or in progress, naming their job ids; attempt 2 never seen, a failed job completed, or a job list unread is FAILED; no annotation": 21, "the hierarchy link: sub_issue_add and sub_issue_remove read the parent's and the child's own answers, then the child's parent (add) or the parent's whole list (remove), immediately before ONE write whose body is the child's REST id from its own answer — never its number, never replace_parent; a pull, a transferred or unread card, a child under another parent, one the parent does not list, or a read that is neither answer is zero writes; a child already under this parent is a no-op landed on the list; the answer confirmed only when the parent's list reads the child back (or no longer does) inside the window; a refused write, a list that never settles or never reads whole is FAILED; no annotation, and the rows ride a stroke beside other ops": 20, @@ -1291,13 +1296,12 @@ export async function selfTest() { const pr = await run(base([{ op: 'pr_create', title: 'T', head: 'h', base: 'main' }, { op: 'issue_create', title: 'I', body: 'B', labels: ['l'] }]), { ...allowed, [`POST /repos/${REPO}/pulls`]: { status: 201, json: { number: 9, html_url: 'https://github.test/p/9' } }, - // The governed-review follow-up reads the created pull's files (its own battery below); an empty list is an ungoverned diff. - [`GET /repos/${REPO}/pulls/9/files`]: { status: 200, json: [] }, + // ⛔ No governed-review follow-up after a create (its own battery below): the pull's files are never read here. [`POST /repos/${REPO}/issues`]: { status: 201, json: { number: 10, html_url: 'https://github.test/i/10' } }, }, { file: paceFile }); t('pr_create sends draft: true whatever was asked', writes(pr.seen)[0].body, { title: 'T', head: 'h', base: 'main', draft: true }); t('issue_create sends title, body and the optional lists it was given', writes(pr.seen)[1].body, { title: 'I', body: 'B', labels: ['l'] }); - t('…and the rows carry the numbers', pr.rows.map((r) => r.result), ['#9 https://github.test/p/9', 'governed review: no governed row hit across 0 file(s) — no review requested', '#10 https://github.test/i/10']); + t('…and the rows carry the numbers — and nothing between them: a create owes no governed-review row', pr.rows.map((r) => r.result), ['#9 https://github.test/p/9', '#10 https://github.test/i/10']); } // ── the run ───────────────────────────────────────────────────────────── @@ -1450,8 +1454,8 @@ export async function selfTest() { const ASSIGN = (n) => `POST /repos/${REPO}/issues/${n}/assignees`; const create = (assignees, extra = []) => base([{ op: 'pr_create', title: 'T', head: 'claude/issue-1-x', base: 'main', body: 'B', ...(assignees ? { assignees } : {}) }, ...extra]); const opened = { status: 201, json: { number: 22700, html_url: `https://github.test/${REPO}/pull/22700` } }; - // The governed-review follow-up reads the created pull's files after the legs land (its own battery below); an empty list is an ungoverned diff. - const NOFILES = { [`GET /repos/${REPO}/pulls/22700/files`]: { status: 200, json: [] } }; + // ⛔ No governed-review follow-up runs after a create (its own battery below): the pull's files are never read here. + const FILES22700 = `GET /repos/${REPO}/pulls/22700/files`; const assignedTo = (logins, n = 22700) => ({ status: 201, json: { number: n, html_url: `https://github.test/${REPO}/pull/${n}`, assignees: logins.map((login) => ({ login })) } }); const legs = OPS.pr_create.requests({ op: 'pr_create', title: 'T', head: 'h', base: 'main', assignees: ['marchtian'] }, REPO); @@ -1459,10 +1463,10 @@ export async function selfTest() { t('the fill: the placeholder becomes the positive integer the previous answer carries at that field, and nothing else about the leg changes', fillFromAnswer(legs[1], { call: 'POST /x', status: 201, json: { number: 22700 } }).req, { ...legs[1], path: `/repos/${REPO}/issues/22700/assignees` }); t('…a previous answer without it — absent, a numeric string, zero, a float — or no previous answer at all, is a refusal to send that names the field', [{ json: {} }, { json: { number: '22700' } }, { json: { number: 0 } }, { json: { number: 1.5 } }, null].map((prev) => { const f = fillFromAnswer(legs[1], prev ? { call: 'POST /x', status: 201, ...prev } : null).fail; return typeof f === 'string' && f.includes('number'); }), [true, true, true, true, true]); - const ok = await run(create(['marchtian']), { ...allowed, ...NOFILES, [PULLS]: opened, [ASSIGN(22700)]: assignedTo(['marchtian']) }, { file: paceFile }); + const ok = await run(create(['marchtian']), { ...allowed, [PULLS]: opened, [ASSIGN(22700)]: assignedTo(['marchtian']) }, { file: paceFile }); t('the run: the create, then the assignee POST at the number the create ANSWERED, both under the App token — exit 0', [ok.exit, writes(ok.seen).map((s) => [s.call, s.body, s.auth === `Bearer ${TOKEN}`])], [EXIT_OK, [[PULLS, { title: 'T', head: 'claude/issue-1-x', base: 'main', body: 'B', draft: true }, true], [ASSIGN(22700), { assignees: ['marchtian'] }, true]]]); - t("…and the leg's row prints the pull and the assignees as the platform lists them, the governed-review verdict row behind it", ok.rows.map((r) => [r.action, r.op, r.call, r.status, r.result]), [[1, 'pr_create', PULLS, 201, `#22700 https://github.test/${REPO}/pull/22700`], [1, 'pr_create', ASSIGN(22700), 201, `#22700 https://github.test/${REPO}/pull/22700 · assignees marchtian`], [1, 'pr_create', `GET /repos/${REPO}/pulls/22700/files`, 200, 'governed review: no governed row hit across 0 file(s) — no review requested']]); - const plain = await run(create(null), { ...allowed, ...NOFILES, [PULLS]: opened }, { file: paceFile }); + t("…and the leg's row prints the pull and the assignees as the platform lists them, with NO governed-review row behind it and the pull's files never read", [ok.rows.map((r) => [r.action, r.op, r.call, r.status, r.result]), ok.seen.some((s) => s.call === FILES22700)], [[[1, 'pr_create', PULLS, 201, `#22700 https://github.test/${REPO}/pull/22700`], [1, 'pr_create', ASSIGN(22700), 201, `#22700 https://github.test/${REPO}/pull/22700 · assignees marchtian`]], false]); + const plain = await run(create(null), { ...allowed, [PULLS]: opened }, { file: paceFile }); t('no assignees, no second request: the create alone, as before', [plain.exit, writes(plain.seen).map((s) => s.call)], [EXIT_OK, [PULLS]]); const dropped = await run(create(['marchtian', 'nobody-here'], [{ op: 'comment', issue: 1, body: 'after' }]), { ...allowed, [PULLS]: opened, [ASSIGN(22700)]: assignedTo(['marchtian']), [`POST /repos/${REPO}/issues/1/comments`]: { status: 201, json: { id: 1 } } }); t('⛔ a 201 whose answer lists fewer logins than sent is a FAILED action naming the one not assigned — never a quiet partial — and the stroke stops there', [dropped.exit, dropped.rows[1]?.result.includes('nobody-here not assigned'), dropped.rows[1]?.result.includes('lists marchtian'), writes(dropped.seen).length, dropped.summary.includes('1 later action(s) NOT attempted')], [EXIT_ACTION_FAILED, true, true, 2, true], dropped.rows[1]?.result); @@ -1480,10 +1484,10 @@ export async function selfTest() { const both = OPS.pr_create.requests({ op: 'pr_create', title: 'T', head: 'h', base: 'main', assignees: ['marchtian'], labels: ['skip-changeset'] }, REPO); t('the table half: with labels the row carries a POST to the issue-labels path at the placeholder, fromAnswer number, the names as labelled — after the assignees leg when both are sent', both.map((r) => [r.verb, r.path, r.fromAnswer ?? null, r.assigned ?? null, r.labelled ?? null]), [['POST', `/repos/${REPO}/pulls`, null, null, null], ['POST', `/repos/${REPO}/issues/${answerPlaceholder('number')}/assignees`, 'number', ['marchtian'], null], ['POST', `/repos/${REPO}/issues/${answerPlaceholder('number')}/labels`, 'number', null, ['skip-changeset']]]); // The assignee answer here carries a DIFFERENT number: the label leg must still land on the pull the CREATE answered. - const labelled = await run(createWith({ assignees: ['marchtian'], labels: ['skip-changeset'] }), { ...allowed, ...NOFILES, [PULLS]: opened, [ASSIGN(22700)]: assignedTo(['marchtian'], 99999), [LABEL(22700)]: labelsOn(['skip-changeset', 'documentation']) }, { file: paceFile }); + const labelled = await run(createWith({ assignees: ['marchtian'], labels: ['skip-changeset'] }), { ...allowed, [PULLS]: opened, [ASSIGN(22700)]: assignedTo(['marchtian'], 99999), [LABEL(22700)]: labelsOn(['skip-changeset', 'documentation']) }, { file: paceFile }); t("the run: the create, the assignee POST, then the label POST — at the number the CREATE answered, not the assignee answer's — exit 0", [labelled.exit, writes(labelled.seen).map((s) => [s.call, s.body])], [EXIT_OK, [[PULLS, { title: 'T', head: 'claude/issue-1-x', base: 'main', body: 'B', draft: true }], [ASSIGN(22700), { assignees: ['marchtian'] }], [LABEL(22700), { labels: ['skip-changeset'] }]]]); t("…and the leg's row prints the labels as the platform lists them — more than sent (the Auto Label workflow's additive writes) is a landing, never a failure", labelled.rows[2], { action: 1, op: 'pr_create', call: LABEL(22700), status: 200, result: '2 label(s) now on the pull · labels skip-changeset, documentation' }); - const labelsOnly = await run(createWith({ labels: ['skip-changeset'] }), { ...allowed, ...NOFILES, [PULLS]: opened, [LABEL(22700)]: labelsOn(['skip-changeset']) }, { file: paceFile }); + const labelsOnly = await run(createWith({ labels: ['skip-changeset'] }), { ...allowed, [PULLS]: opened, [LABEL(22700)]: labelsOn(['skip-changeset']) }, { file: paceFile }); t('labels without assignees: the create, then the label POST alone', [labelsOnly.exit, writes(labelsOnly.seen).map((s) => s.call)], [EXIT_OK, [PULLS, LABEL(22700)]]); const unlabelled = await run(createWith({ labels: ['skip-changeset', 'no-such-label'] }, [{ op: 'comment', issue: 1, body: 'after' }]), { ...allowed, [PULLS]: opened, [LABEL(22700)]: labelsOn(['skip-changeset']), [`POST /repos/${REPO}/issues/1/comments`]: { status: 201, json: { id: 1 } } }); t('⛔ a 200 whose answer lacks a name sent is a FAILED action naming it — never a quiet partial — and the stroke stops there', [unlabelled.exit, unlabelled.rows[1]?.result.includes('no-such-label not applied'), unlabelled.rows[1]?.result.includes('lists skip-changeset'), writes(unlabelled.seen).length, unlabelled.summary.includes('1 later action(s) NOT attempted')], [EXIT_ACTION_FAILED, true, true, 2, true], unlabelled.rows[1]?.result); @@ -1494,7 +1498,7 @@ export async function selfTest() { } // ── the governed-review follow-up ─────────────────────────────────────── - battery("the governed-review follow-up: after pr_create (at the number the create answered) and after pr_ready, the pull's file list is read in full; a Tier H hit requests review from every GOVERNED_APPROVERS account not already requested, assigned or reviewed — the pull's own author assigned instead, never requested; a Tier S or ungoverned diff sends nothing and says so; an unreadable or truncated list, an unreadable pull or review list, or a refused write is a FAILED action"); + battery("the governed-review follow-up: after pr_ready alone — a pr_create summons NOBODY and never reads the pull's files, because a human is asked only at the seat's hand-off (pr_request_reviewers after ACCEPT + a same-head PASS); after pr_ready the pull's file list is read in full, a Tier H hit requests review from every GOVERNED_APPROVERS account not already requested, assigned or reviewed — the pull's own author assigned instead, never requested; a Tier S or ungoverned diff sends nothing and says so; an unreadable or truncated list, an unreadable pull or review list, or a refused write is a FAILED action"); { const PULLS = `POST /repos/${REPO}/pulls`; const N = 25050; @@ -1503,6 +1507,7 @@ export async function selfTest() { const REVIEWS = `GET /repos/${REPO}/pulls/${N}/reviews`; const REQUEST = `POST /repos/${REPO}/pulls/${N}/requested_reviewers`; const ASSIGN = `POST /repos/${REPO}/issues/${N}/assignees`; + const GRAPHQL = 'POST /graphql'; // The approvers are READ from the constant, never restated: a change there moves every expectation here with it. const [A1, A2] = GOVERNED_APPROVERS; const URL = `https://github.test/${REPO}/pull/${N}`; @@ -1513,11 +1518,14 @@ export async function selfTest() { const requestedAnswer = (logins) => ({ status: 201, json: { number: N, html_url: URL, requested_reviewers: logins.map((login) => ({ login })) } }); const assignedAnswer = (logins) => ({ status: 201, json: { number: N, html_url: URL, assignees: logins.map((login) => ({ login })) } }); const create = (extra = []) => base([{ op: 'pr_create', title: 'T', head: 'claude/issue-1-x', base: 'main', body: 'B' }, ...extra]); - const tierH = { [PULLS]: opened, [FILES]: files('docs/adr/0140-x.md', 'packages/core/src/a.ts'), [PULL]: pullOf(), [REVIEWS]: reviewsBy() }; + const ready = (extra = []) => base([{ op: 'pr_ready', pull: N }, ...extra]); + const readied = { [GRAPHQL]: { status: 200, json: { data: { markPullRequestReadyForReview: { pullRequest: { number: N, isDraft: false } } } } } }; + const tierHFiles = files('docs/adr/0140-x.md', 'packages/core/src/a.ts'); + const tierH = { [PULL]: pullOf(), ...readied, [FILES]: tierHFiles, [REVIEWS]: reviewsBy() }; const pick = (plan) => [plan.tier, plan.toRequest, plan.toAssign, plan.covered]; // The table half. - t('the table half: GOVERNED_REVIEW_OPS is exactly pr_create and pr_ready — derived from the rows, never listed — and both spend issues (the assignee fallback) as alsoSpends', [GOVERNED_REVIEW_OPS, GOVERNED_REVIEW_OPS.every((op) => (OPS[op].alsoSpends ?? []).includes('issues'))], [['pr_create', 'pr_ready'], true]); + t("⭐ the table half: GOVERNED_REVIEW_OPS is exactly pr_ready — derived from the rows, never listed — pr_create is OFF it (the draft open summons nobody; the hand-off is the seat's pr_request_reviewers), and the row spends issues (the assignee fallback) as alsoSpends", [GOVERNED_REVIEW_OPS, OPS.pr_create.governedReview ?? null, GOVERNED_REVIEW_OPS.every((op) => (OPS[op].alsoSpends ?? []).includes('issues'))], [['pr_ready'], null, true]); t('…governedReviewRequests spells ONE requested_reviewers POST carrying the logins as requested and ONE issue-assignees POST carrying them as assigned; an empty list spells nothing', [governedReviewRequests(REPO, N, { reviewers: [A1, A2], assignees: [A2] }).map((r) => [r.verb, r.path, r.body, r.requested ?? null, r.assigned ?? null]), governedReviewRequests(REPO, N), governedReviewRequests(REPO, N, { reviewers: [], assignees: [] })], [[['POST', `/repos/${REPO}/pulls/${N}/requested_reviewers`, { reviewers: [A1, A2] }, [A1, A2], null], ['POST', `/repos/${REPO}/issues/${N}/assignees`, { assignees: [A2] }, null, [A2]]], [], []]); // The plan — pure, over the real register. t('the plan: a Tier H path with nobody covered requests every GOVERNED_APPROVERS account, in its order, and assigns nobody', pick(governedReviewPlan({ paths: ['docs/adr/0140-x.md', 'README.md'], repo: REPO })), [GOVERNED_TIER_H, [A1, A2], [], []]); @@ -1531,43 +1539,46 @@ export async function selfTest() { t('…the approver list is INJECTED from GOVERNED_APPROVERS, never baked in: another list is requested in its place', governedReviewPlan({ paths: ['AGENTS.md'], repo: REPO, approvers: ['someone-else'] }).toRequest, ['someone-else']); t('…the verdict words name the tier, the rows hit, who is covered and what is sent', [governedReviewText(governedReviewPlan({ paths: ['docs/adr/0140-x.md'], repo: REPO, requested: [A1] }), { repo: REPO, files: 1 }), governedReviewText(governedReviewPlan({ paths: ['x.ts'], repo: REPO }), { repo: REPO, files: 1 })], [`governed review: Tier H (\`docs/adr/**\`×1) — \`${A1}\` already covered; requesting \`${A2}\``, 'governed review: no governed row hit across 1 file(s) — no review requested']); - // The run — pr_create. - const both = await run(create(), { ...allowed, ...tierH, [REQUEST]: requestedAnswer([A1, A2]) }); - t('⭐ pr_create on a Tier H diff: the create, then ONE requested_reviewers POST naming BOTH approvers, at the number the create ANSWERED, under the App token — exit 0', [both.exit, writes(both.seen).map((s) => [s.call, s.body, s.auth === `Bearer ${TOKEN}`])], [EXIT_OK, [[PULLS, { title: 'T', head: 'claude/issue-1-x', base: 'main', body: 'B', draft: true }, true], [REQUEST, { reviewers: [A1, A2] }, true]]], both.logs.join(' | ')); - t('…the file list, the pull and its reviews were read first, in that order, and the rows say the verdict and the landing', [both.seen.map((s) => s.call), both.rows.map((r) => [r.action, r.op, r.call, r.status, r.result])], [[PERM, PULLS, FILES, PULL, REVIEWS, REQUEST], [[1, 'pr_create', PULLS, 201, `#${N} ${URL}`], [1, 'pr_create', FILES, 200, `governed review: Tier H (\`docs/adr/**\`×1) — requesting \`${A1}\`, \`${A2}\``], [1, 'pr_create', REQUEST, 201, `#${N} ${URL} · review requested from ${A1}, ${A2}`]]]); - const none = await run(create([{ op: 'comment', issue: 1, body: 'after' }]), { ...allowed, [PULLS]: opened, [FILES]: files('packages/core/src/a.ts', 'README.md'), [`POST /repos/${REPO}/issues/1/comments`]: { status: 201, json: { id: 1 } } }); - t('a non-governed diff: the file list is read, NOTHING is requested, the row says so, and the later action runs', [none.exit, writes(none.seen).map((s) => s.call), none.rows[1]?.result, none.seen.some((s) => s.call === PULL)], [EXIT_OK, [PULLS, `POST /repos/${REPO}/issues/1/comments`], 'governed review: no governed row hit across 2 file(s) — no review requested', false]); - const tierS = await run(create(), { ...allowed, [PULLS]: opened, [FILES]: files('.claude/skills/pm-dispatch/SKILL.md') }); - t("a Tier S diff (.claude/** alone) requests nobody — it lands on a seat's review of record", [tierS.exit, writes(tierS.seen).map((s) => s.call), tierS.rows[1]?.result], [EXIT_OK, [PULLS], "governed review: Tier S (`.claude/**`×1) lands on a seat's review of record — no review requested"]); - const authored = await run(create(), { ...allowed, ...tierH, [PULL]: pullOf({ author: A2 }), [REQUEST]: requestedAnswer([A1]), [ASSIGN]: assignedAnswer([A2]) }); - t('⭐ the author-identity branch: a pull authored by an approver requests the OTHER approver and ASSIGNS the author — never a request naming the author', [authored.exit, writes(authored.seen).map((s) => [s.call, s.body]), authored.rows.map((r) => r.result).slice(1)], [EXIT_OK, [[PULLS, { title: 'T', head: 'claude/issue-1-x', base: 'main', body: 'B', draft: true }], [REQUEST, { reviewers: [A1] }], [ASSIGN, { assignees: [A2] }]], [`governed review: Tier H (\`docs/adr/**\`×1) — requesting \`${A1}\` and assigning the author \`${A2}\` (a request naming the author is a 422)`, `#${N} ${URL} · review requested from ${A1}`, `#${N} ${URL} · assignees ${A2}`]]); - const covered = await run(create(), { ...allowed, ...tierH, [PULL]: pullOf({ requested: [A1] }), [REVIEWS]: reviewsBy(A2) }); - t('⭐ idempotent: one approver already requested and the other already reviewed — ZERO writes beyond the create, the row says nothing to send, exit 0', [covered.exit, writes(covered.seen).map((s) => s.call), covered.rows[1]?.result], [EXIT_OK, [PULLS], `governed review: Tier H (\`docs/adr/**\`×1) — \`${A1}\`, \`${A2}\` already requested, assigned or reviewed; nothing to send`]); - const partial = await run(create(), { ...allowed, ...tierH, [PULL]: pullOf({ assignees: [A2] }), [REQUEST]: requestedAnswer([A1]) }); + // The run — pr_create: NOTHING. The draft open summons nobody; the fake platform is primed with every answer the old follow-up would have asked for, and none is asked. + const draft = await run(create([{ op: 'comment', issue: 1, body: 'after' }]), { ...allowed, [PULLS]: opened, [FILES]: tierHFiles, [PULL]: pullOf(), [REVIEWS]: reviewsBy(), [REQUEST]: requestedAnswer([A1, A2]), [`POST /repos/${REPO}/issues/1/comments`]: { status: 201, json: { id: 1 } } }); + t('⭐ pr_create on a Tier H diff: the create and NOTHING else — no file list, pull or review read, no request, no assignee — and the later action runs, exit 0', [draft.exit, draft.seen.map((s) => s.call), writes(draft.seen).map((s) => s.call), draft.rows.length, draft.rows[0]], [EXIT_OK, [PERM, PULLS, `POST /repos/${REPO}/issues/1/comments`], [PULLS, `POST /repos/${REPO}/issues/1/comments`], 2, { action: 1, op: 'pr_create', call: PULLS, status: 201, result: `#${N} ${URL}` }], draft.logs.join(' | ')); + const draftAuthored = await run(create(), { ...allowed, [PULLS]: opened, [FILES]: tierHFiles, [PULL]: pullOf({ author: A2 }), [REVIEWS]: reviewsBy(), [ASSIGN]: assignedAnswer([A2]) }); + t("…an approver-authored pull is not assigned at the open either: the author-is-approver fallback is the seat's hand-off stroke (`assign`), never the create's", [draftAuthored.exit, writes(draftAuthored.seen).map((s) => s.call), draftAuthored.seen.some((s) => [ASSIGN, REQUEST, FILES, REVIEWS].includes(s.call))], [EXIT_OK, [PULLS], false]); + const numberless = await run(create([{ op: 'comment', issue: 1, body: 'after' }]), { ...allowed, [PULLS]: { status: 201, json: { html_url: 'https://github.test/x' } }, [`POST /repos/${REPO}/issues/1/comments`]: { status: 201, json: { id: 1 } } }); + t('a create whose answer carries no number owes no follow-up, so nothing fails on its account: the create row stands, no file list is read, the later action runs', [numberless.exit, numberless.rows.length, numberless.seen.some((s) => s.call.includes('/files'))], [EXIT_OK, 2, false], numberless.logs.join(' | ')); + + // The run — pr_ready, the one follow-up left. + const both = await run(ready(), { ...allowed, ...tierH, [REQUEST]: requestedAnswer([A1, A2]) }); + t('⭐ pr_ready on a Tier H pull: the node id, the mutation, then ONE requested_reviewers POST naming BOTH approvers at the pull the action names, under the App token — exit 0', [both.exit, writes(both.seen).map((s) => [s.call, s.auth === `Bearer ${TOKEN}`]), writes(both.seen)[1]?.body], [EXIT_OK, [[GRAPHQL, true], [REQUEST, true]], { reviewers: [A1, A2] }], both.logs.join(' | ')); + t('…the file list, the pull and its reviews were read first, in that order, and the rows say the verdict and the landing', [both.seen.map((s) => s.call), both.rows.slice(1).map((r) => [r.action, r.op, r.call, r.status, r.result])], [[PERM, PULL, GRAPHQL, FILES, PULL, REVIEWS, REQUEST], [[1, 'pr_ready', FILES, 200, `governed review: Tier H (\`docs/adr/**\`×1) — requesting \`${A1}\`, \`${A2}\``], [1, 'pr_ready', REQUEST, 201, `#${N} ${URL} · review requested from ${A1}, ${A2}`]]]); + const none = await run(ready([{ op: 'comment', issue: 1, body: 'after' }]), { ...allowed, [PULL]: pullOf(), ...readied, [FILES]: files('packages/core/src/a.ts', 'README.md'), [`POST /repos/${REPO}/issues/1/comments`]: { status: 201, json: { id: 1 } } }); + t('a non-governed diff: the file list is read, NOTHING is requested, the row says so, and the later action runs', [none.exit, writes(none.seen).map((s) => s.call), none.rows[1]?.result, none.seen.filter((s) => s.call === PULL).length], [EXIT_OK, [GRAPHQL, `POST /repos/${REPO}/issues/1/comments`], 'governed review: no governed row hit across 2 file(s) — no review requested', 1]); + const tierS = await run(ready(), { ...allowed, [PULL]: pullOf(), ...readied, [FILES]: files('.claude/skills/pm-dispatch/SKILL.md') }); + t("a Tier S diff (.claude/** alone) requests nobody — it lands on a seat's review of record", [tierS.exit, writes(tierS.seen).map((s) => s.call), tierS.rows[1]?.result], [EXIT_OK, [GRAPHQL], "governed review: Tier S (`.claude/**`×1) lands on a seat's review of record — no review requested"]); + const authored = await run(ready(), { ...allowed, ...tierH, [PULL]: pullOf({ author: A2 }), [REQUEST]: requestedAnswer([A1]), [ASSIGN]: assignedAnswer([A2]) }); + t('⭐ the author-identity branch: a pull authored by an approver requests the OTHER approver and ASSIGNS the author — never a request naming the author', [authored.exit, writes(authored.seen).slice(1).map((s) => [s.call, s.body]), authored.rows.map((r) => r.result).slice(1)], [EXIT_OK, [[REQUEST, { reviewers: [A1] }], [ASSIGN, { assignees: [A2] }]], [`governed review: Tier H (\`docs/adr/**\`×1) — requesting \`${A1}\` and assigning the author \`${A2}\` (a request naming the author is a 422)`, `#${N} ${URL} · review requested from ${A1}`, `#${N} ${URL} · assignees ${A2}`]]); + const covered = await run(ready(), { ...allowed, ...tierH, [PULL]: pullOf({ requested: [A1] }), [REVIEWS]: reviewsBy(A2) }); + t("⭐ idempotent — the seat's hand-off already made: one approver requested and the other already reviewed — ZERO writes beyond the mutation, the row says nothing to send, exit 0", [covered.exit, writes(covered.seen).map((s) => s.call), covered.rows[1]?.result], [EXIT_OK, [GRAPHQL], `governed review: Tier H (\`docs/adr/**\`×1) — \`${A1}\`, \`${A2}\` already requested, assigned or reviewed; nothing to send`]); + const partial = await run(ready(), { ...allowed, ...tierH, [PULL]: pullOf({ assignees: [A2] }), [REQUEST]: requestedAnswer([A1]) }); t('…an assigned approver counts as covered, and only the missing one is requested', [partial.exit, writes(partial.seen).map((s) => [s.call, s.body]).slice(1)], [EXIT_OK, [[REQUEST, { reviewers: [A1] }]]]); - // The run — pr_ready. - const readyH = await run(base([{ op: 'pr_ready', pull: N }, { op: 'comment', issue: 1, body: 'after' }]), { ...allowed, [PULL]: pullOf(), 'POST /graphql': { status: 200, json: { data: { markPullRequestReadyForReview: { pullRequest: { number: N, isDraft: false } } } } }, [FILES]: files('skills/objectstack-ui/SKILL.md'), [REVIEWS]: reviewsBy(), [REQUEST]: requestedAnswer([A1, A2]), [`POST /repos/${REPO}/issues/1/comments`]: { status: 201, json: { id: 1 } } }); - t('⭐ pr_ready on a Tier H pull: the mutation lands, then the same follow-up at the pull the action names — both approvers requested, the later action still runs', [readyH.exit, readyH.seen.map((s) => s.call), writes(readyH.seen)[1]?.body], [EXIT_OK, [PERM, PULL, 'POST /graphql', FILES, PULL, REVIEWS, REQUEST, `POST /repos/${REPO}/issues/1/comments`], { reviewers: [A1, A2] }]); - const readyFailed = await run(base([{ op: 'pr_ready', pull: N }]), { ...allowed, [PULL]: pullOf(), 'POST /graphql': { status: 200, json: { data: { markPullRequestReadyForReview: { pullRequest: { number: N, isDraft: true } } } } }, [FILES]: files('docs/adr/0140-x.md') }); + const readyFailed = await run(ready(), { ...allowed, [PULL]: pullOf(), [GRAPHQL]: { status: 200, json: { data: { markPullRequestReadyForReview: { pullRequest: { number: N, isDraft: true } } } } }, [FILES]: files('docs/adr/0140-x.md') }); t('a pr_ready that did not land runs no follow-up: no file list read, no request', [readyFailed.exit, readyFailed.seen.some((s) => s.call === FILES)], [EXIT_ACTION_FAILED, false]); // The refusals. - const unread = await run(create([{ op: 'comment', issue: 1, body: 'after' }]), { ...allowed, [PULLS]: opened, [FILES]: { status: 503, json: { message: 'down' } }, [`POST /repos/${REPO}/issues/1/comments`]: { status: 201, json: { id: 1 } } }); - t('⛔ an unreadable file list is a FAILED action — the pull is open (its row 201), the follow-up row names the read, exit 5, the later action never attempted — never a quiet "not governed"', [unread.exit, unread.rows.map((r) => [r.status, r.result.startsWith('FAILED') ? r.result.includes('file list could not be read') : r.result]), writes(unread.seen).map((s) => s.call), unread.summary.includes('1 later action(s) NOT attempted')], [EXIT_ACTION_FAILED, [[201, `#${N} ${URL}`], [503, true]], [PULLS], true], unread.rows[1]?.result); + const unread = await run(ready([{ op: 'comment', issue: 1, body: 'after' }]), { ...allowed, [PULL]: pullOf(), ...readied, [FILES]: { status: 503, json: { message: 'down' } }, [`POST /repos/${REPO}/issues/1/comments`]: { status: 201, json: { id: 1 } } }); + t('⛔ an unreadable file list is a FAILED action — the pull is ready (its row landed), the follow-up row names the read, exit 5, the later action never attempted — never a quiet "not governed"', [unread.exit, unread.rows.map((r) => [r.status, r.result.startsWith('FAILED') ? r.result.includes('file list could not be read') : 'landed']), writes(unread.seen).map((s) => s.call), unread.summary.includes('1 later action(s) NOT attempted')], [EXIT_ACTION_FAILED, [[200, 'landed'], [503, true]], [GRAPHQL], true], unread.rows[1]?.result); const page = (n) => files(...Array.from({ length: n }, (_, i) => `packages/core/src/f${i}.ts`)); - const twoPages = await run(create(), { ...allowed, [PULLS]: opened, [FILES]: [page(100), files('docs/adr/0140-x.md')], [PULL]: pullOf(), [REVIEWS]: reviewsBy(), [REQUEST]: requestedAnswer([A1, A2]) }); - t('the walk reads EVERY page: a governed path on the second page is found, and the pages were asked in order', [twoPages.exit, twoPages.seen.filter((s) => s.call === FILES).length, writes(twoPages.seen).map((s) => s.call)], [EXIT_OK, 2, [PULLS, REQUEST]]); - const truncated = await run(create(), { ...allowed, [PULLS]: opened, [FILES]: Array.from({ length: GOVERNED_REVIEW_FILES_PAGE_CAP }, () => page(100)) }); + const twoPages = await run(ready(), { ...allowed, [PULL]: pullOf(), ...readied, [FILES]: [page(100), files('docs/adr/0140-x.md')], [REVIEWS]: reviewsBy(), [REQUEST]: requestedAnswer([A1, A2]) }); + t('the walk reads EVERY page: a governed path on the second page is found, and the pages were asked in order', [twoPages.exit, twoPages.seen.filter((s) => s.call === FILES).length, writes(twoPages.seen).map((s) => s.call)], [EXIT_OK, 2, [GRAPHQL, REQUEST]]); + const truncated = await run(ready(), { ...allowed, [PULL]: pullOf(), ...readied, [FILES]: Array.from({ length: GOVERNED_REVIEW_FILES_PAGE_CAP }, () => page(100)) }); t('⛔ a walk that would need more than the page cap is a FAILED action naming the cap — a truncated list is a subset, never "nothing governed"', [truncated.exit, truncated.seen.filter((s) => s.call === FILES).length, truncated.rows[1]?.result.includes(`${GOVERNED_REVIEW_FILES_PAGE_CAP}-page cap`)], [EXIT_ACTION_FAILED, GOVERNED_REVIEW_FILES_PAGE_CAP, true], truncated.rows[1]?.result); - const short = await run(create(), { ...allowed, ...tierH, [REQUEST]: requestedAnswer([A1]) }); + const short = await run(ready(), { ...allowed, ...tierH, [REQUEST]: requestedAnswer([A1]) }); t('⛔ a 201 whose answer lists fewer reviewers than sent is a FAILED action naming the one not requested — never a quiet partial', [short.exit, short.rows[2]?.result.includes(`${A2} not requested`), short.rows[2]?.result.includes(`lists ${A1}`)], [EXIT_ACTION_FAILED, true, true], short.rows[2]?.result); - const refused = await run(create(), { ...allowed, ...tierH, [REQUEST]: { status: 422, json: { message: 'Reviews may only be requested from collaborators.' } } }); - t("a refused request is a FAILED action with the platform's sentence, the pull already open", [refused.exit, refused.rows.map((r) => r.status), refused.rows[2]?.result.includes('Reviews may only be requested from collaborators')], [EXIT_ACTION_FAILED, [201, 200, 422], true]); - const noPull = await run(create(), { ...allowed, ...tierH, [PULL]: { status: 503, json: { message: 'down' } } }); - const noReviews = await run(create(), { ...allowed, ...tierH, [REVIEWS]: { status: 503, json: { message: 'down' } } }); + const refused = await run(ready(), { ...allowed, ...tierH, [REQUEST]: { status: 422, json: { message: 'Reviews may only be requested from collaborators.' } } }); + t("a refused request is a FAILED action with the platform's sentence, the pull already ready", [refused.exit, refused.rows.map((r) => r.status), refused.rows[2]?.result.includes('Reviews may only be requested from collaborators')], [EXIT_ACTION_FAILED, [200, 200, 422], true]); + const noPull = await run(ready(), { ...allowed, ...tierH, [PULL]: [pullOf(), { status: 503, json: { message: 'down' } }] }); + const noReviews = await run(ready(), { ...allowed, ...tierH, [REVIEWS]: { status: 503, json: { message: 'down' } } }); t('an unreadable pull or review list on a Tier H hit is FAILED naming the read — never a request sent blind, never a quiet skip', [noPull.exit, noPull.rows[1]?.result.includes('could not be re-read'), noReviews.exit, noReviews.rows[1]?.result.includes('reviews could not be read'), [...noPull.seen, ...noReviews.seen].some((s) => s.call === REQUEST)], [EXIT_ACTION_FAILED, true, EXIT_ACTION_FAILED, true, false]); - const numberless = await run(create([{ op: 'comment', issue: 1, body: 'after' }]), { ...allowed, [PULLS]: { status: 201, json: { html_url: 'https://github.test/x' } }, [`POST /repos/${REPO}/issues/1/comments`]: { status: 201, json: { id: 1 } } }); - t('a create whose answer carries no number: the follow-up cannot run — FAILED, its row says not sent, no file list read, the later action never attempted', [numberless.exit, numberless.rows[1]?.status, numberless.rows[1]?.result.includes('without a positive integer number'), numberless.seen.some((s) => s.call.includes('/files')), writes(numberless.seen).map((s) => s.call)], [EXIT_ACTION_FAILED, 'not sent', true, false, [PULLS]]); - t('no annotation is emitted for the follow-up — neither row is an annotated op', [both.annotations, authored.logs.some((l) => l.startsWith('::notice'))], [[], false]); + t('no annotation is emitted for the follow-up — pr_ready is not an annotated op', [both.annotations, authored.logs.some((l) => l.startsWith('::notice'))], [[], false]); } // ── the transfer ──────────────────────────────────────────────────────── @@ -1998,8 +2009,7 @@ export async function selfTest() { [`POST /repos/${REPO}/issues/1/labels`]: { status: 200, json: [{ name: 'x' }] }, [`PATCH /repos/${REPO}/issues/1`]: { status: 200, json: { number: 1, html_url: issueUrl(REPO, 1) } }, [`POST /repos/${REPO}/pulls`]: { status: 201, json: { number: 9, html_url: `https://github.test/${REPO}/pull/9` } }, - // The governed-review follow-up reads the created pull's files (its own battery); an empty list is an ungoverned diff, and it emits no note either. - [`GET /repos/${REPO}/pulls/9/files`]: { status: 200, json: [] }, + // ⛔ No governed-review follow-up after a create (its own battery): the pull's files are never read, and no note is emitted. }, { file: paceFile }); t('no other op emits one — a comment, labels, a patch and a NEW PULL REQUEST all landed, zero workflow commands, zero notes', [quiet.exit, notices(quiet), quiet.annotations], [EXIT_OK, [], []]); @@ -2096,7 +2106,7 @@ export async function selfTest() { `✓ fleet-write/execute self-test: ${cases.length} cases pass across ${declared.length} batteries — the sender gate from the target repo's answer, ` + 'every op as the request the table declares, stop at the first failure with later actions untouched, the idempotent label DELETE, the GraphQL ' + 'ops behind a node-id read, a pull mutation landing only when its answer shows the state it asked for (armed or queued, off, ready, draft), a base sync confirmed by the head leaving its expected sha or measured as a no-op, an assignees leg sent to the number the create answered and landing only when its answer lists every login sent, ' + - 'a governed-review follow-up after pr_create and pr_ready that reads the pull\'s whole file list and, on a Tier H hit alone, requests every GOVERNED_APPROVERS account not already covered (the author assigned instead), zero writes when covered and a FAILED action when the list cannot be read whole, a transfer gated on both repositories and landing only on its target, ' + + 'a governed-review follow-up after pr_ready alone (the draft open summons nobody — a human is asked at the seat\'s hand-off) that reads the pull\'s whole file list and, on a Tier H hit alone, requests every GOVERNED_APPROVERS account not already covered (the author assigned instead), zero writes when covered and a FAILED action when the list cannot be read whole, a transfer gated on both repositories and landing only on its target, ' + "a failed-jobs re-run fenced on the run and its pull's own current head, read just before its one POST (zero writes on any fence), and landing only when attempt 2 reads back with its failed jobs queued or in progress, " + "an issue-hierarchy link fenced on its two cards, sent as the child's REST id and landing only when the parent's list reads it back, one summary row per request, " + "one annotation per created or moved card carrying the number the platform answered, read back by the reader's own parser, a stroke at or " + diff --git a/scripts/pm/fleet-write/ops.mjs b/scripts/pm/fleet-write/ops.mjs index a867648857b..2abc59798c1 100644 --- a/scripts/pm/fleet-write/ops.mjs +++ b/scripts/pm/fleet-write/ops.mjs @@ -85,20 +85,31 @@ * neither is the first request of its action; `execute.mjs --self-test` pins * the fill, the refusal to send and the landing. * - * ## The governed-review follow-up — `pr_create` and `pr_ready` on a Tier H diff + * ## The governed-review follow-up — `pr_ready` on a Tier H diff, ⛔ never the draft open * * A Tier H governed surface (`GOVERNED_SURFACES` rows with tier `H` in * `check-governed-merges.mjs`: `docs/adr/**`, `skills/**`, `AGENTS.md`, * `CLAUDE.md`, `docs/NORTH-STAR.md`, `.github/workflows/**`) lands only on an * APPROVED review by an account in `GOVERNED_APPROVERS` - * (`check-governed-queue-guard.mjs`, the ONE list of those accounts). The - * platform summons a code owner only when a pull is opened READY or marked - * ready for review — measured on this repository's own Tier H pulls, the - * CODEOWNERS request arrived at `ready_for_review` and never at the draft - * create — and every relay `pr_create` is a draft (forced above), held draft - * until that approval. So without this follow-up a seat-driven Tier H pull - * waits on an approver nobody told. The rows `pr_create` and `pr_ready` - * therefore carry `governedReview: true`, and the executor, once the row's own + * (`check-governed-queue-guard.mjs`, the ONE list of those accounts). WHEN a + * human is asked to look is the owning seat's act, never the relay's: a pull + * reaches an approver only once the seat's `## 席位核对` ACCEPT and a + * `## Contract review` PASS on the pull's CURRENT head are both on its thread, + * and then as the seat's own hand-off stroke — `pr_request_reviewers` below, + * naming the approvers it owes, and `assign` for an approver who is the pull's + * author (pm-dispatch SKILL.md, ACCEPT 之后的路径分叉 ③). The maintainer, + * verbatim: 「应该是开发完了你审核通过才会推送给我审核」. So the draft open + * summons nobody: a `pr_create` is the create plus the legs the caller spelled, + * nothing reads the pull's files there, and a dev's `pr_create` carries no + * `assignees` at all — the seat's own account is never assigned to or + * requested on a pull. The one follow-up left is `pr_ready`. The platform + * summons a code owner only when a pull is opened READY or marked ready for + * review — measured on this repository's own Tier H pulls, the CODEOWNERS + * request arrived at `ready_for_review` and never at the draft create — and a + * Tier H pull is readied only after an approver has already approved it, so + * the follow-up there costs zero writes when the hand-off was made and names + * the one approver still uncovered when it was not. The row `pr_ready` + * therefore carries `governedReview: true`, and the executor, once the row's own * requests have landed, reads the pull's file list in full * (`GOVERNED_REVIEW_FILES_PAGE_SIZE` per page, at most * `GOVERNED_REVIEW_FILES_PAGE_CAP` pages — the endpoint's own ceiling; a walk @@ -109,8 +120,8 @@ * `POST /repos/{o}/{r}/pulls/{n}/requested_reviewers` naming every * `GOVERNED_APPROVERS` account not already requested, assigned or holding a * submitted review (the union the H43 patrol reads as covered — so a re-run, - * a `pr_ready` after a `pr_create`, or the platform's own CODEOWNERS request - * landing first costs zero writes), and — because GitHub answers 422 to a + * a `pr_ready` after the seat's hand-off, or the platform's own CODEOWNERS + * request landing first costs zero writes), and — because GitHub answers 422 to a * request naming the pull's own author — ONE * `POST /repos/{o}/{r}/issues/{n}/assignees` for the author when the author is * itself a missing approver. Each answer is judged as the legs above are: @@ -120,13 +131,14 @@ * ungoverned diff send nothing, and the row says which. The reads are unpaced * GETs; the writes are paced like every other write verb and are outside the * rule ⑤ count (`strokePacing` counts a stroke's spelled requests; these two - * are conditional and at most two per pull). Both rows spend `issues: write` + * are conditional and at most two per pull). The row spends `issues: write` * for the assignee fallback (`alsoSpends`), a literal grant on every stroke. - * `execute.mjs --self-test` pins the plan, the writes and both refusals; - * `validate.mjs --self-test` pins the two rows and the two endpoints against - * the refused families. ⛔ This is an ACTION, not a judge: no gate, check-run - * or patrol row is added by it — the H43 row in `check-half-states.mjs` keeps - * reading the gap it closes. + * `execute.mjs --self-test` pins the plan, the writes and both refusals, and + * that `pr_create` sends none of it; `validate.mjs --self-test` pins the one + * row and the two endpoints against the refused families. ⛔ This is an + * ACTION, not a judge: no gate, check-run or patrol row is added by it — the + * H43 row in `check-half-states.mjs` reads the gap left once the hand-off is + * due (ACCEPT and a same-head PASS on the thread, and nobody asked). * * ## The permissions the relay's token is narrowed to * @@ -134,7 +146,7 @@ * (`github/docs`, `src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json`): * every `/issues/**` write here is `issues: write` — `pr_create`'s assignees * and labels legs and the governed-review follow-up's assignee fallback on - * `pr_create` and `pr_ready` spend it as `alsoSpends`; `/pulls/**` and the + * `pr_ready` spend it as `alsoSpends`; `/pulls/**` and the * ready / draft GraphQL mutations are `pull-requests: write`; the two * auto-merge mutations (`enablePullRequestAutoMerge` / * `disablePullRequestAutoMerge`) additionally require `contents: write` — @@ -933,12 +945,14 @@ export function transferRemedy(source, target) { * each a literal mint input, never a stroke-scoped one * (`validate.mjs --self-test` pins it): `pr_create`'s * assignees and labels legs, LATER requests, the governed-review - * follow-up's assignee fallback on `pr_create` and `pr_ready`, and + * follow-up's assignee fallback on `pr_ready`, and * `automerge_enable` on a pull that changes `.github/workflows/**` * (header above); * `governedReview` (optional) true for a row whose landed requests are followed - * by the governed-review follow-up (header above) — `pr_create` - * and `pr_ready`, the two moments a seat opens or readies a pull; + * by the governed-review follow-up (header above) — `pr_ready` + * alone, the moment a seat readies a pull; ⛔ never `pr_create`: + * the draft open summons nobody, a human is asked at the seat's + * hand-off (`pr_request_reviewers`, after ACCEPT + same-head PASS); * the executor reads the pull's files and sends what * `governedReviewRequests` spells, on a Tier H hit alone; * `distinct` (optional) two keys the action must give different values — @@ -1038,8 +1052,9 @@ export const OPS = Object.freeze({ alsoSpends: Object.freeze(['issues']), required: Object.freeze(['title', 'head', 'base']), optional: Object.freeze(['body', 'assignees', 'labels']), - // The draft open is the moment the platform's CODEOWNERS is silent (header: "The governed-review follow-up"). - governedReview: true, + // ⛔ No `governedReview` here (header: "The governed-review follow-up"): the draft open summons nobody. A human + // sees the pull at the seat's hand-off — `pr_request_reviewers` after ACCEPT + a same-head PASS — and a dev's + // create carries no `assignees`; the leg stays for the author-is-approver fallback, never for a seat's account. // `draft: true` is FORCED — a seat cannot open a ready PR through the relay. requests: (a, repo) => [ { verb: 'POST', path: `/repos/${repo}/pulls`, body: { title: a.title, head: a.head, base: a.base, ...('body' in a ? { body: a.body } : {}), draft: true } }, diff --git a/scripts/pm/fleet-write/validate.mjs b/scripts/pm/fleet-write/validate.mjs index f45ef2aebdc..b3d539bf527 100644 --- a/scripts/pm/fleet-write/validate.mjs +++ b/scripts/pm/fleet-write/validate.mjs @@ -980,7 +980,7 @@ export async function selfTest() { ]; }); t('every op produces at least one request', everyRequest.length >= OP_NAMES.length); - t('the governed-review follow-up runs after exactly pr_create and pr_ready — derived from the rows, never listed — and its two writes are a POST to requested_reviewers carrying requested and a POST to the additive issue-assignees endpoint carrying assigned, both in the family pin above', [GOVERNED_REVIEW_OPS, everyRequest.filter((r) => r.governedReview).map((r) => [r.op, r.verb, r.path, r.requested ?? null, r.assigned ?? null])], [['pr_create', 'pr_ready'], [['pr_create', 'POST', `/repos/${TARGET_OWNER}/objectstack/pulls/7/requested_reviewers`, ['u'], null], ['pr_create', 'POST', `/repos/${TARGET_OWNER}/objectstack/issues/7/assignees`, null, ['u']], ['pr_ready', 'POST', `/repos/${TARGET_OWNER}/objectstack/pulls/7/requested_reviewers`, ['u'], null], ['pr_ready', 'POST', `/repos/${TARGET_OWNER}/objectstack/issues/7/assignees`, null, ['u']]]]); + t("the governed-review follow-up runs after exactly pr_ready — derived from the rows, never listed; ⛔ pr_create is OFF it, the draft open summons nobody and a human is asked at the seat's hand-off (pr_request_reviewers after ACCEPT + a same-head PASS) — and its two writes are a POST to requested_reviewers carrying requested and a POST to the additive issue-assignees endpoint carrying assigned, both in the family pin above", [GOVERNED_REVIEW_OPS, OPS.pr_create.governedReview ?? null, everyRequest.filter((r) => r.governedReview).map((r) => [r.op, r.verb, r.path, r.requested ?? null, r.assigned ?? null])], [['pr_ready'], null, [['pr_ready', 'POST', `/repos/${TARGET_OWNER}/objectstack/pulls/7/requested_reviewers`, ['u'], null], ['pr_ready', 'POST', `/repos/${TARGET_OWNER}/objectstack/issues/7/assignees`, null, ['u']]]]); t('…an empty list spells no request, and a governedReview row is never a row whose spelled requests change: pr_create and pr_ready keep the request lists they always had', [governedReviewRequests('o/r', 7), OPS.pr_create.requests({ op: 'pr_create', title: 't', head: 'h', base: 'b' }, 'o/r').length, OPS.pr_ready.requests({ op: 'pr_ready', pull: 7 }, 'o/r').length], [[], 1, 1]); t('⛔ no request path reaches a refused family', everyRequest.filter((r) => REFUSED_PATH_FAMILIES.some((re) => re.test(r.path))).map((r) => `${r.op} ${r.path}`), []); t('⛔ the table\'s one PUT is pr_update_branch\'s update-branch, an action endpoint — no PUT reaches /labels or /assignees, the whole-set replaces', [everyRequest.filter((r) => r.verb === 'PUT').map((r) => `${r.op} ${r.path}`), everyRequest.some((r) => r.verb === 'PUT' && /\/(labels|assignees)$/.test(r.path))], [[`pr_update_branch /repos/${TARGET_OWNER}/objectstack/pulls/7/update-branch`], false]); @@ -994,7 +994,7 @@ export async function selfTest() { t('…and neither is the FIRST request of its action — the answer they read exists before either is sent', OP_NAMES.filter((op) => { const a = { op }; for (const k of [...OPS[op].required, ...OPS[op].optional]) a[k] = sample[k]; return OPS[op].requests(a, 'o/r')[0].fromAnswer !== undefined; }), []); t('…every such leg is a POST to the one additive issue endpoint for its list — never a PATCH or PUT, the whole-set replaces', legs.map((r) => [r.verb, /\/(assignees|labels)$/.test(r.path)]), [['POST', true], ['POST', true]]); t('…their placeholder is the one spelling the executor fills, spelled once, and no path without fromAnswer carries a brace', [answerPlaceholder('number'), everyRequest.filter((r) => r.fromAnswer === undefined && /[{}]/.test(r.path)).map((r) => `${r.op} ${r.path}`)], ['{number}', []]); - t('every grant a row ALSO spends is a literal mint input of PERMISSIONS, never a stroke-scoped one — so the mint widens nothing for it; pr_create\'s and pr_ready\'s issues write (the legs and the governed-review assignee fallback) and automerge_enable\'s workflows write are the three such rows', [OP_NAMES.flatMap((op) => OPS[op].alsoSpends ?? []).every((name) => name in PERMISSIONS && !STROKE_SCOPED_PERMISSIONS.includes(name)), OP_NAMES.filter((op) => OPS[op].alsoSpends).map((op) => [op, [...OPS[op].alsoSpends]])], [true, [['pr_create', ['issues']], ['pr_ready', ['issues']], ['automerge_enable', ['workflows']]]]); + t('every grant a row ALSO spends is a literal mint input of PERMISSIONS, never a stroke-scoped one — so the mint widens nothing for it; pr_create\'s issues write (its assignees and labels legs), pr_ready\'s (the governed-review assignee fallback) and automerge_enable\'s workflows write are the three such rows', [OP_NAMES.flatMap((op) => OPS[op].alsoSpends ?? []).every((name) => name in PERMISSIONS && !STROKE_SCOPED_PERMISSIONS.includes(name)), OP_NAMES.filter((op) => OPS[op].alsoSpends).map((op) => [op, [...OPS[op].alsoSpends]])], [true, [['pr_create', ['issues']], ['pr_ready', ['issues']], ['automerge_enable', ['workflows']]]]); t('pr_create without assignees or labels is the one create request it always was — no later leg', OPS.pr_create.requests({ op: 'pr_create', title: 't', head: 'h', base: 'b' }, 'o/r').length, 1); t('…with labels alone, the create then the labels leg; with assignees alone, the create then the assignees leg', [OPS.pr_create.requests({ op: 'pr_create', title: 't', head: 'h', base: 'b', labels: ['skip-changeset'] }, 'o/r').map((r) => r.labelled ?? r.assigned ?? 'create'), OPS.pr_create.requests({ op: 'pr_create', title: 't', head: 'h', base: 'b', assignees: ['u'] }, 'o/r').map((r) => r.labelled ?? r.assigned ?? 'create')], [['create', ['skip-changeset']], ['create', ['u']]]); t('labels_remove is one directed DELETE per name, URL-encoded, idempotent on 404', OPS.labels_remove.requests({ op: 'labels_remove', issue: 1, labels: ['a b', 'c'] }, 'o/r').map((r) => [r.verb, r.path, r.idempotent404]), [['DELETE', '/repos/o/r/issues/1/labels/a%20b', true], ['DELETE', '/repos/o/r/issues/1/labels/c', true]]);