Skip to content

Commit 4feccc2

Browse files
feat(pm): the relay requests review from every GOVERNED_APPROVERS account on a Tier H pull; CODEOWNERS routes every Tier H surface to both approvers (#25064)
Fixes #25046 Clause-②: no ## 维护者速读(草稿) ### 改了什么 - `.github/CODEOWNERS`:六个 Tier H 受管面(`docs/adr/`、`skills/`、`AGENTS.md`、`CLAUDE.md`、`docs/NORTH-STAR.md`、`.github/workflows/`)与三条执法链行(本文件、`check-governed-merges.mjs`、`check-governed-queue-guard.mjs`)全部路由到 `@os-zhuang @hotlong`;文件头写明 owner 列表镜像 `GOVERNED_APPROVERS` 常量。 - 中继 `fleet-write`:`pr_create`(开 draft PR)与 `pr_ready`(转正式)落地后,执行器读 PR 完整文件清单、按注册表判级;命中 Tier H 就向 `GOVERNED_APPROVERS` 里尚未被请审、指派或已评审的每个账户发 review request;PR 作者本人是批准账户时改为 assign(GitHub 对"请审作者"回 422)。Tier S、未受管、已覆盖的 PR 零写并在 run 摘要说明;文件清单读不到或被截断、写入被拒,都是 FAILED action,绝不静默跳过。 - `check-governed-queue-guard.mjs --self-test` 新增 12 条用例:读磁盘上的 CODEOWNERS,与注册表 Tier H 行、`GOVERNED_APPROVERS` 常量比对,漂移即红。不新增门禁、check-run 或巡检行。 - pm-dispatch `SKILL.md` 第 ③ 条与 `rest-channel.md` 改为指向该 op(各净 0 行)。 ### 为什么改 - 席位开的 Tier H PR 是 draft,且一直 draft 到获批;GitHub 的 CODEOWNERS 只在 PR 开为 ready 或转 ready 时才请审(本仓实测:PR 24859 的 draft 创建于 11:19:43,`hotlong` 的请审 11:22:14 才到,紧跟 `ready_for_review` 一秒之后)。所以 draft 阶段没有任何机制通知批准人;原 CODEOWNERS 又只写了 `docs/adr/` 到 `@hotlong`,`os-zhuang` 从未被叫到。「应该是skills有问题,申请我批准的应该推送给我,我看不到任何提示」—— 请审现在是一个 op,不再是一句散文。 ### 风险与代价(含回滚) - 每次中继开 PR 或转正式多 1 到 3 次读(文件清单分页、PR、reviews);Tier H 命中才有最多 2 次写,走原有写节流。 - 文件清单读不到时该 action 判 FAILED(PR 已开着,run 摘要点名"未发请审"),dev 的 dispatch 以 exit 5 收场 —— 这是刻意的响亮失败;代价是偶发平台抖动会把一次开 PR 记成失败,席位按行手补即可。 - CODEOWNERS 每一行都要求 owner 集合恰等于常量:以后改 `GOVERNED_APPROVERS` 必须同一 PR 改 CODEOWNERS,否则守卫自检红。 - 回滚:revert 本 PR 即可;无数据、无 workflow、无发布面变动。 ### 席位意见 (留空) ### 你要做的 - 批准本 PR(Tier H:`.github/CODEOWNERS` 自身行;AGENTS.md 所列受管面的路由规则由本文件承载)。获批后席位落地。 ## What this PR does A seat-driven Tier H pull is opened draft and stays draft until the approval, and GitHub requests a code owner only at `ready_for_review`. CODEOWNERS listed `docs/adr/` to `@hotlong` alone, and SKILL.md's "request review from both approvers" was prose no op performed. Three things change: 1. **`.github/CODEOWNERS`** routes every Tier H register row (`/docs/adr/`, `/skills/`, `/AGENTS.md`, `/CLAUDE.md`, `/docs/NORTH-STAR.md`, `/.github/workflows/`) and the enforcement chain (itself, `check-governed-merges.mjs`, `check-governed-queue-guard.mjs`) to `@os-zhuang @hotlong`; its header names `GOVERNED_APPROVERS` as the list it mirrors. `GET /repos/objectstack-ai/objectstack/codeowners/errors?ref=claude/issue-25046-tier-h-review-request` answers `{"errors":[]}` (read 2026-10-21 after the push), so both rows are live — a code owner without write access would be an error there. 2. **The request is an op.** `fleet-write/ops.mjs` marks `pr_create` and `pr_ready` with `governedReview: true` and spells the two conditional writes in `governedReviewRequests`. After the row's own requests land, `execute.mjs` reads the pull's whole file list (every page; a walk past the endpoint's 30-page ceiling is a FAILED action, never "nothing governed"), classifies through the register (`governedPathsIn` + `landingTierOf`, scoped to `payload.repo` by `repoIdForSlug`), and on a Tier H hit requests review from every `GOVERNED_APPROVERS` account not already requested, assigned or holding a submitted review — the union the H43 patrol reads as covered. The pull's own author is never requested (GitHub answers 422); a missing approver who is the author is assigned instead. Each answer is judged like the assignees leg: a 2xx lacking a login sent is a FAILED action naming it. Tier S (`.claude/**` alone), ungoverned and covered pulls send nothing and the summary row says which. 3. **The mirror is pinned inside an existing self-test.** `check-governed-queue-guard.mjs` gains `codeownersMirrorVerdict`, and its `--self-test` reads `.github/CODEOWNERS` from disk: a Tier H row with no line, any line whose owner set differs from the constant, or a routed Tier S row reds it. No new gate, check-run or patrol row. Landing site for the op: the relay executor (candidate b). Of the three homes the card named, only the executor knows the pull number at the right moment (the create's answer), holds the token, and runs on the board's `main` checkout where the register lives, so the Tier H decision is the board's single source rather than a seat checkout that may be behind. The seat client (a) would have to compute the diff from a compare and could not name a number that does not exist yet; the landing helper (c) does not exist as a script (the landing stroke is an actions file) and its moment — after the approval — is too late for Tier H. ## Measured — the five hypotheses - **H1 (CODEOWNERS premise): held.** `origin/main` `63085c380` had three rows, all `@hotlong`. The Tier H row set is derived from `GOVERNED_SURFACES` (`governedSurfacesFor('objectstack')`, tier H) and spelled as root-anchored prefix/exact patterns; the self-test pins the on-disk file against that derivation and against `GOVERNED_APPROVERS`, so the two stay in step by a pin, not by hand. The merges self-test's classification of `.github/CODEOWNERS` as a near-miss (not a governed row) is unchanged. - **H2 (home of the op): the executor.** See above. Both governed scripts import only Node built-ins and sibling modules, so the runner (no `pnpm install`) loads them; `validate.mjs` already imported `check-governed-merges.mjs` on the runner. - **H3 (trigger moment): both `pr_create` and `pr_ready`.** The timeline of PR 24859 shows the draft create at 2026-10-20T11:19:43Z with no `review_requested`, then `ready_for_review` at 11:22:13Z and CODEOWNERS' `review_requested hotlong` at 11:22:14Z. So the draft open is the moment the platform is silent, and the op must fire there. It also fires at `pr_ready`, because the file list may have grown a Tier H path since the open and the follow-up is idempotent (zero writes when covered), so the second firing costs one read. - **H4 (author identity): both branches pinned with fixtures.** A `objectstack-fleet[bot]`-authored pull requests both approvers; a pull authored by an approver requests the other and assigns the author. - **H5 (idempotence): the API's duplicate behaviour is never exercised.** The plan subtracts the pull's pending `requested_reviewers`, its `assignees` and the authors of its submitted reviews (`GET …/pulls/{n}/reviews`, any state but PENDING) before sending, so a re-run, a `pr_ready` after a `pr_create`, or the platform's own CODEOWNERS request landing first costs zero writes. No throwaway PR was opened. ## Tests - `node scripts/pm/fleet-write/execute.mjs --self-test` → `✓ fleet-write/execute self-test: 167 cases pass across 18 batteries` (new battery of 29 cases; floor 17 → 18). - `node scripts/pm/fleet-write/validate.mjs --self-test` → `✓ fleet-write/validate self-test: 160 cases pass across 11 batteries` (the refused-family pin now covers the two follow-up endpoints; the alsoSpends pin names three rows). - `node scripts/pm/fleet-write/dispatch.mjs --self-test` → `✓ fleet-write/dispatch self-test: 235 cases pass across 21 batteries` (unchanged file). - `node scripts/pm/check-governed-queue-guard.mjs --self-test` → `✓ check-governed-queue-guard self-test: 334 cases pass` (new battery of 12 cases; floor 26 → 27). - **Ablation A** (the change committed first, then `scripts/ablation-replace.mjs`): anchor `followed = await requestGovernedReview(…)` → `followed = { ok: true }`; tool evidence `anchor x1 -> x0`, `blob c660b17 -> 58e7fdeed4fd`; the executor self-test went red `17 of 167 case(s) failed`, including `⭐ pr_create on a Tier H diff: … ONE requested_reviewers POST naming BOTH approvers`; restore verified `blob == HEAD (c660b17) and git diff HEAD is empty`. - **Ablation B**: anchor `/docs/adr/ @os-zhuang @hotlong` → `/docs/adr/ @hotlong` in `.github/CODEOWNERS`; `blob 15d3fff -> f223af300219`; the guard self-test went red `2 of 334 case(s) failed` (`the-file-on-disk-mirrors-the-register-and-the-approver-list: CODEOWNERS line 42 /docs/adr/ names @hotlong, not every GOVERNED_APPROVERS account`); restore verified `blob == HEAD (15d3fff) and git diff HEAD is empty`. ## Gates (local subset, at head `91669b139`) `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths; changeset from the merge base) derived 60 families on the actual diff — the dispatch lead's 58 plus `pnpm check:closing-target-claim` and `pnpm check:ratchet-remedy-authority`. Every one was run with the capture idiom and reconciled: `✓ dispatch-gates --ran: 60 derived famil(ies) accounted for — 59 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`. The NOT MEASURED one is `pnpm --filter @objectstack/lint run check:doc-formula-expressions` (exit 3, PREREQUISITE NOT MET: it wants a `@objectstack/formula` + `@objectstack/lint` build; this diff touches no package, so no build closure was made — CI runs it). Among the 59 green: `check:pm-fleet-write-{dispatch,execute,validate}`, `check:pm-governed-merges`, `check:pm-governed-prose`, `check:pm-skill-ratchet` (SKILL.md 319/319, rest-channel.md 82/82, every edited line under the 120-byte line budget), `check:pm-skill-id-lint`, `check:skill-frame-sync`, `check:pm-dispatch-gates` (fast tier, 1913 cases, 328.7 s), `check:pm-write-pace`, `check:doc-authoring`, `check:required-contexts`, `node scripts/check-self-test-wired.mjs` and its `--self-test`, `node scripts/pm/check-governed-queue-guard.mjs --self-test`. The families CI runs on the PR are read from its check-runs by the seat; none was re-run locally beyond this subset. ## Acceptance notes - AGENTS.md Prime Directive 14 says "CODEOWNERS routes review requests for `docs/adr/` only, so nothing summons the maintainer on the other Tier H surfaces". After this PR that sentence is stale. AGENTS.md is outside this card's file surface (Tier H, its own line ratchet), so it is not edited here; a one-line rewrite is the follow-up. `check:pm-governed-prose` does not pin that sentence (verified green here). - The `pr_ready` + `automerge_enable` landing stroke now performs one extra read (the readied pull's files) and, on a Tier H pull, up to two paced writes; `strokePacing` does not count them (conditional, at most two per pull), so they ride rule ①'s gap — documented in the ops.mjs header. - A relay `pr_create` whose file list cannot be read is now a FAILED action (exit 5 for the dev's dispatch) with the pull already open; the row and the log say what was not sent. This is the loud direction on purpose: silence here is the defect. - This PR's own `pr_create` goes through the relay executor on `main`, which predates this change, so this pull requests nobody by itself; the seat requests review after its at-tier contract review, per the dispatch order. - `check-governed-merges.mjs --self-test` keeps pinning `.github/CODEOWNERS` as a near-miss that is NOT a governed row; CODEOWNERS routes its own edits instead. Not changed, and I do not think it is wrong: the file is an enforcer, and the register is the audit's surface list. ## Out-of-scope findings - AGENTS.md Prime Directive 14's CODEOWNERS sentence (above): an agent-read rule that becomes false, not a runtime contract — noted, not filed; carrier: the `domain:skills` seat, as a one-line follow-up on the Tier H file. Dedupe words: CODEOWNERS, docs/adr only, Prime Directive 14, summons. --- _Generated by [Claude Code](https://claude.ai/code/session_01M82feuetLzrCiBz41BJb7L)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent d6a5fa3 commit 4feccc2

7 files changed

Lines changed: 525 additions & 44 deletions

File tree

‎.claude/skills/pm-dispatch/SKILL.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,10 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
195195
- PR 上的标签 = 待维护者审阅,不入六态;与请审同为等人批清单,随获批或撤回判决离开。
196196
- 批准判定单源 = 队列守卫常量 `GOVERNED_APPROVERS`:授权账户 APPROVED 即算,⛔ 不卡 `commit_id`。
197197
- 批准后再推亦不过期;席位落地 = 过落地前检、清标、ready、auto-merge,踢出/变基同法。
198-
- ③ 在 draft PR 上向两个授权批准账户 `os-zhuang` 与 `hotlong` 都 request review,主动推。
199-
- PR 作者身份即两账户之一的席位时,对该账户请审必失败(author-identity 422)。
200-
- 该账户改为把 PR assign 给它替代通知,另一账户照常请审;轮次报告点名说明走了兜底。
201-
- 请审走免碰 draft 位的 REST 专用路,ready/draft 走中继 op;MCP 兜底已拒;端点见 rest-channel。
198+
- ③ 请审是中继 op:`pr_create`/`pr_ready` 命中 Tier H 即向 `GOVERNED_APPROVERS` 全员请审。
199+
- 已请审/已指派/已评审者跳过(零写);PR 作者即其一时请审必 422,op 改为 assign 该账户替代。
200+
- 席位只核对 run 摘要 governed review 行,缺口才手补(免碰 draft 位 REST 路)并在轮次报告点名。
201+
- ready/draft 走中继 op;MCP 兜底已拒;端点见 rest-channel。
202202
- ④ 轮次报告单列 awaiting a human merge。
203203
- skills 车道自有 PR:纯代码面如 `scripts/pm/` 由本席按达档自审(清单不减)后落地。
204204
- 受管面两层:Tier S = `.claude/**` 全树,余皆 Tier H 等人批;S 经达档复核 PASS 后 ready → 入队。

‎.claude/skills/pm-dispatch/references/rest-channel.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
- ✓ 建卡带标签 `POST .../issues` · 改正文 `PATCH .../issues/{n}` · 认领 `POST .../issues/{n}/assignees`。
4545
- ✓ 该 `PATCH` 带 `state` 关卡/重开,`state_reason` 交付 `completed`、撤单 `not_planned`,走裸 REST。
4646
- 请求体走文件(`-d @file`)或引号定界 heredoc(`<<'EOF'`),⛔ 永不内联双引号串。
47-
- ✓ 请求复审 `POST .../pulls/{n}/requested_reviewers` · 开 PR `POST .../pulls` 带 `draft=true`。
47+
- ✓ 请审 `POST .../pulls/{n}/requested_reviewers`(Tier H 归中继 op)· 开 PR `POST .../pulls` 带 `draft=true`。
4848
- ✓ `origin/main` 合进 PR head:中继 op `pr_update_branch`(`pull` + 40 位 `expected_head_sha`),真合并提交。
4949
- ✓ 落地经中继 `node scripts/pm/fleet-write/dispatch.mjs --repo O/N --actions-file F`;`--via dispatch` 同载。
5050
- ✓ 启动 run 走 op `workflow_dispatch`:解锁物是白名单 run 才启;入表只经 workflow 自己的 PR。

‎.github/CODEOWNERS‎

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,52 @@
1-
# .github/CODEOWNERS — governance routing (#6785; ruling #6741).
1+
# .github/CODEOWNERS — governance routing: every Tier H governed surface to
2+
# every authorized approver.
23
#
34
# The ruling this file helps enforce (maintainer, verbatim):
45
# 「adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。」
56
#
6-
# Routing docs/adr/ to the maintainer does two things: every PR touching an
7-
# ADR requests his review automatically, and a ruleset / branch-protection
8-
# rule "require review from Code Owners" has an entry to bind to (Half 2 of
9-
# #6785 — a repo-settings change only the maintainer can apply; the
10-
# button-by-button instructions are in the PR that added this file).
7+
# The owner list on EVERY row mirrors `GOVERNED_APPROVERS` in
8+
# scripts/pm/check-governed-queue-guard.mjs — the one list of accounts whose
9+
# APPROVED review lands a Tier H surface (「os-zhuang hotlong 批准算数」). The
10+
# rows mirror the Tier H rows of `GOVERNED_SURFACES` in
11+
# scripts/pm/check-governed-merges.mjs. This file is static and can import
12+
# neither, so the two stay in step the only way a static file can:
13+
# `node scripts/pm/check-governed-queue-guard.mjs --self-test` reads this file
14+
# (`codeownersMirrorVerdict`) and reds when a Tier H row has no line here, when
15+
# any line names fewer or other owners than that constant, or when a Tier S row
16+
# (`.claude/**`, which lands on a seat's review of record) is routed here.
17+
# Change the constant or the register first; this file follows in the same PR.
1118
#
12-
# The per-PR CI half that used to sit beside this file (`ADR maintainer
13-
# approval`) retired under the 2026-08-18 ruling (#9495): a human merge IS
14-
# the review record for a governed surface, the pre-merge line of defense is
15-
# seat discipline (no AI seat flips ready / enqueues / arms auto-merge on a
16-
# governed PR — AGENTS.md Prime Directive #14), and detection moved to the
17-
# report-only post-merge audit, scripts/pm/check-governed-merges.mjs, whose
18-
# header carries the incident history that used to live in the gate script.
19+
# What routing buys, and what it does not: GitHub requests a code owner's
20+
# review when a pull is opened READY or marked ready for review. A DRAFT open
21+
# requests nobody — measured on this repository's own Tier H pulls, where the
22+
# request arrived at `ready_for_review` and never at the draft create. Every
23+
# seat-driven Tier H pull is opened draft and stays draft until the approval,
24+
# so the fleet relay requests the same accounts itself at `pr_create` and
25+
# `pr_ready` (scripts/pm/fleet-write/ops.mjs, "The governed-review follow-up");
26+
# this file is the platform half — for a pull opened ready or readied by hand —
27+
# and the entry a ruleset's "require review from Code Owners" can bind to. A
28+
# code owner without write access is ignored by GitHub without a word:
29+
# `GET /repos/{owner}/{repo}/codeowners/errors` reads this file's own verdict.
1930
#
20-
# `hotlong` is verified against the repo history, not assumed: 2,153 commits
21-
# on main are authored as 50353452+hotlong@users.noreply.github.com — GitHub's
22-
# {id}+{login} noreply address form ties the login to the account.
31+
# Both logins are verified, not assumed: `hotlong` authors the bulk of main's
32+
# history as 50353452+hotlong@users.noreply.github.com (GitHub's {id}+{login}
33+
# noreply form ties the login to the account); `os-zhuang` is named by the
34+
# ruling above and has readied and reviewed this repository's pulls.
2335
#
24-
# The enforcement chain itself is routed the same way, deliberately: a PR
25-
# that edits the audit script or this file is a governance change — without
26-
# these entries the docs/adr/ rule could be lifted by editing the enforcers
27-
# instead of the ADRs.
36+
# The enforcement chain is routed the same way, deliberately: a PR that edits
37+
# the audit script, the approver constant or this file is a governance change —
38+
# without these entries the rule could be lifted by editing the enforcers
39+
# instead of the surfaces.
2840

29-
/docs/adr/ @hotlong
30-
/.github/CODEOWNERS @hotlong
31-
/scripts/pm/check-governed-merges.mjs @hotlong
41+
# Tier H governed surfaces — the register's rows with tier H
42+
/docs/adr/ @os-zhuang @hotlong
43+
/skills/ @os-zhuang @hotlong
44+
/AGENTS.md @os-zhuang @hotlong
45+
/CLAUDE.md @os-zhuang @hotlong
46+
/docs/NORTH-STAR.md @os-zhuang @hotlong
47+
/.github/workflows/ @os-zhuang @hotlong
48+
49+
# The enforcement chain
50+
/.github/CODEOWNERS @os-zhuang @hotlong
51+
/scripts/pm/check-governed-merges.mjs @os-zhuang @hotlong
52+
/scripts/pm/check-governed-queue-guard.mjs @os-zhuang @hotlong

‎scripts/pm/check-governed-queue-guard.mjs‎

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
559559
'⭐ the 2026-09-20 ruling: a certified PURE REGENERATION carries the record to the queued head': 13,
560560
'the per-row repository scope (ruling B2, 2026-10-13)': 14,
561561
'⭐ B′ with C: the record stands at the tier its FACE maps to, read off the committed paths': 8,
562+
'the CODEOWNERS mirror: every Tier H register row routes to every GOVERNED_APPROVERS account, nothing else is routed, and the file says so': 12,
562563
});
563564

564565
// DELETING an entry silences that battery's floor exactly as effectively as
@@ -567,7 +568,8 @@ const SELF_TEST_BATTERIES = Object.freeze({
567568
// 5770886272 on #19061, letter B): the ordinary direction, one battery, one row.
568569
// Raised 23 → 24 with the queued pull-back battery: one battery, one row.
569570
// Raised 25 → 26 with the B′-with-C face battery (ruling record 6159870311): one battery, one row.
570-
const SELF_TEST_BATTERY_FLOOR = 26;
571+
// Raised 26 → 27 with the CODEOWNERS mirror battery: one battery, one row.
572+
const SELF_TEST_BATTERY_FLOOR = 27;
571573

572574
// The key an assertion is filed under when no battery is open. It is not a
573575
// declared battery, so it reds by the same set difference rather than silently
@@ -618,6 +620,84 @@ export const EVENT_PULL_REQUEST = 'pull_request';
618620
*/
619621
export const GOVERNED_APPROVERS = Object.freeze(['os-zhuang', 'hotlong']);
620622

623+
// ── the CODEOWNERS mirror — the platform half of "both approvers are asked" ─
624+
625+
/**
626+
* `.github/CODEOWNERS` is the platform's own review router: GitHub requests a
627+
* code owner's review when a pull is opened READY or marked ready for review.
628+
* It is a static file — it can import neither `GOVERNED_SURFACES` nor
629+
* `GOVERNED_APPROVERS` — so the two stay in step the only way a static file
630+
* can: this predicate reads it and the self-test reds when it drifts. ⛔ It is
631+
* a pin inside this existing self-test, not a gate, a check-run or a patrol
632+
* row: the fleet's own request on the draft open is the relay's
633+
* governed-review follow-up (`fleet-write/ops.mjs`), and the gap both close is
634+
* what the H43 patrol row keeps reading.
635+
*
636+
* `codeownersRows` parses the file's effective lines — a `#` comment to end of
637+
* line stripped, blank lines dropped — into `{ pattern, owners, line }`.
638+
* `codeownersPatternFor` spells the one CODEOWNERS pattern a register row maps
639+
* to: its prefix or exact path, root-anchored. Both pure.
640+
*/
641+
export function codeownersRows(text) {
642+
const rows = [];
643+
String(text ?? '')
644+
.split('\n')
645+
.forEach((raw, i) => {
646+
const line = raw.replace(/#.*$/, '').trim();
647+
if (line === '') return;
648+
const [pattern, ...owners] = line.split(/\s+/);
649+
rows.push({ pattern, owners, line: i + 1 });
650+
});
651+
return rows;
652+
}
653+
654+
export function codeownersPatternFor(surface) {
655+
return `/${surface.prefix ?? surface.exact}`;
656+
}
657+
658+
/**
659+
* The verdict: `{ ok, problems, tierHPatterns }`. `ok` holds only when
660+
* - every Tier H row of the register that governs `repo` has a CODEOWNERS
661+
* line at exactly its pattern, naming exactly `@` + every account in
662+
* `approvers` (order-free; nobody else, nobody fewer);
663+
* - every other line — the enforcement chain's rows — names exactly that
664+
* same owner set, so an enforcer cannot be routed to a subset;
665+
* - no line routes a Tier S row's pattern: Tier S lands on a seat's review of
666+
* record, and a code-owner request there would summon the maintainer for a
667+
* landing that is not his;
668+
* - the file's prose names `GOVERNED_APPROVERS`, so a reader finds the
669+
* constant the owner list mirrors.
670+
* Every problem is a sentence naming the row or line; the list is whole, never
671+
* the first hit. Pure.
672+
*/
673+
export function codeownersMirrorVerdict({ text, approvers = GOVERNED_APPROVERS, repo = SELF_REPO_ID } = {}) {
674+
const rows = codeownersRows(text);
675+
const wanted = [...approvers].map((a) => `@${a}`.toLowerCase()).sort();
676+
const sameOwners = (owners) => JSON.stringify([...owners].map((o) => o.toLowerCase()).sort()) === JSON.stringify(wanted);
677+
const problems = [];
678+
// The rows that govern THIS repository, read from the register (ruling B2: the question has a repository in it).
679+
const governing = governedSurfacesFor(repo);
680+
const tierH = governing.filter((s) => s.tier === GOVERNED_TIER_H);
681+
const tierHPatterns = tierH.map(codeownersPatternFor);
682+
for (const surface of tierH) {
683+
const pattern = codeownersPatternFor(surface);
684+
const row = rows.find((r) => r.pattern === pattern);
685+
if (!row) {
686+
problems.push(`Tier H row \`${surface.id}\` (${surface.glob}) has no CODEOWNERS line at \`${pattern}\` — a pull touching it summons nobody when opened ready`);
687+
continue;
688+
}
689+
if (!sameOwners(row.owners)) problems.push(`CODEOWNERS line ${row.line} \`${pattern}\` names ${row.owners.join(' ') || 'nobody'}, not every GOVERNED_APPROVERS account (${wanted.join(' ')}) — a Tier H pull then waits on an approver who is never asked`);
690+
}
691+
for (const row of rows) {
692+
if (tierHPatterns.includes(row.pattern)) continue;
693+
const tierS = governing.find((s) => s.tier === GOVERNED_TIER_S && codeownersPatternFor(s) === row.pattern);
694+
if (tierS) problems.push(`CODEOWNERS line ${row.line} routes \`${row.pattern}\`, a Tier S row (\`${tierS.id}\`) that lands on a seat's review of record — a code-owner request there asks the maintainer for a landing that is not his`);
695+
else if (!sameOwners(row.owners)) problems.push(`CODEOWNERS line ${row.line} \`${row.pattern}\` (an enforcement-chain row) names ${row.owners.join(' ') || 'nobody'}, not every GOVERNED_APPROVERS account (${wanted.join(' ')})`);
696+
}
697+
if (!/GOVERNED_APPROVERS/.test(String(text ?? ''))) problems.push('the file never names `GOVERNED_APPROVERS` — its header must say which constant the owner list mirrors');
698+
return { ok: problems.length === 0, problems, tierHPatterns, rows };
699+
}
700+
621701
// ── the landing TIER: Tier S lands on a review of record, Tier H on an approval ─
622702

623703
/**
@@ -4529,6 +4609,38 @@ export async function selfTest() {
45294609
assert('⭐ a-record-on-an-OLDER-head-CARRIES-when-the-head-moved-ONLY-on-prose-faces-outside-the-review-surface', proseCarried.exitCode === EXIT_CLEAR && proseCarried.entries[0].record.state === 'stands' && proseCarried.entries[0].record.carriedHops === 1, JSON.stringify(proseCarried.entries[0].record));
45304610
const proseCode = await tierRun({ comments: onOldHead, runGit: qgit(qProseCode), baseRef: QBASE });
45314611
assert('⛔ a-CODE-path-moved-beside-the-prose-REFUSES-exactly-as-an-uncarried-old-head-does', proseCode.exitCode === EXIT_REFUSED_UNAPPROVED && proseCode.entries[0].record.state === 'absent', JSON.stringify(proseCode.entries[0].record));
4612+
// ── the CODEOWNERS mirror ─────────────────────────────────────────────────
4613+
//
4614+
// The platform half of "both approvers are asked": `.github/CODEOWNERS` is a
4615+
// static file, so this is the ONE place that proves it mirrors the register's
4616+
// Tier H rows and `GOVERNED_APPROVERS`. The file on disk is read here; the
4617+
// negative controls drive the predicate with fixtures.
4618+
battery('the CODEOWNERS mirror: every Tier H register row routes to every GOVERNED_APPROVERS account, nothing else is routed, and the file says so');
4619+
{
4620+
const onDisk = readFileSync(join(repoRoot, '.github', 'CODEOWNERS'), 'utf8');
4621+
const live = codeownersMirrorVerdict({ text: onDisk });
4622+
assert('⭐ the-file-on-disk-mirrors-the-register-and-the-approver-list', live.ok, live.problems.join(' | '));
4623+
assert('every-Tier-H-row-that-governs-this-repository-is-a-pattern-the-file-carries', live.tierHPatterns.length === governedSurfacesFor(REPO).filter((s) => s.tier === GOVERNED_TIER_H).length && live.tierHPatterns.every((p) => live.rows.some((r) => r.pattern === p)), JSON.stringify(live.tierHPatterns));
4624+
assert('the-patterns-are-the-register-rows-root-anchored-prefix-or-exact', live.tierHPatterns.join() === '/docs/adr/,/skills/,/AGENTS.md,/CLAUDE.md,/docs/NORTH-STAR.md,/.github/workflows/', live.tierHPatterns.join());
4625+
assert('every-line-on-disk-names-every-approver-spelled-as-a-login', live.rows.every((r) => GOVERNED_APPROVERS.every((a) => r.owners.includes(`@${a}`))) && live.rows.length >= live.tierHPatterns.length, JSON.stringify(live.rows));
4626+
assert('the-parser-strips-comments-and-blank-lines-and-splits-owners', JSON.stringify(codeownersRows('# c\n\n/docs/adr/ @a @b # trailing\n /x.md @c\n')) === JSON.stringify([{ pattern: '/docs/adr/', owners: ['@a', '@b'], line: 3 }, { pattern: '/x.md', owners: ['@c'], line: 4 }]));
4627+
const owners = GOVERNED_APPROVERS.map((a) => `@${a}`).join(' ');
4628+
const full = (extra = '') => `# mirrors GOVERNED_APPROVERS\n${live.tierHPatterns.map((p) => `${p} ${owners}`).join('\n')}\n${extra}`;
4629+
assert('a-fixture-carrying-exactly-the-Tier-H-rows-with-every-owner-passes', codeownersMirrorVerdict({ text: full() }).ok);
4630+
const one = codeownersMirrorVerdict({ text: full().replace(`/docs/adr/ ${owners}`, `/docs/adr/ @${GOVERNED_APPROVERS[1]}`) });
4631+
assert('⛔ one-approver-on-the-adr-row-is-a-problem-naming-the-row-and-the-missing-account', !one.ok && one.problems.length === 1 && one.problems[0].includes('`/docs/adr/`') && one.problems[0].includes(`@${GOVERNED_APPROVERS[0]}`), one.problems.join(' | '));
4632+
const missing = codeownersMirrorVerdict({ text: full().split('\n').filter((l) => !l.startsWith('/skills/')).join('\n') });
4633+
assert('⛔ a-Tier-H-row-with-no-line-is-a-problem-naming-the-row', !missing.ok && missing.problems.length === 1 && missing.problems[0].includes('`skills-catalog`'), missing.problems.join(' | '));
4634+
const tierS = codeownersMirrorVerdict({ text: full(`/.claude/ ${owners}\n`) });
4635+
assert('⛔ routing-the-Tier-S-row-is-a-problem-a-seat-record-landing-summons-nobody', !tierS.ok && tierS.problems.length === 1 && tierS.problems[0].includes('`claude-tree`'), tierS.problems.join(' | '));
4636+
const enforcer = codeownersMirrorVerdict({ text: full(`/.github/CODEOWNERS @${GOVERNED_APPROVERS[1]}\n`) });
4637+
assert('⛔ an-enforcement-chain-row-routed-to-a-subset-is-a-problem', !enforcer.ok && enforcer.problems.length === 1 && enforcer.problems[0].includes('enforcement-chain'), enforcer.problems.join(' | '));
4638+
const silent = codeownersMirrorVerdict({ text: full().replace('# mirrors GOVERNED_APPROVERS\n', '') });
4639+
assert('⛔ a-file-that-never-names-the-constant-is-a-problem', !silent.ok && silent.problems.length === 1 && silent.problems[0].includes('GOVERNED_APPROVERS'), silent.problems.join(' | '));
4640+
const other = codeownersMirrorVerdict({ text: onDisk, approvers: ['someone-else'] });
4641+
assert('⭐ the-approver-list-is-READ-not-restated-another-list-reds-the-file-on-disk', !other.ok && other.problems.length >= live.tierHPatterns.length, String(other.problems.length));
4642+
}
4643+
45324644
// ── The floor: every declared battery RAN, and ran its cases (#13489) ────
45334645
//
45344646
// Evaluated after every battery has had its chance and BEFORE the verdict, so

0 commit comments

Comments
 (0)