diff --git a/.github/workflows/governed-surface-guard.yml b/.github/workflows/governed-surface-guard.yml new file mode 100644 index 0000000000..0dda70c6d4 --- /dev/null +++ b/.github/workflows/governed-surface-guard.yml @@ -0,0 +1,127 @@ +# The machine half of the governed-surface no-bypass rule (objectui#6596, +# maintainer ruling 2026-08-27 accepting Option C on #6325). The rule, the event +# split, the approval predicate and the exit contract all live in +# `scripts/check-governed-queue-guard.mjs`; that header is authoritative and this +# file is the invocation. Only the wiring decisions are argued here. +# +# What it is for, in one measurement: PR #6183 touched `AGENTS.md`, was correctly +# parked as a draft, and a GitHub MCP `update_pull_request` call passing only +# `reviewers` silently set `draft: false`. The pull request entered the merge +# queue and landed as `5b3290fd5` with no human approval; converting it back to a +# draft did NOT dequeue it. Nothing in this repository could have refused that — +# the whole defence was seat discipline, and the failure contained no seat +# decision at all. +name: Governed Surface Guard + +on: + # BOTH legs are load-bearing and they mean DIFFERENT things — see the script + # header. `merge_group` is the leg that REFUSES: the queue build is the last + # thing between a speculative merge and `main`, and it is the path #6183 took. + # `pull_request` is an EARLY WARNING that deliberately exits 0, because a + # governed PR held as a draft for the maintainer to merge by hand is this + # regime's healthy end state, and a check that reddens on the healthy case is + # a permanently red check nobody reads. + pull_request: + # `develop` is included for the same reason every other requirable gate here + # names both: a context that does not report on a branch it could be + # required on leaves the pull request pending rather than failing it. + branches: [main, develop] + # ⚠️ Naming `types:` REPLACES GitHub's default set rather than extending it, + # so all three defaults are restated. `ready_for_review` is the addition and + # it is the point: flipping a governed draft to ready is the first move of + # the exact sequence this guard exists to interrupt, and it is not in the + # default set — without it the warning would not re-fire at the one moment a + # seat most needs to read it. `scripts/check-governed-queue-guard.mjs + # --self-test` pins this line. + types: [opened, synchronize, reopened, ready_for_review] + # Merge queue (objectui#3523 — see `ci.yml`'s trigger block for the full note + # and the measurements behind it). This is the leg that refuses, and a + # requirable context that does not report on a queue build stalls the queue + # until the ruleset's 60-minute status-check timeout. `types:` is named + # although `checks_requested` is currently the only one GitHub defines. + merge_group: + types: [checks_requested] + +# ⛔ NO `paths:` filter, on either leg, and this is not an oversight. A skipped +# job counts as SUCCESS in branch protection, so a path filter would hand the +# queue a green "Governed Surface Queue Guard" for a pull request the filter +# mis-scoped — on the one check whose entire job is to refuse. The path test +# belongs INSIDE the script, where "nothing governed" is a verdict that says so +# and costs zero API calls. (`merge_group` supports no path filter at all, so a +# filter would also make the two legs disagree about what they cover.) The +# script's `--self-test` fails if one is ever added; the same conclusion +# `control-bytes.yml` and `changeset-presence.yml` reached for themselves. + +concurrency: + group: governed-surface-guard-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +# The default read scopes and nothing beyond them. `pull-requests: read` is what +# the review and head reads need; there is no write scope here, and adding one +# to let this check "fix" anything would be a widening no ruling covers. +permissions: + contents: read + pull-requests: read + +jobs: + governed-surface-guard: + # ⚠️ THIS LITERAL IS THE CHECK-RUN NAME branch protection would pin, and it + # is duplicated in `CHECK_CONTEXT_NAME` in the script — deliberately, and + # pinned in both directions: the script's `--self-test` reads THIS FILE and + # fails if the two ever disagree. Renaming a job silently detaches a + # required context, and a name that lives in exactly one place is a name + # nothing can pin. It is also registered in `REQUIRED_CONTEXTS` in + # `scripts/dependabot-merge-gate.mjs`, which is this repository's own + # written-down answer to "which checks are blocking". + # + # ⛔ Making it a REQUIRED context in the live branch-protection ruleset is + # not this pull request's step and cannot be: that is a repository-settings + # surface only the maintainer can change. Until it is flipped, this job + # REPORTS on a queue build without stopping it. + name: Governed Surface Queue Guard + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + # A governed-surface question answered over a truncated history + # answers with SILENCE, and silence reads as compliance. A merge + # group's base sha can predate a shallow fetch's floor, so the whole + # history is the only depth that cannot under-report. The script + # refuses outright (exit 1) if either sha is missing, rather than + # diffing what it happens to have. + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: '22.x' + + # The self-test runs FIRST and in its own step, because it is the + # precondition for trusting the run after it: this guard's predicates + # decide whether a merge lands, so a rotted predicate must redden here + # rather than quietly wave a governed diff through. It also holds the + # wiring pins — the job name above, the triggers, `fetch-depth: 0`, the + # absent path filter, and the `REQUIRED_CONTEXTS` registration. + # + # No install and no build: the script imports node builtins and one local + # module (`scripts/invoked-as.mjs`) only, which is also what keeps it + # clear of `scripts/check-pre-install-import-graph.mjs`. + - name: Guard predicate self-test + run: node scripts/check-governed-queue-guard.mjs --self-test + + # The live judgment. Everything it reads arrives through `env:` or the + # event payload on disk — no `${{ }}` interpolation into the shell line, + # so no pull-request-controlled text ever becomes part of a command. + # + # GITHUB_TOKEN is the workflow's own default token at the read scopes + # declared above; it is what makes the review read possible at all, and an + # unreadable review list is a REFUSAL with its own exit code, never a + # pass. ⛔ Fail-open is wrong in this file specifically — it exists + # because every other layer in this chain failed open. + - name: Governed surfaces may not enter the merge queue unreviewed + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node scripts/check-governed-queue-guard.mjs diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index ddead2537a..783478e5ad 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -38,6 +38,7 @@ one has its own section below. | `shell-escape-residue.yml` | Shell Escape Residue Scan | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a fenced block in `AGENTS.md`, `CLAUDE.md`, `skills/**` or `content/docs/**` carries the enumerated machine-produced shell escape, or a scan root fails to resolve | | `readme-exports.yml` | README Export Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a `packages/**/README.md` imports a name from its own package that the package does not export, or the scan's population collapses | | `docs-route-eager-closure.yml` | Docs Route Eager Closure Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a package named in `apps/site/app/components/registerCatalogBlocks.ts` is not already reachable from the docs route's module graph (exit 1), or when the gate's own gauge cannot be trusted (exit 2) | +| `governed-surface-guard.yml` | Governed Surface Queue Guard | PR to `main`, `develop` (incl. `ready_for_review`) — **no path filter**; merge-queue builds | **Yes on a queue build only** — a governed-surface diff with no authorized approval pinned to the PR's current head is refused there; on the pull request itself it is deliberately green and prints an early warning | | `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget | | `live-e2e.yml` | Live E2E (informational) | PR to `main`, `develop` (code paths); nightly cron `30 6 * * *`; manual | No — informational lane, `continue-on-error` | | `labeler.yml` | Auto Label PRs | PR `opened`, `synchronize`, `reopened` | No | @@ -1053,6 +1054,60 @@ record it in `MEASURED_PAYLOAD` with what it is for. Run it locally with `pnpm check:docs-route-closure`; a green run prints the full classification, including which file each *free* package is already imported by. +## Governed Surface Guard (`governed-surface-guard.yml`) + +**Trigger:** Pull request to `main` / `develop` (`opened`, `synchronize`, `reopened`, +`ready_for_review`) and merge-queue builds — **no path filter** on either leg. +**Appears as:** **Governed Surface Queue Guard**. +**Blocks a PR?** Not on the pull request, by design. On a merge-queue build it refuses. + +The **governed surface** is a fixed list — `AGENTS.md`, `CLAUDE.md`, `.claude/**`, `skills/**`, +`docs/adr/**` — and the rule about it is that a change to any of them is merged by a human, not by +the queue. That rule used to live only in prose. On +[#6183](https://github.com/objectstack-ai/objectui/pull/6183) an `AGENTS.md` change was correctly +parked as a draft; a GitHub MCP `update_pull_request` call passing only `reviewers` silently also +set `draft: false`; the pull request entered the merge queue and landed with no human approval, and +converting it back to a draft did not dequeue it. Nothing in CI could have refused that. This +workflow is the refusal ([#6596](https://github.com/objectstack-ai/objectui/issues/6596)). + +**The two legs mean different things, and that is the whole design.** On a **pull request** the +check is deliberately green whatever it finds, and prints an early warning naming the governed paths +and the sequence not to start. A governed pull request sitting as a draft for the maintainer to +merge by hand is the *healthy* end state, so a check that reddened on it would be red on the healthy +case forever — and a permanently red check is one everybody learns to ignore. On a **merge-queue +build** the same finding is a refusal: that is a state a governed pull request should never be in at +all, so red there is red on the anomaly. + +**What clears the queue leg** is an `APPROVED` review by an account in `GOVERNED_APPROVERS` whose +`commit_id` equals the pull request's *current* head sha. The sha pin is what makes the approval an +approval *of something*: a push after the approval goes stale and reopens the refusal, so a +clearance cannot outlive the bytes it was given for. Dismissed and superseded approvals never count. +The remedy the refusal prints **first** is not approval at all — convert the pull request back to a +draft and leave the merge to the maintainer. + +**What it costs when nothing is governed:** nothing. The path test runs before any request is +constructed, so an ordinary pull request produces a `CLEAR` verdict and **zero** GitHub API calls; +an API outage cannot block a diff that touches no governed path. The mirrored requirement is that an +API error on a diff that *is* governed is a refusal with its own exit code (4, distinct from 3 for +"nobody approved"), never a pass — this gate exists because every other layer in the chain failed +open. + +**What it deliberately does not do.** It does not govern its own workflow or CI configuration +generally: that would be a larger rule than the one that was ruled. It cannot stop a maintainer +merging a governed pull request by hand, and does not try — under this regime the human merge *is* +the review record. And it does not make itself required: that is a branch-protection setting only +the maintainer can flip. Until it is flipped, the queue leg reports without stopping anything. What +this repository can write down, and has, is `REQUIRED_CONTEXTS` in `scripts/dependabot-merge-gate.mjs`. + +**If it fails:** read the verdict — it names every governed path that matched, the pull request each +belongs to, and the two ways out. To ask the same question about a file list before pushing, run +`pnpm governed -- AGENTS.md packages/core/src/index.ts` (or +`node scripts/check-governed-queue-guard.mjs --test `); it exits 0 when nothing is governed. +The predicates are covered by `node scripts/check-governed-queue-guard.mjs --self-test`, which the +workflow runs as its own first step because a rotted predicate must redden rather than wave a +governed diff through, and the wiring is pinned by +`scripts/__tests__/check-governed-queue-guard.test.ts`. + ## Link Checking (`check-links.yml`) **Trigger:** Weekly cron (`17 4 * * 0` — Sundays, off the top of the hour, when the scheduled-run diff --git a/package.json b/package.json index 90157792f4..729fe9c92e 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,8 @@ "check:docs-route-closure": "node scripts/check-docs-route-eager-closure.mjs", "check:entry-guard": "node scripts/check-entry-guard.mjs", "check:upstream-port-parity": "node scripts/check-upstream-port-parity.mjs", + "check:governed-queue-guard": "node scripts/check-governed-queue-guard.mjs --self-test", + "governed": "node scripts/check-governed-queue-guard.mjs --test", "check:pre-install-import-graph": "node scripts/check-pre-install-import-graph.mjs", "check:vi-mock-specifiers": "node scripts/check-vi-mock-specifiers.mjs", "check:shell-escape-residue": "node scripts/check-shell-escape-residue.mjs", diff --git a/scripts/__tests__/check-governed-queue-guard.test.ts b/scripts/__tests__/check-governed-queue-guard.test.ts new file mode 100644 index 0000000000..380f8b6cf3 --- /dev/null +++ b/scripts/__tests__/check-governed-queue-guard.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, it } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parse as parseYaml } from 'yaml'; + +import { REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs'; +import { + CHECK_CONTEXT_NAME, + CHECK_JOB_ID, + CHECK_WORKFLOW, + GOVERNED_SURFACES, + governedPathsIn, +} from '../check-governed-queue-guard.mjs'; + +const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); +const GATE = 'scripts/check-governed-queue-guard.mjs'; +const WORKFLOW = `.github/workflows/${CHECK_WORKFLOW}`; + +/** + * objectui#6596. PR #6183 touched `AGENTS.md`, was correctly parked as a draft, + * and a GitHub MCP `update_pull_request` call passing only `reviewers` silently + * set `draft: false`. It entered the merge queue and landed as `5b3290fd5` with + * no human approval; converting it back to a draft did not dequeue it. + * + * The gate itself carries a `--self-test` covering its predicates, and this file + * does NOT duplicate it — it pins the gate to its WIRING, in the direction that + * goes wrong quietly. A refusal nothing runs is indistinguishable from a refusal + * that passes, which is the state the whole governed-surface rule was already in + * one level up: a rule with no mechanism under it. + * + * Deliberately NOT asserted here: whether the context is in the live required + * set. That is repository settings, which no test here can read and no agent may + * change. What IS asserted is that this repository has written the answer down + * where its own tooling reads it, and that every piece of the chain exists. + */ +describe('check-governed-queue-guard is wired, not merely present', () => { + const workflowText = fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8'); + const workflow = parseYaml(workflowText); + const job = workflow.jobs[CHECK_JOB_ID]; + + it('the gate script and its workflow both exist', () => { + expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true); + expect(fs.existsSync(path.join(ROOT, WORKFLOW))).toBe(true); + }); + + it('package.json aliases it, and the alias points at the script that exists', () => { + const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); + for (const alias of ['check:governed-queue-guard', 'governed']) { + expect(pkg.scripts[alias], `package.json must alias ${alias}`).toBeTruthy(); + expect(pkg.scripts[alias]).toContain(GATE); + } + }); + + it('the job publishes exactly the check name the script declares', () => { + // The #6865-shape defect one repo over: renaming a job detaches a required + // context with nothing to say so. The name lives in two files because a name + // in one file is a name nothing can pin; this is the assertion that keeps + // the two equal. + expect(job.name).toBe(CHECK_CONTEXT_NAME); + }); + + it('this repository has written the context down as blocking', () => { + // `REQUIRED_CONTEXTS` is objectui's own answer to "which checks are + // blocking" (`scripts/dependabot-merge-gate.mjs`), and it is what + // `merge-queue-reporting.test.ts` derives the merge_group floor from. A + // guard outside it is a guard that floor cannot see. + expect(REQUIRED_CONTEXTS).toContain(CHECK_CONTEXT_NAME); + }); + + it('subscribes BOTH legs — the queue build refuses, the pull request warns', () => { + // Both, not either. Without `merge_group` there is no refusal at all; + // without `pull_request` a seat gets no warning before the queue, which is + // the moment #6183 could still have been stopped. + expect(workflow.on).toHaveProperty('merge_group'); + expect(workflow.on).toHaveProperty('pull_request'); + }); + + it('re-fires on ready_for_review — the first move of the incident', () => { + // Naming `types:` REPLACES GitHub's default set rather than extending it, + // so the three defaults have to be restated alongside the addition. This + // asserts the addition AND that restating did not drop one. + expect(workflow.on.pull_request.types).toEqual([ + 'opened', + 'synchronize', + 'reopened', + 'ready_for_review', + ]); + }); + + it('carries no path filter on either leg — a skipped job counts as SUCCESS', () => { + // The one direction this gate may not be wrong in. A path filter that + // mis-scopes hands branch protection a green verdict from a job that never + // ran, on the one check whose entire purpose is to refuse. + for (const leg of ['pull_request', 'merge_group'] as const) { + const on = workflow.on[leg] ?? {}; + expect(on).not.toHaveProperty('paths'); + expect(on).not.toHaveProperty('paths-ignore'); + } + }); + + it('checks out full history — a truncated diff answers with silence', () => { + const checkout = job.steps.find((s: Record) => String(s.uses ?? '').startsWith('actions/checkout')); + expect(checkout, 'the job must check out the repository').toBeDefined(); + expect(checkout.with['fetch-depth']).toBe(0); + }); + + it('grants pull-requests: read and no write scope anywhere', () => { + expect(workflow.permissions).toEqual({ contents: 'read', 'pull-requests': 'read' }); + }); + + it('runs the self-test BEFORE the live judgment, as its precondition', () => { + const runs: string[] = job.steps + .filter((s: Record) => typeof s.run === 'string') + .map((s: Record) => s.run); + const selfTestAt = runs.findIndex((r) => r.includes(`${GATE} --self-test`)); + const liveAt = runs.findIndex((r) => r.includes(GATE) && !r.includes('--self-test')); + expect(selfTestAt, 'the workflow must run the self-test').toBeGreaterThan(-1); + expect(liveAt, 'the workflow must run the live judgment').toBeGreaterThan(-1); + expect(selfTestAt).toBeLessThan(liveAt); + }); + + it('its self-test passes — the half that makes a green run mean something', () => { + const out = execFileSync('node', [GATE, '--self-test'], { cwd: ROOT, encoding: 'utf8' }); + expect(out).toMatch(/check-governed-queue-guard self-test: \d+ cases pass/); + }); +}); + +/** + * The governed surface itself, asserted from OUTSIDE the gate. The gate's own + * `--self-test` pins the same set, and that is not a duplicate: this file pins + * it against the repository's real tree, so a surface that is declared but does + * not exist on disk fails here — a governed path nobody can edit is a rule about + * nothing, and the reverse (a surface silently dropped) is the rule quietly + * shrinking. + */ +describe('the governed surface is the 2026-08-18 definition, and it is real', () => { + it('declares exactly the five ruled surfaces', () => { + expect(GOVERNED_SURFACES.map((s) => s.glob)).toEqual([ + 'docs/adr/**', + '.claude/**', + 'skills/**', + 'AGENTS.md', + 'CLAUDE.md', + ]); + }); + + it('every declared surface exists in this tree', () => { + for (const surface of GOVERNED_SURFACES) { + // `in` rather than `surface.prefix ?? surface.exact`: the register is a + // union of frozen literals, so exactly one of the two keys exists on each + // member and neither exists on all of them. `tsconfig.scripts.json` infers + // these types from the `.mjs` (allowJs, checkJs off), so the optional-chain + // spelling is a real type error here rather than a stylistic preference. + const target = 'prefix' in surface ? surface.prefix : surface.exact; + expect( + fs.existsSync(path.join(ROOT, target)), + `GOVERNED_SURFACES declares ${target}, which does not exist. A governed path nobody can ` + + `edit is a rule about nothing; either the surface moved (update the register) or the ` + + `entry was speculative (drop it).`, + ).toBe(true); + } + }); + + it('does not govern its own workflow, or CI configuration generally', () => { + // The widening deliberately NOT taken. Promoting CI config to a governed + // surface is a strictly larger rule than the one ruled, and taking it in an + // implementation would be a governance gate acquiring policy nobody agreed + // to. Pinned so a later edit has to be a decision. + expect(governedPathsIn([WORKFLOW, GATE, '.github/workflows/ci.yml'])).toEqual([]); + }); + + it('the two root rows are EXACT — a vendored copy is ordinary source', () => { + expect(governedPathsIn(['examples/AGENTS.md', 'packages/cli/templates/CLAUDE.md'])).toEqual([]); + expect(governedPathsIn(['AGENTS.md']).map((s) => s.id)).toEqual(['agents-md']); + }); +}); diff --git a/scripts/__tests__/merge-queue-reporting.test.ts b/scripts/__tests__/merge-queue-reporting.test.ts index 6a5658ce04..7697e9ecc4 100644 --- a/scripts/__tests__/merge-queue-reporting.test.ts +++ b/scripts/__tests__/merge-queue-reporting.test.ts @@ -115,6 +115,17 @@ const MUST_SUBSCRIBE_MERGE_GROUP = new Map([ 'this gate carries no path filter, reports on every pull request, and is requirable; ' + '`scripts/dependabot-merge-gate.mjs` already classifies it as a required context', ], + [ + 'governed-surface-guard.yml', + 'produces Governed Surface Queue Guard — added by objectui#6596. The one entry here whose ' + + 'REASON is the queue build itself rather than merely reporting on it: its `pull_request` leg ' + + 'is deliberately green whatever it finds, and its refusal exists only on `merge_group`, so a ' + + 'missing subscription would remove the entire verdict rather than just delay it. It carries ' + + 'no path filter either — a skipped job counts as SUCCESS in branch protection, which on a ' + + 'check whose whole job is to refuse is the failure mode itself — so it reports on every pull ' + + 'request and is requirable; `scripts/dependabot-merge-gate.mjs` classifies it as a required ' + + 'context', + ], ]); /** diff --git a/scripts/check-governed-queue-guard.mjs b/scripts/check-governed-queue-guard.mjs new file mode 100644 index 0000000000..f764af4cf0 --- /dev/null +++ b/scripts/check-governed-queue-guard.mjs @@ -0,0 +1,1361 @@ +#!/usr/bin/env node + +/** + * check-governed-queue-guard — a governed-surface change may not reach `main` + * through the merge queue without a human approval pinned to the bytes that + * approval was given for. + * + * node scripts/check-governed-queue-guard.mjs # in CI, from the event payload + * node scripts/check-governed-queue-guard.mjs --test # offline: "would these govern a PR?" + * node scripts/check-governed-queue-guard.mjs --self-test # offline, no network, no git + * + * ## The measured incident this exists for (objectui#6596, ruling 2026-08-27) + * + * PR #6183 touched `AGENTS.md` and was correctly parked as a draft for the + * maintainer to merge by hand. A GitHub MCP `update_pull_request` call passing + * only `reviewers` silently also set `draft: false`; the pull request entered + * the merge queue and landed as `5b3290fd5` with no human approval. Converting + * it back to a draft afterwards did NOT dequeue it. The tool fact is + * objectstack-ai/objectstack#12200; the incident record and the decision are + * objectui#6325. + * + * Every layer that could have stopped it was a layer of seat discipline, and + * the failure had no seat in it at all — a hidden side effect on a tool call + * plus a queue that does not release what it has taken. The maintainer's ruling + * (「同意,并继续」, accepting Option A + C on #6325) is that the rule stops + * resting on discipline: this file is the refusal. + * + * ## ⭐ The split by EVENT — the single most load-bearing decision here + * + * `merge_group` → a governed diff with no AUTHORIZED approval pinned to the + * pull request's CURRENT head is a REFUSAL. The queue build + * is the last thing between a speculative merge and `main`, + * and it is the path the incident took. + * `pull_request` → the identical finding is an EARLY WARNING that exits 0. + * + * The pull-request leg must not redden, and not out of politeness. A governed + * PR sitting as a draft awaiting the maintainer's own merge is the CORRECT + * terminal state of this regime, so a check that is red on it is red on the + * healthy case, forever — and a permanently red check trains everyone to ignore + * red. The sibling repository retired a gate for exactly that (objectstack's + * 2026-08-18 ruling, 红灯常态化本身有毒). The queue build is the opposite: a + * state a governed PR should never be in at all, so red there is red on the + * anomaly. + * + * ⚠️ Stated out loud rather than discovered: this guard CANNOT stop a + * maintainer merging a governed PR by hand, and does not try. A direct merge + * produces no `merge_group` event. That is not a hole — under this regime the + * human merge IS the review record. What it closes is the seat path: flip + * ready → enqueue → the queue is the entire review, which is the shape of + * #6183 exactly. + * + * ## What satisfies the queue leg + * + * An APPROVED review by an account in `GOVERNED_APPROVERS` whose `commit_id` + * equals the pull request's CURRENT head sha. Stale approvals (any push after + * the approval) never count; DISMISSED and superseded approvals never count. + * The sha pin is what makes the approval an approval OF SOMETHING: it binds the + * clearance to the exact bytes that were read, so a later push reopens this + * refusal instead of riding the old approval through — the generalisation of + * #6183, where the PR's own state changed under a review nobody had given. + * + * The preferred remedy is NOT approval, and the refusal text says so first: + * take the pull request out of the queue, convert it back to DRAFT, and leave + * the merge to the maintainer. + * + * ⛔ An agent seat never submits an approving review on a governed-surface pull + * request, under any account. Every seat in this repository writes under a + * shared GitHub identity, so `GOVERNED_APPROVERS` is a technical control that + * is only as good as that normative rule — the same class as the seat-side + * no-merge rule, and the reason the DRAFT remedy is listed first. + * + * ## Ordering: the path test runs FIRST, and a clear diff costs zero API calls + * + * ⛔ Fail-open on an API error is wrong in this file — it exists because + * everything else in the chain failed open — so an unreadable review list is a + * REFUSAL with its own exit code, never a pass. But a diff touching nothing + * governed must never be blocked by an API hiccup either. The two are + * reconciled by ORDER, not by tolerance: `runGuard` decomposes the diff and + * returns before constructing a single request when nothing governed is in it. + * The self-test pins that with a `fetchReviews` that THROWS if it is called at + * all — a spy, not a mock, because "we did not need the API" is the claim. + * + * ## Multi-PR merge groups, and the under-enumeration trap + * + * A merge group can carry SEVERAL pull requests. `merge_group.head_ref` names + * only the LAST one, so keying the whole group's diff to it would check the + * wrong PR's reviews — and in the direction that reads as compliance: PR B is + * approved, PR A's governed diff rides in behind it. So the group is decomposed + * PER COMMIT: each first-parent commit is one PR landing, its number read from + * its subject, and every governed PR is judged on its OWN reviews. A commit + * that touches a governed path and names no pull request is UNATTRIBUTED — its + * own refusal, with its own exit code. + * + * ## Why this is objectui-native rather than a pinned port + * + * The sibling repository runs this mechanism already, and this file follows its + * shape closely. It is NOT registered in `scripts/upstream-port-pin.json`, and + * that is a measurement rather than a preference: + * + * - Upstream splits the mechanism over two files, and the register half + * (`scripts/pm/check-governed-merges.mjs`, 2,614 lines) is mostly a + * multi-repo post-merge audit plus a provenance-recompute engine for + * GENERATED artifacts sitting inside governed paths. This repository has + * none of those artifacts: `.claude/workflows/` does not exist here, and + * `skills/` carries no generator output (no `references/_index.md`, no + * react-blocks contract) — measured on the tree this landed against. Every + * row of that register is inapplicable, so a port would carry ~1,500 lines + * of machinery that can never fire. + * - `scripts/check-upstream-port-parity.mjs` cannot express that. Its + * `validatePin` refuses a divergence whose `ported` side is empty + * (`files[0].divergences[0].ported is empty`, exit 2 — measured directly + * against the shipped function), so a pin has no way to declare a DELETION. + * A pinned port here is structurally impossible, not merely undesirable. + * + * So the divergence is declared in prose, where a reader can act on it, and the + * obligation the pin would have carried is stated instead: when the sibling's + * predicate changes, this file is a hand re-read, not an automatic re-sync. + * + * ## Exit codes — the refusal is impossible to read as clean + * + * 0 CLEAR — nothing governed in the diff (no API call was made), or every + * governed PR carries an authorized APPROVED review pinned to + * its current head, or this is the `pull_request` early warning. + * 3 REFUSED — governed, and at least one governed PR carries no authorized + * APPROVED review pinned to its current head (none at all, + * unauthorized account, stale sha, dismissed or superseded). + * 4 REFUSED — governed, and the PR head or review list could not be READ. + * Distinct from 3 on purpose: "nobody approved" and "we could + * not find out" are different facts and must be separable. + * 5 REFUSED — governed paths on a commit attributable to no pull request. + * 1 CANNOT RUN — unusable event payload, unsupported event, unreadable git. + * Still non-zero, still red: this file has no green that means + * "did not look". + * 2 BAD USAGE — `--test` with no paths. Not a verdict about a tree. + * + * ## What this file does NOT do + * + * It does not make itself a required context. That is a branch-protection + * setting only the maintainer can flip. What this repository CAN write down is + * `REQUIRED_CONTEXTS` in `scripts/dependabot-merge-gate.mjs` — its own answer + * to "which checks are blocking" — and `CHECK_CONTEXT_NAME` below is registered + * there. Until the live required set carries the same name, this guard REPORTS + * on a queue build without stopping it; the self-test pins the registration and + * the workflow's `name:` so the two can never drift apart silently. + */ + +import { execFileSync } from 'node:child_process'; +import { appendFileSync, readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(scriptDir, '..'); + +/** The exit contract, named so the header's table is machine-checkable. */ +export const EXIT_CLEAR = 0; +export const EXIT_CANNOT_RUN = 1; +export const EXIT_BAD_USAGE = 2; +export const EXIT_REFUSED_UNAPPROVED = 3; +export const EXIT_REFUSED_UNREADABLE = 4; +export const EXIT_REFUSED_UNATTRIBUTED = 5; + +/** + * The check-run name branch protection would pin, and the wiring it belongs to. + * Declared HERE as well as in the YAML, deliberately and pinned in both + * directions: the self-test reads the workflow and fails if the two disagree. + * Renaming a job silently detaches a required context, and a name that lives in + * exactly one place is a name nothing can pin. + */ +export const CHECK_CONTEXT_NAME = 'Governed Surface Queue Guard'; +export const CHECK_WORKFLOW = 'governed-surface-guard.yml'; +export const CHECK_JOB_ID = 'governed-surface-guard'; + +/** The events this guard understands, and what each one means to it. */ +export const EVENT_MERGE_GROUP = 'merge_group'; +export const EVENT_PULL_REQUEST = 'pull_request'; + +/** + * The governed surface, verbatim from the 2026-08-18 definition the card + * restates: `AGENTS.md`, `CLAUDE.md`, `.claude/**`, `skills/**`, `docs/adr/**`. + * + * ⛔ This list is the WHOLE scope and widening it is a maintainer decision, not + * an implementation detail. Two omissions are deliberate and worth naming so a + * future reader does not read them as oversights: + * + * - `.github/workflows/**` is NOT governed, so this guard does not govern its + * own workflow file. Making CI configuration governed is a strictly larger + * rule than the one ruled, and taking it here would be widening a + * governance gate past its own ruling. + * - `examples/**` and any vendored template copy of `AGENTS.md` stay out: the + * two root rows are EXACT matches, so `examples/AGENTS.md` is ordinary + * source. + * + * `prefix` rows match by path prefix; `exact` rows match the whole path. `glob` + * is the spelling a human reads in a verdict, never a matcher. + */ +export const GOVERNED_SURFACES = Object.freeze([ + Object.freeze({ id: 'adr', prefix: 'docs/adr/', glob: 'docs/adr/**', what: 'architecture decision records' }), + Object.freeze({ id: 'claude-tree', prefix: '.claude/', glob: '.claude/**', what: 'the agent instruction tree (skills, hooks, settings)' }), + Object.freeze({ id: 'skills-catalog', prefix: 'skills/', glob: 'skills/**', what: 'the published skills catalog' }), + Object.freeze({ id: 'agents-md', exact: 'AGENTS.md', glob: 'AGENTS.md', what: 'the repo-root agent instruction file' }), + Object.freeze({ id: 'claude-md', exact: 'CLAUDE.md', glob: 'CLAUDE.md', what: 'the repo-root Claude instruction file' }), +]); + +/** + * The ONLY accounts whose APPROVED review satisfies the `merge_group` leg. + * + * ⭐ Single source: every rendering derives the names it prints from this array, + * and nothing else in the repository restates them as data. Changing the set is + * a one-line maintainer decision here and nowhere else. + * + * ⚠️ Provenance, stated because it is INHERITED rather than ruled for this + * repository: these are the two accounts the sibling repository's maintainer + * ruled authoritative for its own governed surface on 2026-08-27 (verbatim: + * 「os-zhuang hotlong 批准算数」). objectui's own card rules the MECHANISM, not + * the roster. The roster is carried across because it is the same maintainer + * and the same governed surface, and it is flagged here — and in this change's + * pull request — as the one line the maintainer should confirm or edit. Nothing + * is blocked either way: the ruled remedy this guard prints FIRST is + * back-to-draft plus a human merge, which needs no approver at all. + */ +export const GOVERNED_APPROVERS = Object.freeze(['os-zhuang', 'hotlong']); + +/** + * The governed slice of a path list, grouped by surface. Surfaces with no hit + * are absent — `length === 0` IS the clean answer. + */ +export function governedPathsIn(paths) { + const list = Array.isArray(paths) ? paths : []; + return GOVERNED_SURFACES.map((surface) => ({ + ...surface, + files: list.filter((p) => typeof p === 'string' && (surface.prefix ? p.startsWith(surface.prefix) : p === surface.exact)), + })).filter((surface) => surface.files.length > 0); +} + +/** + * The seat-side answer: "would a pull request touching these paths be + * governed?" Pure, so `--test` costs a process start and nothing else, and the + * refusal text can hand a reader a command that answers the same question the + * queue build answered. + */ +export function testVerdict(paths) { + const list = (Array.isArray(paths) ? paths : []).filter((p) => typeof p === 'string' && p !== ''); + const matched = governedPathsIn(list); + const hit = new Set(matched.flatMap((s) => s.files)); + return { + governed: matched.length > 0, + checked: list.length, + surfacesChecked: GOVERNED_SURFACES.length, + matched, + hitPaths: [...hit], + clearPaths: list.filter((p) => !hit.has(p)), + }; +} + +/** + * The pull-request number a mainline commit subject names, in either spelling + * GitHub writes: a merge commit's `Merge pull request #N from …`, or a squash + * commit's TRAILING `(#N)`. This repository squashes, so the trailing form is + * the one that fires; a subject citing an issue mid-title keeps only the + * trailing parenthetical, which is the pull request. + */ +export function pullNumberFromSubject(subject) { + if (typeof subject !== 'string') return null; + let m = /^Merge pull request #(\d+)\b/.exec(subject); + if (m) return Number(m[1]); + m = /\(#(\d+)\)\s*$/.exec(subject.trim()); + return m ? Number(m[1]) : null; +} + +/** + * The pull-request number a merge-queue head ref names, or null. + * + * GitHub writes `refs/heads/gh-readonly-queue//pr--`. The + * `gh-readonly-queue/` segment is required rather than decorative: a plain + * branch called `pr-12-abcdef1` is not a queue ref, and reading one as a pull + * request number would attribute a diff to a pull request unrelated to it. + * + * The base-branch segment is `.+` rather than `[^/]+` because a base branch may + * itself contain slashes; the trailing `pr--` anchor is what makes the + * greedy match safe. ⚠️ In a MULTI-PR group this names only the LAST pull + * request, so it is a fallback for single-commit groups and never the key the + * whole group is judged on. + */ +export function pullNumberFromQueueRef(ref) { + const m = /(?:^|\/)gh-readonly-queue\/.+\/pr-(\d+)-[0-9a-f]{7,40}$/.exec(String(ref ?? '')); + return m ? Number(m[1]) : null; +} + +/** + * The shas and pull identity a workflow event carries. Pure, so every branch — + * including the malformed payloads — is offline-testable. + * + * `ok: false` is never a quiet default: an event this guard cannot read is + * `EXIT_CANNOT_RUN`, because "I could not tell what was being merged" must not + * render as "nothing governed was being merged". + */ +export function resolveEventContext({ eventName, payload }) { + if (eventName === EVENT_MERGE_GROUP) { + const group = payload?.merge_group; + if (!group?.base_sha || !group?.head_sha) { + return { ok: false, reason: 'the merge_group payload carries no base_sha/head_sha — nothing to diff' }; + } + return { + ok: true, + event: EVENT_MERGE_GROUP, + baseSha: group.base_sha, + headSha: group.head_sha, + namedPull: pullNumberFromQueueRef(group.head_ref), + label: `merge group on ${group.base_ref ?? 'main'}`, + }; + } + if (eventName === EVENT_PULL_REQUEST) { + const pull = payload?.pull_request; + if (!pull?.number || !pull?.base?.sha || !pull?.head?.sha) { + return { ok: false, reason: 'the pull_request payload carries no number/base.sha/head.sha — nothing to diff' }; + } + return { + ok: true, + event: EVENT_PULL_REQUEST, + baseSha: pull.base.sha, + headSha: pull.head.sha, + namedPull: Number(pull.number), + draft: pull.draft === true, + label: `pull request #${pull.number}`, + }; + } + return { + ok: false, + reason: + `unsupported event '${eventName ?? '(none)'}' — this guard reads ${EVENT_MERGE_GROUP} (the refusal) and ` + + `${EVENT_PULL_REQUEST} (the early warning) only`, + }; +} + +/** + * Split the work in a diff into the pull requests that touched a governed + * surface, plus the governed work no pull request can be found for. + * + * Pure. `rows` are `{ sha, subject, pr, paths }` — one per first-parent commit + * in a merge group, or one synthetic row for a `pull_request` run. A row hitting + * nothing governed is dropped entirely and costs nothing downstream; that is + * what makes "a clear diff makes no API call" a property of the data flow + * rather than a promise in a comment. + */ +export function decomposeGovernedWork(rows) { + const byPull = new Map(); + const unattributed = []; + for (const row of Array.isArray(rows) ? rows : []) { + const surfaces = governedPathsIn(row?.paths ?? []); + if (surfaces.length === 0) continue; + const paths = surfaces.flatMap((s) => s.files); + if (typeof row.pr !== 'number' || !Number.isInteger(row.pr) || row.pr <= 0) { + unattributed.push({ sha: row.sha ?? null, subject: row.subject ?? '', paths }); + continue; + } + const seen = byPull.get(row.pr) ?? { pr: row.pr, paths: new Set(), shas: [] }; + for (const p of paths) seen.paths.add(p); + if (row.sha) seen.shas.push(row.sha); + byPull.set(row.pr, seen); + } + const governed = [...byPull.values()] + .map((entry) => ({ pr: entry.pr, shas: entry.shas, paths: [...entry.paths], surfaces: governedPathsIn([...entry.paths]) })) + .sort((a, b) => a.pr - b.pr); + return { governed, unattributed }; +} + +/** + * Does an APPROVED review exist on this pull request? The early-warning leg's + * reading — any approver counts, no sha pin — because that leg never reddens + * and so never needs the stricter, more expensive question. + * + * The reduction is LATEST-DECISIVE-PER-REVIEWER, matching how GitHub itself + * computes a review decision: `COMMENTED` and `PENDING` carry no decision, and a + * `DISMISSED` approval is not an approval any more. A reviewer who approved and + * later requested changes must not still read as an approver — the naive + * `reviews.some(r => r.state === 'APPROVED')` gets that wrong in the fail-open + * direction, which is the one direction this file may not be wrong in. + * + * Pure; the array is expected in GitHub's chronological order, so last wins. + */ +export function approvalVerdict(reviews) { + const decisive = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']); + const latest = new Map(); + for (const review of Array.isArray(reviews) ? reviews : []) { + const state = String(review?.state ?? '').toUpperCase(); + if (!decisive.has(state)) continue; + const login = review?.user?.login ?? `(unknown:${review?.id ?? latest.size})`; + latest.set(login, state); + } + return { + state: [...latest.values()].includes('APPROVED') ? 'approved' : 'unapproved', + approvers: [...latest].filter(([, s]) => s === 'APPROVED').map(([login]) => login), + changesRequestedBy: [...latest].filter(([, s]) => s === 'CHANGES_REQUESTED').map(([login]) => login), + reviewsRead: Array.isArray(reviews) ? reviews.length : 0, + }; +} + +/** + * The `merge_group` predicate: does an account in `GOVERNED_APPROVERS` hold a + * latest-decisive APPROVED review whose `commit_id` equals the pull request's + * CURRENT head sha? + * + * Same latest-decisive-per-reviewer reduction as `approvalVerdict`, with two + * more ways to not count, each reported separately so a queue log can be acted + * on: `staleApprovers` (authorized, APPROVED, wrong sha — a push happened after + * the approval) and `unauthorizedApprovers` (APPROVED, not in the set). An empty + * or unparsable head sha pins NOTHING: fail closed, never "any sha". + * + * ⚠️ An outstanding CHANGES_REQUESTED from another reviewer does not flip the + * verdict, and that is restraint rather than an oversight — the predicate is the + * authorized pinned approval, and widening a governance gate past its own rule + * is how gates acquire policy nobody agreed to. It is printed loudly instead. + * + * Pure; the array is expected in GitHub's chronological order, so last wins. + */ +export function pinnedApprovalVerdict(reviews, headSha) { + const decisive = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']); + const latest = new Map(); + for (const review of Array.isArray(reviews) ? reviews : []) { + const state = String(review?.state ?? '').toUpperCase(); + if (!decisive.has(state)) continue; + const login = review?.user?.login ?? `(unknown:${review?.id ?? latest.size})`; + latest.set(login, { state, commitId: String(review?.commit_id ?? '').toLowerCase() }); + } + const head = /^[0-9a-f]{7,40}$/.test(String(headSha ?? '').toLowerCase()) ? String(headSha).toLowerCase() : null; + const approvers = []; + const staleApprovers = []; + const unauthorizedApprovers = []; + for (const [login, review] of latest) { + if (review.state !== 'APPROVED') continue; + if (!GOVERNED_APPROVERS.includes(login)) unauthorizedApprovers.push(login); + else if (head !== null && review.commitId === head) approvers.push(login); + else staleApprovers.push({ login, commitId: review.commitId }); + } + return { + state: approvers.length > 0 ? 'approved' : 'unapproved', + approvers, + staleApprovers, + unauthorizedApprovers, + changesRequestedBy: [...latest].filter(([, r]) => r.state === 'CHANGES_REQUESTED').map(([login]) => login), + reviewsRead: Array.isArray(reviews) ? reviews.length : 0, + headSha: head ?? String(headSha ?? ''), + }; +} + +/** The refusal an unreadable PR head or review list produces. Never a pass — see the header. */ +export function unreadableApproval(reason) { + return { state: 'unreadable', approvers: [], changesRequestedBy: [], reviewsRead: 0, reason: String(reason ?? 'unknown error') }; +} + +/** + * The verdict, as data. Pure — every branch of the decision is here, and the + * renderer and the exit code both read it rather than re-deriving it. + */ +export function guardVerdict({ event, governed = [], unattributed = [], approvals = new Map(), apiCalls = 0 }) { + const entries = governed.map((entry) => ({ + ...entry, + approval: approvals.get(entry.pr) ?? unreadableApproval('no review reading was recorded for this pull request'), + })); + const base = { event, entries, unattributed, apiCalls, contextName: CHECK_CONTEXT_NAME }; + + if (entries.length === 0 && unattributed.length === 0) { + return { ...base, conclusion: 'clear', exitCode: EXIT_CLEAR, refusalKind: null }; + } + // The early-warning run never reddens: a governed PR awaiting the maintainer's + // own merge is this regime's healthy terminal state, and a check that is red + // on the healthy case is a permanently red check (see the header). + if (event !== EVENT_MERGE_GROUP) { + return { ...base, conclusion: 'warned', exitCode: EXIT_CLEAR, refusalKind: null }; + } + if (unattributed.length > 0) { + return { ...base, conclusion: 'refused', exitCode: EXIT_REFUSED_UNATTRIBUTED, refusalKind: 'unattributed' }; + } + if (entries.some((e) => e.approval.state === 'unreadable')) { + return { ...base, conclusion: 'refused', exitCode: EXIT_REFUSED_UNREADABLE, refusalKind: 'unreadable' }; + } + if (entries.some((e) => e.approval.state !== 'approved')) { + return { ...base, conclusion: 'refused', exitCode: EXIT_REFUSED_UNAPPROVED, refusalKind: 'unapproved' }; + } + return { ...base, conclusion: 'cleared', exitCode: EXIT_CLEAR, refusalKind: null }; +} + +/** + * The words a reader gets. Every rendering names the exact paths that matched + * and states what would satisfy the guard — a refusal a reader cannot act on is + * a refusal they route around. + */ +export function renderGuardVerdict(verdict) { + const lines = []; + const surfaceLines = (entry) => + entry.surfaces.flatMap((s) => [ + ` ${s.glob} x${s.files.length} — ${s.what}`, + ...s.files.slice(0, 12).map((f) => ` - ${f}`), + ...(s.files.length > 12 ? [` … and ${s.files.length - 12} more`] : []), + ]); + + const apiLabel = verdict.event === EVENT_MERGE_GROUP ? 'API read(s) (PR head + reviews)' : 'review lookup(s)'; + lines.push( + `${CHECK_CONTEXT_NAME} — ${verdict.event} — ${verdict.entries.length} governed pull request(s), ` + + `${verdict.unattributed.length} unattributed governed commit(s), ${verdict.apiCalls} ${apiLabel}.`, + ); + + if (verdict.conclusion === 'clear') { + lines.push( + ' ✅ CLEAR — the diff touches no governed surface, so this guard has nothing to judge.', + ` Derived from GOVERNED_SURFACES in scripts/check-governed-queue-guard.mjs (${GOVERNED_SURFACES.length} surfaces),`, + ' never from a restated list. ⛔ ZERO review lookups were made: the path test runs first and returns,', + ' so a GitHub API outage can never block a diff that touches nothing governed.', + ); + return lines.join('\n'); + } + + // A pinned verdict (the queue leg) carries `headSha`; the early-warning leg's + // `approvalVerdict` shape does not. + const pinned = (approval) => approval.headSha !== undefined; + for (const entry of verdict.entries) { + lines.push('', ` #${entry.pr} — governed:`); + lines.push(...surfaceLines(entry)); + if (entry.approval.state === 'approved') { + lines.push( + pinned(entry.approval) + ? ` ✅ authorized APPROVED review pinned to head ${entry.approval.headSha.slice(0, 12)}, by: ${entry.approval.approvers.join(', ')}` + : ` ✅ APPROVED review present, by: ${entry.approval.approvers.join(', ')}`, + ); + } else if (entry.approval.state === 'unreadable') { + lines.push(` ⛔ the review list could NOT be read — ${entry.approval.reason}`); + } else if (pinned(entry.approval)) { + lines.push( + ` ⛔ NO authorized APPROVED review pinned to head ${String(entry.approval.headSha).slice(0, 12)} ` + + `(${entry.approval.reviewsRead} review(s) read; authorized: ${GOVERNED_APPROVERS.join(', ')})`, + ); + for (const stale of entry.approval.staleApprovers ?? []) { + lines.push( + ` ⚠️ ${stale.login} approved at ${(stale.commitId || '(no commit_id)').slice(0, 12)} but the head is ` + + `${String(entry.approval.headSha).slice(0, 12)} — STALE, never counts: a push after the approval reopens this gate`, + ); + } + if ((entry.approval.unauthorizedApprovers ?? []).length > 0) { + lines.push( + ` ℹ️ APPROVED by account(s) outside GOVERNED_APPROVERS: ${entry.approval.unauthorizedApprovers.join(', ')} — never counts`, + ); + } + } else { + lines.push(` ⛔ NO approving review (${entry.approval.reviewsRead} review(s) read, none decisive-APPROVED)`); + } + if (entry.approval.changesRequestedBy.length > 0) { + lines.push( + ` ⚠️ outstanding CHANGES_REQUESTED from: ${entry.approval.changesRequestedBy.join(', ')}`, + ' (informational — the predicate is an authorized APPROVED review pinned to the', + " pull request's current head; this guard does not widen past its own rule)", + ); + } + } + for (const row of verdict.unattributed) { + lines.push( + '', + ` ⛔ UNATTRIBUTED — commit ${String(row.sha ?? '(unknown)').slice(0, 12)} touches a governed surface and names no pull request:`, + ` subject: ${row.subject || '(empty)'}`, + ...row.paths.slice(0, 12).map((p) => ` - ${p}`), + ); + } + + lines.push(''); + if (verdict.conclusion === 'warned') { + lines.push( + ' ⚠️ EARLY WARNING, not a failure — this run is on the pull request, and this check is deliberately', + ' GREEN here. A governed PR held as a draft for the maintainer to merge by hand IS this regime\'s', + ' healthy end state, and a check that reddens on the healthy case is a permanently red check.', + '', + ' ⛔ What a seat must NOT do with this pull request: flip it ready, enqueue it, or arm auto-merge.', + ' One governed path governs the WHOLE pull request — proportion is not a question.', + ' ⚠️ A GitHub MCP `update_pull_request` call can set `draft: false` as a SIDE EFFECT of passing', + ' only `reviewers` (objectstack-ai/objectstack#12200). That is how objectui#6183 left draft,', + ' and converting back to a draft did NOT dequeue it. Do not send that call on this PR.', + '', + ' If it IS enqueued anyway, the merge-queue run of this same check REFUSES it unless every', + ' governed pull request above carries an authorized approval pinned to its head by then.', + ); + return lines.join('\n'); + } + if (verdict.conclusion === 'cleared') { + lines.push( + ' ✅ CLEARED — every governed pull request in this merge group carries an APPROVED review by an', + ` authorized approver (GOVERNED_APPROVERS: ${GOVERNED_APPROVERS.join(', ')}) whose commit_id equals that`, + " pull request's CURRENT head sha (a stale, dismissed, superseded or unauthorized approval never", + ' counts). ⛔ An agent seat never submits an approving review on a governed-surface pull request,', + ' under any account — every seat here writes under a shared identity, so that rule is what this', + ' technical control rests on.', + ); + return lines.join('\n'); + } + + lines.push(' ⛔ REFUSED — this merge group must not land.'); + if (verdict.refusalKind === 'unattributed') { + lines.push( + ' A governed-surface change is in this merge group that cannot be attributed to any pull request,', + ' so there is no review record it could possibly satisfy. Fail closed: a governed change nobody', + ' can point at a reviewable pull request for is the most anomalous input this guard can receive.', + ); + } else if (verdict.refusalKind === 'unreadable') { + lines.push( + ' The review list could not be READ for at least one governed pull request above. ⛔ This is a', + ' refusal and not a pass, deliberately: this guard exists because every other layer in this chain', + ' failed open. "Nobody approved" and "we could not find out" are different facts (exit 3 vs 4) and', + ' neither of them is "approved". Re-run the job once the API is reachable.', + ); + } else { + lines.push( + ' At least one governed pull request above carries NO authorized APPROVED review pinned to its', + ' current head, and the merge queue would have been the entire review — the shape of objectui#6183,', + ' which left draft through a hidden tool side effect and landed as 5b3290fd5 unreviewed.', + ); + } + lines.push( + '', + ' What satisfies this check:', + ' 1. ⭐ PREFERRED — take the pull request out of the queue: convert it back to DRAFT (disarming', + ' auto-merge alone does NOT dequeue it), and leave the merge to the maintainer. A human merge', + ' IS the review record for a governed surface; that is the regime, not a workaround of it.', + ` 2. Or: obtain an APPROVED review by an authorized approver (GOVERNED_APPROVERS: ${GOVERNED_APPROVERS.join(', ')})`, + " pinned to each governed pull request's CURRENT head sha, then re-queue (any push after the", + ' approval goes stale and reopens this refusal). ⛔ An agent seat never submits that approval,', + ' under any account.', + ' Neither of those is "edit this check".', + '', + ' Verify any file list before acting: node scripts/check-governed-queue-guard.mjs --test ', + ); + return lines.join('\n'); +} + +/** + * The orchestrator, with its one IO dependency injected. + * + * ⭐ The early return below is the ordering guarantee expressed as control flow: + * nothing governed ⇒ verdict, before `fetchReviews` exists as a possibility. The + * self-test passes a `fetchReviews` that THROWS, so "a clear diff costs zero API + * calls" is measured rather than asserted. + */ +export async function runGuard({ event, rows, fetchReviews, fetchPullHead }) { + const { governed, unattributed } = decomposeGovernedWork(rows); + if (governed.length === 0 && unattributed.length === 0) { + return guardVerdict({ event, governed, unattributed, apiCalls: 0 }); + } + const approvals = new Map(); + let apiCalls = 0; + for (const entry of governed) { + try { + if (event === EVENT_MERGE_GROUP) { + // The queue leg judges the pinned predicate, so it needs the pull + // request's CURRENT head sha — the merge_group payload carries no + // per-PR heads. Two reads, head first: an unreadable head refuses + // without ever constructing the review request. + apiCalls += 1; + const headSha = await fetchPullHead(entry.pr); + apiCalls += 1; + approvals.set(entry.pr, pinnedApprovalVerdict(await fetchReviews(entry.pr), headSha)); + } else { + // The early-warning leg never reddens, so it never needs the head. + apiCalls += 1; + approvals.set(entry.pr, approvalVerdict(await fetchReviews(entry.pr))); + } + } catch (error) { + approvals.set(entry.pr, unreadableApproval(String(error?.message ?? error).split('\n')[0])); + } + } + return guardVerdict({ event, governed, unattributed, approvals, apiCalls }); +} + +// -- git (diff decomposition; zero API) ------------------------------------- + +function git(root, args) { + return execFileSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }); +} + +/** Is `rev` an object this checkout actually has? A missing sha is a hard failure, never an empty diff. */ +function hasRev(root, rev) { + try { + git(root, ['cat-file', '-e', `${rev}^{commit}`]); + return true; + } catch { + return false; + } +} + +/** + * The first-parent commits between `baseSha` and `headSha`, each with the paths + * it changed and the pull request its subject names. One row per pull-request + * landing in a merge group; see the header on why the group is decomposed + * rather than keyed to `head_ref`. + */ +export function enumerateRows(root, baseSha, headSha, fallbackPull = null) { + const log = git(root, ['log', '--first-parent', '--format=%H%x09%s', `${baseSha}..${headSha}`]); + const commits = log + .split('\n') + .filter((l) => l !== '') + .map((l) => { + const [sha, ...rest] = l.split('\t'); + return { sha, subject: rest.join('\t') }; + }); + return commits.map((commit) => ({ + ...commit, + // The fallback is only unambiguous when the range holds exactly one commit; + // in a multi-PR group `head_ref` names the LAST pull request, and applying + // it to an earlier commit attributes a governed diff to the wrong one. + pr: pullNumberFromSubject(commit.subject) ?? (commits.length === 1 ? fallbackPull : null), + paths: git(root, ['diff-tree', '-r', '--no-commit-id', '--no-renames', '--name-only', '-m', '--first-parent', commit.sha]) + .split('\n') + .filter((p) => p !== ''), + })); +} + +// -- the GitHub reads (PR head + reviews — the only API surface) ------------- + +function apiHeaders(token) { + return { + accept: 'application/vnd.github+json', + 'x-github-api-version': '2022-11-28', + ...(token ? { authorization: `Bearer ${token}` } : {}), + }; +} + +/** + * Every review on a pull request, paginated. Throws on any non-2xx — the caller + * turns a throw into a REFUSAL, never into a pass, so there is no tolerant + * branch to get wrong here. + */ +export function makeReviewReader({ apiUrl, slug, token, fetchImpl = fetch, perPage = 100, maxPages = 10 }) { + return async function fetchReviews(pull) { + const all = []; + for (let page = 1; page <= maxPages; page += 1) { + const url = `${apiUrl}/repos/${slug}/pulls/${pull}/reviews?per_page=${perPage}&page=${page}`; + const res = await fetchImpl(url, { headers: apiHeaders(token) }); + if (!res.ok) throw new Error(`GET /repos/${slug}/pulls/${pull}/reviews answered HTTP ${res.status}`); + const batch = await res.json(); + if (!Array.isArray(batch)) throw new Error(`the reviews endpoint answered a non-array body for #${pull}`); + all.push(...batch); + if (batch.length < perPage) return all; + } + throw new Error(`#${pull} has more than ${perPage * maxPages} reviews — refusing to judge a truncated list`); + }; +} + +/** + * The pull request's CURRENT head sha — what the predicate pins a review's + * `commit_id` against. Same channel as the review read, under the workflow's + * `pull-requests: read` scope and nothing wider. Throws on any non-2xx and on a + * body with no parseable `head.sha`: a head this guard cannot read pins NOTHING, + * and the caller turns the throw into a REFUSAL (exit 4), never a pass. + */ +export function makePullHeadReader({ apiUrl, slug, token, fetchImpl = fetch }) { + return async function fetchPullHead(pull) { + const res = await fetchImpl(`${apiUrl}/repos/${slug}/pulls/${pull}`, { headers: apiHeaders(token) }); + if (!res.ok) throw new Error(`GET /repos/${slug}/pulls/${pull} answered HTTP ${res.status}`); + const body = await res.json(); + const sha = String(body?.head?.sha ?? ''); + if (!/^[0-9a-f]{7,40}$/i.test(sha)) { + throw new Error(`GET /repos/${slug}/pulls/${pull} answered no parseable head.sha — cannot pin approvals`); + } + return sha; + }; +} + +// -- the seat-side `--test` predicate --------------------------------------- + +/** The words `--test` prints. Pure, so the self-test reads them without a process. */ +export function renderTestVerdict(verdict) { + const lines = []; + if (!verdict.governed) { + lines.push( + `✅ NOT GOVERNED — ${verdict.checked} path(s) checked against ${verdict.surfacesChecked} governed surface(s); none matched.`, + ' An ordinary pull request: the normal review and merge-queue route applies.', + ); + return lines.join('\n'); + } + lines.push(`⛔ GOVERNED — ${verdict.hitPaths.length} of ${verdict.checked} path(s) are on a governed surface:`); + for (const surface of verdict.matched) { + lines.push(` ${surface.glob} x${surface.files.length} — ${surface.what}`); + for (const file of surface.files.slice(0, 12)) lines.push(` - ${file}`); + if (surface.files.length > 12) lines.push(` … and ${surface.files.length - 12} more`); + } + lines.push( + '', + ' One governed path governs the WHOLE pull request — proportion is not a question.', + ' ⛔ Do not flip it ready, enqueue it, or arm auto-merge. Park it as a DRAFT and leave the merge', + ' to the maintainer; a human merge IS the review record for a governed surface.', + ` The merge-queue run of "${CHECK_CONTEXT_NAME}" refuses this diff unless an authorized approval`, + ` (GOVERNED_APPROVERS: ${GOVERNED_APPROVERS.join(', ')}) is pinned to the pull request's current head.`, + ); + return lines.join('\n'); +} + +// -- CLI -------------------------------------------------------------------- + +function runTestMode(argv) { + const paths = argv.slice(argv.indexOf('--test') + 1).filter((a) => !a.startsWith('--')); + if (paths.length === 0) { + console.error( + 'check-governed-queue-guard --test needs at least one path.\n' + + ' node scripts/check-governed-queue-guard.mjs --test AGENTS.md packages/core/src/index.ts\n' + + 'Refusing to answer "not governed" about an empty list: a question nobody asked must not read as a clearance.', + ); + return EXIT_BAD_USAGE; + } + const verdict = testVerdict(paths); + console.log(renderTestVerdict(verdict)); + return verdict.governed ? EXIT_REFUSED_UNAPPROVED : EXIT_CLEAR; +} + +async function main() { + const env = process.env; + const eventName = env.GITHUB_EVENT_NAME; + let payload; + try { + payload = JSON.parse(readFileSync(env.GITHUB_EVENT_PATH ?? '', 'utf8')); + } catch (error) { + console.error( + `⛔ ${CHECK_CONTEXT_NAME}: could not read GITHUB_EVENT_PATH (${String(error?.message ?? error).split('\n')[0]}).\n` + + ' This guard reads the workflow event payload and nothing else; without it there is no diff to judge,\n' + + ' and "could not look" must never exit 0 here.', + ); + return EXIT_CANNOT_RUN; + } + + const context = resolveEventContext({ eventName, payload }); + if (!context.ok) { + console.error(`⛔ ${CHECK_CONTEXT_NAME}: ${context.reason}.`); + return EXIT_CANNOT_RUN; + } + + for (const rev of [context.baseSha, context.headSha]) { + if (!hasRev(repoRoot, rev)) { + console.error( + `⛔ ${CHECK_CONTEXT_NAME}: ${rev} is not in this checkout, so the diff cannot be read.\n` + + ' The job must check out with `fetch-depth: 0`; a truncated history answers a governed-surface\n' + + ' question with silence, and silence reads as compliance.', + ); + return EXIT_CANNOT_RUN; + } + } + + let rows; + try { + const mergeBase = git(repoRoot, ['merge-base', context.baseSha, context.headSha]).trim(); + rows = enumerateRows(repoRoot, mergeBase, context.headSha, context.namedPull); + } catch (error) { + console.error(`⛔ ${CHECK_CONTEXT_NAME}: could not read the diff (${String(error?.message ?? error).split('\n')[0]}).`); + return EXIT_CANNOT_RUN; + } + + const slug = env.GITHUB_REPOSITORY ?? 'objectstack-ai/objectui'; + const reader = { + apiUrl: (env.GITHUB_API_URL ?? 'https://api.github.com').replace(/\/+$/, ''), + slug, + token: env.GITHUB_TOKEN || env.GH_TOKEN || null, + }; + + const verdict = await runGuard({ + event: context.event, + rows, + fetchReviews: makeReviewReader(reader), + fetchPullHead: makePullHeadReader(reader), + }); + const report = [`${context.label} — ${rows.length} commit(s) in range`, renderGuardVerdict(verdict)].join('\n'); + console.log(report); + + // The step summary is where a reader actually looks at a red queue build. + if (env.GITHUB_STEP_SUMMARY) { + try { + appendFileSync(env.GITHUB_STEP_SUMMARY, `## ${CHECK_CONTEXT_NAME}\n\n\`\`\`text\n${report}\n\`\`\`\n`); + } catch { + /* a summary that cannot be written changes no verdict */ + } + } + return verdict.exitCode; +} + +if (isEntrypoint(import.meta.url) && !process.argv.includes('--self-test')) { + process.exitCode = process.argv.includes('--test') ? runTestMode(process.argv) : await main(); +} + +// -- self-test (offline: pure functions + replay fixtures; no network, no git) -- + +/** + * The measured incident this guard descends from, with its real surface. + * Predicted direction, and the whole point of pinning it: it is REFUSED on + * `merge_group` with no approving review, and it is a GREEN early warning on + * `pull_request`. A fixture that passed the queue leg would mean this guard + * would not have stopped the thing it was built to stop. + */ +const REPLAYS = [ + { + name: 'objectui#6183 — a governed AGENTS.md PR left draft through an update_pull_request side effect, queued, and merged as 5b3290fd5 with zero reviews', + pr: 6183, + subject: 'docs(agents): seat protocol updates (#6183)', + files: ['AGENTS.md'], + }, + { + name: 'the same shape on the instruction tree — a .claude/** change mixed with ordinary source', + pr: 6184, + subject: 'chore(hooks): tighten a PreToolUse guard (#6184)', + files: ['.claude/hooks/guard-shared-stash.sh', 'packages/core/src/index.ts'], + }, + { + name: 'the published skills catalog, the surface the domain label routes on', + pr: 6185, + subject: 'docs(skill): rewrite the composition rules (#6185)', + files: ['skills/objectui/rules/composition.md', 'skills/objectui/SKILL.md'], + }, + { + name: 'an architecture decision record', + pr: 6186, + subject: 'docs(adr): record the governed-surface decision (#6186)', + files: ['docs/adr/0099-example.md'], + }, +]; + +export async function selfTest() { + let checked = 0; + const failures = []; + const assert = (name, cond, detail) => { + checked += 1; + if (!cond) failures.push(`${name}${detail ? `: ${detail}` : ''}`); + }; + const row = (pr, files, sha = 'a'.repeat(40), subject = `x (#${pr})`) => ({ sha, subject, pr, paths: files }); + const approved = (...logins) => logins.map((login) => ({ state: 'APPROVED', user: { login } })); + const HEAD = 'f'.repeat(40); + const OLD = '0'.repeat(40); + const approvedAt = (login, sha) => ({ state: 'APPROVED', user: { login }, commit_id: sha }); + const pinnedPass = (login = GOVERNED_APPROVERS[0]) => pinnedApprovalVerdict([approvedAt(login, HEAD)], HEAD); + const run = (event, rows, approvals = new Map()) => { + const { governed, unattributed } = decomposeGovernedWork(rows); + return guardVerdict({ event, governed, unattributed, approvals, apiCalls: governed.length }); + }; + + // -- the governed surface IS the ruled definition, in both directions ------ + // + // The card restates the 2026-08-18 definition as five surfaces. Pinning the + // membership BOTH ways is what stops a sixth being added here instead of by a + // maintainer, and a fifth being dropped by an edit nobody reads. + assert('the-register-declares-exactly-the-five-ruled-surfaces', GOVERNED_SURFACES.length === 5, String(GOVERNED_SURFACES.length)); + assert( + 'and-they-are-the-ruled-globs-verbatim', + GOVERNED_SURFACES.map((s) => s.glob).join() === 'docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md', + GOVERNED_SURFACES.map((s) => s.glob).join(), + ); + for (const surface of GOVERNED_SURFACES) { + const sample = surface.prefix ? `${surface.prefix}sample.md` : surface.exact; + const { governed } = decomposeGovernedWork([row(1, [sample])]); + assert(`the-register-drives-the-verdict-for-${surface.id}`, governed.length === 1 && governed[0].paths.includes(sample), sample); + } + // The two exact rows are EXACT: a vendored or example copy is ordinary source. + assert('an-example-copy-of-AGENTS.md-is-not-the-governed-one', governedPathsIn(['examples/AGENTS.md']).length === 0); + assert('a-near-miss-adr-directory-is-not-governed', governedPathsIn(['docs/adrs/z.md', 'docs/adr-notes.md']).length === 0); + // ⛔ The widening this guard deliberately does NOT take: it does not govern + // its own workflow, and pinning that keeps the scope honest against a future + // edit that quietly promotes CI config to a governed surface. + assert('this-guards-own-workflow-is-NOT-governed', governedPathsIn([`.github/workflows/${CHECK_WORKFLOW}`, 'scripts/check-governed-queue-guard.mjs']).length === 0); + assert('ordinary-source-is-not-governed', governedPathsIn(['packages/core/src/index.ts', 'content/docs/guide/x.md', 'package.json']).length === 0); + + // -- the exit contract as a table ------------------------------------------ + assert('exit-clear-is-0', EXIT_CLEAR === 0); + assert('exit-cannot-run-is-1', EXIT_CANNOT_RUN === 1); + assert('exit-bad-usage-is-2', EXIT_BAD_USAGE === 2); + assert( + 'the-three-refusals-are-distinct-non-zero-codes', + new Set([EXIT_REFUSED_UNAPPROVED, EXIT_REFUSED_UNREADABLE, EXIT_REFUSED_UNATTRIBUTED]).size === 3 && + ![EXIT_REFUSED_UNAPPROVED, EXIT_REFUSED_UNREADABLE, EXIT_REFUSED_UNATTRIBUTED].includes(0), + ); + + // -- the merge-queue head ref ---------------------------------------------- + assert('queue-ref-yields-its-pr', pullNumberFromQueueRef('refs/heads/gh-readonly-queue/main/pr-6183-484ae0019cd') === 6183); + assert('queue-ref-without-the-refs-prefix-too', pullNumberFromQueueRef('gh-readonly-queue/main/pr-42-abcdef1') === 42); + assert('a-base-branch-with-a-slash-is-still-read', pullNumberFromQueueRef('refs/heads/gh-readonly-queue/release/v5/pr-7-abcdef1') === 7); + assert('an-ordinary-branch-that-merely-looks-like-one-is-NOT-a-queue-ref', pullNumberFromQueueRef('refs/heads/pr-12-abcdef1') === null); + assert('a-plain-branch-is-null', pullNumberFromQueueRef('refs/heads/claude/issue-1-x') === null); + assert('nonsense-is-null-never-a-number', pullNumberFromQueueRef(undefined) === null && pullNumberFromQueueRef('') === null); + + // -- the subject parser (this repo squashes, so the trailing form fires) ---- + assert('a-squash-subject-yields-its-pr', pullNumberFromSubject('fix(grid): read the policy first (#6722)') === 6722); + assert('a-merge-commit-subject-does-too', pullNumberFromSubject('Merge pull request #6722 from claude/x') === 6722); + assert( + 'a-subject-citing-an-issue-mid-title-keeps-only-the-trailing-parenthetical', + pullNumberFromSubject('fix(plugin-detail): re-key three fetch effects (#6697) (#6725)') === 6725, + ); + assert('a-subject-naming-no-pr-is-null', pullNumberFromSubject('chore: direct work') === null && pullNumberFromSubject(undefined) === null); + + // -- event payloads, including the malformed ones --------------------------- + const mg = resolveEventContext({ + eventName: 'merge_group', + payload: { merge_group: { base_sha: 'b'.repeat(40), head_sha: 'h'.repeat(40), head_ref: 'refs/heads/gh-readonly-queue/main/pr-99-abcdef1', base_ref: 'refs/heads/main' } }, + }); + assert('a-merge_group-payload-resolves-to-its-shas-and-named-pr', mg.ok && mg.event === 'merge_group' && mg.namedPull === 99, JSON.stringify(mg)); + const pr = resolveEventContext({ + eventName: 'pull_request', + payload: { pull_request: { number: 123, draft: true, base: { sha: 'b'.repeat(40) }, head: { sha: 'h'.repeat(40) } } }, + }); + assert('a-pull_request-payload-resolves-to-its-number-and-draft-state', pr.ok && pr.namedPull === 123 && pr.draft === true, JSON.stringify(pr)); + assert('a-merge_group-with-no-shas-CANNOT-RUN-never-reads-as-an-empty-diff', resolveEventContext({ eventName: 'merge_group', payload: { merge_group: {} } }).ok === false); + assert('a-pull_request-with-no-head-sha-CANNOT-RUN', resolveEventContext({ eventName: 'pull_request', payload: { pull_request: { number: 1, base: { sha: 'x' } } } }).ok === false); + const unsupported = resolveEventContext({ eventName: 'push', payload: {} }); + assert('an-unsupported-event-CANNOT-RUN-and-names-both-events-it-does-read', !unsupported.ok && /merge_group/.test(unsupported.reason) && /pull_request/.test(unsupported.reason), unsupported.reason); + + // -- the any-approver predicate (the early-warning leg's) ------------------- + assert('an-approval-is-an-approval', approvalVerdict(approved('hotlong')).state === 'approved'); + assert('no-reviews-at-all-is-unapproved', approvalVerdict([]).state === 'unapproved'); + assert('a-COMMENTED-review-is-not-an-approval', approvalVerdict([{ state: 'COMMENTED', user: { login: 'a' } }]).state === 'unapproved'); + // ⭐ The fail-open direction a naive `.some(r => r.state === 'APPROVED')` gets + // wrong, and the only direction this file may not be wrong in. + assert( + 'an-approval-later-superseded-by-CHANGES_REQUESTED-is-NOT-an-approval', + approvalVerdict([ + { state: 'APPROVED', user: { login: 'a' } }, + { state: 'CHANGES_REQUESTED', user: { login: 'a' } }, + ]).state === 'unapproved', + ); + assert( + 'a-CHANGES_REQUESTED-later-superseded-by-an-approval-IS-an-approval', + approvalVerdict([ + { state: 'CHANGES_REQUESTED', user: { login: 'a' } }, + { state: 'APPROVED', user: { login: 'a' } }, + ]).state === 'approved', + ); + assert('a-DISMISSED-approval-is-not-an-approval', approvalVerdict([{ state: 'DISMISSED', user: { login: 'a' } }]).state === 'unapproved'); + assert( + 'one-reviewers-changes-request-does-not-erase-anothers-approval-but-IS-reported', + (() => { + const v = approvalVerdict([...approved('a'), { state: 'CHANGES_REQUESTED', user: { login: 'b' } }]); + return v.state === 'approved' && v.changesRequestedBy.join() === 'b'; + })(), + ); + assert('the-state-comparison-is-case-insensitive-the-API-has-shipped-both', approvalVerdict([{ state: 'approved', user: { login: 'a' } }]).state === 'approved'); + + // -- the pinned predicate (the queue leg's) -------------------------------- + assert('the-authorized-set-is-the-two-carried-accounts', GOVERNED_APPROVERS.join() === 'os-zhuang,hotlong'); + for (const login of GOVERNED_APPROVERS) { + assert(`an-authorized-approval-pinned-to-the-current-head-passes: ${login}`, pinnedApprovalVerdict([approvedAt(login, HEAD)], HEAD).state === 'approved'); + } + const stale = pinnedApprovalVerdict([approvedAt(GOVERNED_APPROVERS[0], OLD)], HEAD); + assert('a-STALE-authorized-approval-never-counts', stale.state === 'unapproved' && stale.staleApprovers[0]?.login === GOVERNED_APPROVERS[0]); + assert('an-approval-with-no-commit_id-is-stale-never-pinned', pinnedApprovalVerdict(approved(GOVERNED_APPROVERS[0]), HEAD).state === 'unapproved'); + const outsider = pinnedApprovalVerdict([approvedAt('not-authorized', HEAD)], HEAD); + assert('an-unauthorized-approval-never-counts-even-pinned-to-head', outsider.state === 'unapproved' && outsider.unauthorizedApprovers.join() === 'not-authorized'); + assert( + 'an-authorized-approval-later-superseded-by-CHANGES_REQUESTED-never-counts', + pinnedApprovalVerdict([approvedAt(GOVERNED_APPROVERS[0], HEAD), { state: 'CHANGES_REQUESTED', user: { login: GOVERNED_APPROVERS[0] }, commit_id: HEAD }], HEAD).state === 'unapproved', + ); + assert( + 'a-DISMISSED-authorized-approval-never-counts', + pinnedApprovalVerdict([approvedAt(GOVERNED_APPROVERS[1], HEAD), { state: 'DISMISSED', user: { login: GOVERNED_APPROVERS[1] }, commit_id: HEAD }], HEAD).state === 'unapproved', + ); + assert('no-reviews-at-all-is-unapproved-under-the-pinned-predicate-too', pinnedApprovalVerdict([], HEAD).state === 'unapproved'); + assert( + 'an-unauthorized-approval-does-not-mask-an-authorized-pinned-one', + pinnedApprovalVerdict([approvedAt('not-authorized', HEAD), approvedAt(GOVERNED_APPROVERS[1], HEAD)], HEAD).approvers.join() === GOVERNED_APPROVERS[1], + ); + assert('the-sha-comparison-is-case-insensitive', pinnedApprovalVerdict([approvedAt(GOVERNED_APPROVERS[0], HEAD.toUpperCase())], HEAD).state === 'approved'); + assert( + 'an-unparsable-head-sha-pins-NOTHING-fail-closed', + pinnedApprovalVerdict([approvedAt(GOVERNED_APPROVERS[0], '')], '').state === 'unapproved' && + pinnedApprovalVerdict([approvedAt(GOVERNED_APPROVERS[0], HEAD)], undefined).state === 'unapproved', + ); + + // -- decomposition, and the multi-PR group trap ---------------------------- + const clearRows = [row(1, ['packages/core/src/index.ts', 'content/docs/guide/x.md'])]; + assert('a-clear-diff-decomposes-to-nothing', decomposeGovernedWork(clearRows).governed.length === 0 && decomposeGovernedWork(clearRows).unattributed.length === 0); + const mixed = decomposeGovernedWork([row(5, ['AGENTS.md', 'packages/core/src/index.ts'])]); + assert('a-mixed-diff-governs-the-whole-pr-and-lists-only-the-governed-paths', mixed.governed[0].paths.join() === 'AGENTS.md', JSON.stringify(mixed.governed[0].paths)); + const batched = decomposeGovernedWork([row(11, ['docs/adr/0099-x.md'], 'a'.repeat(40)), row(12, ['packages/core/src/x.ts'], 'b'.repeat(40))]); + assert('a-batched-group-attributes-the-governed-diff-to-ITS-OWN-pr-not-the-last-one', batched.governed.length === 1 && batched.governed[0].pr === 11, JSON.stringify(batched.governed.map((g) => g.pr))); + const twoGoverned = decomposeGovernedWork([row(11, ['AGENTS.md']), row(12, ['skills/objectui/SKILL.md'], 'b'.repeat(40))]); + assert('two-governed-prs-in-one-group-are-both-carried', twoGoverned.governed.map((g) => g.pr).join() === '11,12'); + const unattributed = decomposeGovernedWork([{ sha: 'c'.repeat(40), subject: 'chore: direct work', pr: null, paths: ['CLAUDE.md'] }]); + assert('a-governed-commit-naming-no-pr-is-UNATTRIBUTED-never-dropped', unattributed.unattributed.length === 1 && unattributed.governed.length === 0); + assert('an-UNGOVERNED-commit-naming-no-pr-is-simply-not-our-business', decomposeGovernedWork([{ sha: 'd'.repeat(40), subject: 'x', pr: null, paths: ['README.md'] }]).unattributed.length === 0); + + // -- the verdict table, both events ---------------------------------------- + const clearV = run('merge_group', clearRows); + assert('a-clear-merge-group-is-CLEAR-and-exits-0', clearV.conclusion === 'clear' && clearV.exitCode === EXIT_CLEAR); + assert('and-it-made-zero-review-lookups', clearV.apiCalls === 0); + const refusedV = run('merge_group', [row(6183, ['AGENTS.md'])], new Map([[6183, pinnedApprovalVerdict([], HEAD)]])); + assert('an-unapproved-governed-merge-group-is-REFUSED-with-code-3', refusedV.conclusion === 'refused' && refusedV.exitCode === EXIT_REFUSED_UNAPPROVED); + const clearedV = run('merge_group', [row(6183, ['AGENTS.md'])], new Map([[6183, pinnedPass()]])); + assert('an-authorized-pinned-approval-CLEARS-the-merge-group-and-exits-0', clearedV.conclusion === 'cleared' && clearedV.exitCode === EXIT_CLEAR); + const staleV = run('merge_group', [row(6183, ['AGENTS.md'])], new Map([[6183, pinnedApprovalVerdict([approvedAt(GOVERNED_APPROVERS[0], OLD)], HEAD)]])); + assert('a-stale-sha-approval-REFUSES-the-merge-group-with-code-3', staleV.conclusion === 'refused' && staleV.exitCode === EXIT_REFUSED_UNAPPROVED); + const outsiderV = run('merge_group', [row(6183, ['AGENTS.md'])], new Map([[6183, pinnedApprovalVerdict([approvedAt('not-authorized', HEAD)], HEAD)]])); + assert('an-unauthorized-account-approval-REFUSES-the-merge-group-with-code-3', outsiderV.conclusion === 'refused' && outsiderV.exitCode === EXIT_REFUSED_UNAPPROVED); + const unreadableV = run('merge_group', [row(6183, ['AGENTS.md'])], new Map([[6183, unreadableApproval('HTTP 502')]])); + assert('an-unreadable-review-list-is-a-REFUSAL-not-a-pass', unreadableV.conclusion === 'refused' && unreadableV.exitCode === EXIT_REFUSED_UNREADABLE); + const missingV = run('merge_group', [row(6183, ['AGENTS.md'])]); + assert('a-governed-pr-with-NO-recorded-reading-refuses-too-there-is-no-default-pass', missingV.exitCode === EXIT_REFUSED_UNREADABLE); + const unattrV = run('merge_group', [{ sha: 'c'.repeat(40), subject: 'chore: x', pr: null, paths: ['CLAUDE.md'] }]); + assert('an-unattributed-governed-commit-is-REFUSED-with-its-own-code', unattrV.conclusion === 'refused' && unattrV.exitCode === EXIT_REFUSED_UNATTRIBUTED); + const partial = run( + 'merge_group', + [row(11, ['AGENTS.md']), row(12, ['skills/objectui/SKILL.md'], 'b'.repeat(40))], + new Map([[11, pinnedPass()], [12, pinnedApprovalVerdict([], HEAD)]]), + ); + assert('one-approved-pr-does-NOT-carry-an-unapproved-sibling-through-the-same-group', partial.exitCode === EXIT_REFUSED_UNAPPROVED); + + // -- the pull_request leg is an EARLY WARNING and never reddens ------------- + const warnedV = run('pull_request', [row(6183, ['AGENTS.md'])], new Map([[6183, approvalVerdict([])]])); + assert('a-governed-unapproved-PULL-REQUEST-is-WARNED-not-refused', warnedV.conclusion === 'warned' && warnedV.exitCode === EXIT_CLEAR); + assert( + 'the-pr-leg-never-reddens-under-ANY-approval-state-that-is-the-permanently-red-poison', + ['unapproved', 'unreadable', 'approved'].every( + (state) => run('pull_request', [row(1, ['AGENTS.md'])], new Map([[1, { state, approvers: [], changesRequestedBy: [], reviewsRead: 0 }]])).exitCode === EXIT_CLEAR, + ), + ); + assert('and-an-unattributed-governed-commit-does-not-redden-a-pr-run-either', run('pull_request', [{ sha: 'c'.repeat(40), subject: 'x', pr: null, paths: ['CLAUDE.md'] }]).exitCode === EXIT_CLEAR); + + // -- the replay fixtures --------------------------------------------------- + for (const replay of REPLAYS) { + const rows = [row(replay.pr, replay.files, 'e'.repeat(40), replay.subject)]; + const queued = run('merge_group', rows, new Map([[replay.pr, pinnedApprovalVerdict([], HEAD)]])); + assert(`replay-REFUSES-at-the-queue: ${replay.name}`, queued.exitCode === EXIT_REFUSED_UNAPPROVED, JSON.stringify(queued.conclusion)); + const early = run('pull_request', rows, new Map([[replay.pr, approvalVerdict([])]])); + assert(`replay-only-WARNS-on-the-pr: ${replay.name}`, early.conclusion === 'warned' && early.exitCode === EXIT_CLEAR); + const text = renderGuardVerdict(queued); + assert(`replay-names-its-governed-paths: ${replay.name}`, replay.files.filter((f) => governedPathsIn([f]).length > 0).every((f) => text.includes(f)), text); + // The subject the fixture carries is the one `enumerateRows` would parse, + // so the attribution leg is exercised on the same string a queue build sees. + assert(`replay-subject-attributes-to-its-own-pr: ${replay.name}`, pullNumberFromSubject(replay.subject) === replay.pr, replay.subject); + } + + // -- ⭐ the ordering guarantee, measured with a spy that THROWS ------------- + // + // "The path test runs first and a clear diff makes no API call" is a claim + // about control flow, so it is tested by making the API impossible to touch. + // A mock returning [] would have passed against a version that called it. + let apiTouched = 0; + const explode = () => { + apiTouched += 1; + throw new Error('the API must not be reached for a diff that touches nothing governed'); + }; + let orderedClear = null; + let orderedThrow = null; + try { + orderedClear = await runGuard({ event: 'merge_group', rows: clearRows, fetchReviews: explode, fetchPullHead: explode }); + } catch (error) { + orderedThrow = String(error?.message ?? error); + } + assert( + 'a-clear-diff-NEVER-constructs-a-head-or-review-request', + apiTouched === 0 && orderedThrow === null && orderedClear?.exitCode === EXIT_CLEAR && orderedClear?.conclusion === 'clear', + `apiTouched=${apiTouched} threw=${orderedThrow ?? 'no'}`, + ); + // …and the other half: a governed diff DOES reach both reads — head first — + // so the case above proves an ordering, not a dead code path. + const trace = []; + const traced = await runGuard({ + event: 'merge_group', + rows: [row(1, ['AGENTS.md'])], + fetchPullHead: () => { + trace.push('head'); + return HEAD; + }, + fetchReviews: () => { + trace.push('reviews'); + return [approvedAt(GOVERNED_APPROVERS[0], HEAD)]; + }, + }); + assert('a-governed-queue-diff-reads-head-THEN-reviews', trace.join() === 'head,reviews', trace.join()); + assert( + 'the-pinned-predicate-is-wired-end-to-end-an-authorized-pinned-approval-CLEARS', + traced.conclusion === 'cleared' && traced.exitCode === EXIT_CLEAR && traced.apiCalls === 2, + JSON.stringify({ conclusion: traced.conclusion, apiCalls: traced.apiCalls }), + ); + const tracedStale = await runGuard({ + event: 'merge_group', + rows: [row(1, ['AGENTS.md'])], + fetchPullHead: () => HEAD, + fetchReviews: () => [approvedAt(GOVERNED_APPROVERS[0], OLD)], + }); + assert('the-pinned-predicate-is-wired-end-to-end-a-stale-approval-REFUSES', tracedStale.exitCode === EXIT_REFUSED_UNAPPROVED); + // The early-warning leg makes NO head read: it never reddens, so the stricter + // question would be traffic bought for nothing. + let prHeadReads = 0; + const prLeg = await runGuard({ + event: 'pull_request', + rows: [row(1, ['AGENTS.md'])], + fetchPullHead: () => { + prHeadReads += 1; + return HEAD; + }, + fetchReviews: () => approved('anyone'), + }); + assert( + 'the-pr-leg-makes-NO-head-read-and-keeps-the-any-approver-reading', + prHeadReads === 0 && prLeg.conclusion === 'warned' && prLeg.apiCalls === 1 && + renderGuardVerdict(prLeg).includes('✅ APPROVED review present, by: anyone') && + renderGuardVerdict(prLeg).includes('1 review lookup(s).'), + renderGuardVerdict(prLeg), + ); + // A throwing reader on a GOVERNED diff becomes a refusal, never a pass — and + // `runGuard` must CONTAIN the throw rather than propagate it, so this is + // caught too: an escaping error would abort every case after it, and an + // aborted self-test hides the failures it already collected. + let thrown = null; + let thrownEscaped = null; + try { + thrown = await runGuard({ + event: 'merge_group', + rows: [row(1, ['AGENTS.md'])], + fetchPullHead: () => HEAD, + fetchReviews: () => { + throw new Error('HTTP 403'); + }, + }); + } catch (error) { + thrownEscaped = String(error?.message ?? error); + } + assert( + 'a-throwing-review-read-on-a-governed-diff-REFUSES-and-the-throw-never-escapes', + thrownEscaped === null && thrown?.exitCode === EXIT_REFUSED_UNREADABLE && /403/.test(renderGuardVerdict(thrown)), + thrownEscaped ? `escaped: ${thrownEscaped}` : renderGuardVerdict(thrown), + ); + // An unreadable PR HEAD is its own refusal, and the review request is never + // even constructed after it — fail closed, in order. + let reviewsAfterHeadFailure = 0; + const headFailed = await runGuard({ + event: 'merge_group', + rows: [row(1, ['AGENTS.md'])], + fetchPullHead: () => { + throw new Error('HTTP 500'); + }, + fetchReviews: () => { + reviewsAfterHeadFailure += 1; + return []; + }, + }); + assert( + 'an-unreadable-pr-head-REFUSES-with-exit-4-and-never-reads-reviews', + headFailed.exitCode === EXIT_REFUSED_UNREADABLE && reviewsAfterHeadFailure === 0 && /500/.test(renderGuardVerdict(headFailed)), + `reviewsAfterHeadFailure=${reviewsAfterHeadFailure}`, + ); + + // -- the words a reader acts on -------------------------------------------- + const refusalText = renderGuardVerdict(refusedV); + assert('a-refusal-names-the-exact-paths-that-matched', refusalText.includes('AGENTS.md'), refusalText); + assert('a-refusal-names-the-pull-request', refusalText.includes('#6183'), refusalText); + assert('a-refusal-states-what-would-satisfy-it', /What satisfies this check/.test(refusalText) && /DRAFT/.test(refusalText) && /APPROVED review/.test(refusalText), refusalText); + assert('a-refusal-names-the-preferred-remedy-first-and-it-is-DEQUEUE-not-approve', refusalText.indexOf('DRAFT') < refusalText.indexOf('obtain an APPROVED review'), refusalText); + assert('a-refusal-forecloses-the-edit-the-check-remedy', /Neither of those is "edit this check"/.test(refusalText), refusalText); + assert('a-refusal-carries-the-runnable-derivation-command', refusalText.includes('check-governed-queue-guard.mjs --test'), refusalText); + assert('a-refusal-names-the-incident-it-descends-from', /6183/.test(refusalText) && /5b3290fd5/.test(refusalText), refusalText); + const clearText = renderGuardVerdict(clearV); + assert('a-clear-run-says-it-cost-zero-lookups', /ZERO review lookups/.test(clearText), clearText); + assert('a-clear-run-points-at-the-register-rather-than-listing-surfaces', clearText.includes('GOVERNED_SURFACES') && !clearText.includes('docs/adr/**'), clearText); + const warnText = renderGuardVerdict(warnedV); + assert('the-warning-says-out-loud-that-it-is-deliberately-green', /EARLY WARNING/.test(warnText) && /GREEN here/.test(warnText), warnText); + assert('the-warning-tells-a-seat-what-not-to-do', /flip it ready, enqueue it, or arm auto-merge/.test(warnText), warnText); + // ⭐ The warning names the exact mechanism of the incident, because the seat + // reading it is the seat about to make the same call. + assert('the-warning-names-the-hidden-draft-false-side-effect', /draft: false/.test(warnText) && /update_pull_request/.test(warnText), warnText); + assert('the-warning-forecasts-the-queue-refusal', /REFUSES it/.test(warnText), warnText); + assert('an-outstanding-changes-request-is-reported-even-though-it-does-not-flip-the-verdict', /CHANGES_REQUESTED from: b/.test(renderGuardVerdict(run('merge_group', [row(1, ['AGENTS.md'])], new Map([[1, approvalVerdict([...approved('a'), { state: 'CHANGES_REQUESTED', user: { login: 'b' } }])]]))))); + const kinds = [refusedV, unreadableV, unattrV].map((v) => renderGuardVerdict(v)); + assert('the-three-refusal-kinds-render-three-different-explanations', new Set(kinds).size === 3); + assert('the-unreadable-refusal-says-it-is-deliberately-not-a-pass', /refusal and not a pass/.test(kinds[1]), kinds[1]); + assert( + 'the-refusal-remedy-names-every-authorized-approver-from-the-constant', + GOVERNED_APPROVERS.every((login) => refusalText.includes(login)) && refusalText.includes('GOVERNED_APPROVERS'), + refusalText, + ); + assert('the-refusal-remedy-states-the-agent-no-approve-prohibition', /An agent seat never submits that approval/.test(refusalText), refusalText); + const staleText = renderGuardVerdict(staleV); + assert( + 'a-stale-refusal-names-both-shas-so-a-reader-can-see-the-push-that-unpinned-it', + staleText.includes(OLD.slice(0, 12)) && staleText.includes(HEAD.slice(0, 12)) && /STALE, never counts/.test(staleText), + staleText, + ); + assert( + 'an-unauthorized-refusal-says-the-approval-never-counts', + /APPROVED by account\(s\) outside GOVERNED_APPROVERS: not-authorized — never counts/.test(renderGuardVerdict(outsiderV)), + renderGuardVerdict(outsiderV), + ); + const clearedText = renderGuardVerdict(clearedV); + assert( + 'the-cleared-summary-states-the-pinned-predicate-and-derives-its-accounts-from-the-constant', + /commit_id equals/.test(clearedText) && GOVERNED_APPROVERS.every((login) => clearedText.includes(login)), + clearedText, + ); + assert('a-pinned-pass-renders-the-head-it-is-pinned-to', clearedText.includes(`pinned to head ${HEAD.slice(0, 12)}`), clearedText); + + // -- the seat-side `--test` predicate -------------------------------------- + const governedTest = testVerdict(['AGENTS.md', 'packages/core/src/index.ts']); + assert('--test-reports-a-mixed-diff-as-GOVERNED', governedTest.governed && governedTest.hitPaths.join() === 'AGENTS.md' && governedTest.clearPaths.join() === 'packages/core/src/index.ts'); + const clearTest = testVerdict(['packages/core/src/index.ts', 'package.json']); + assert('--test-reports-an-ordinary-diff-as-NOT-GOVERNED', !clearTest.governed && clearTest.checked === 2); + const governedTestText = renderTestVerdict(governedTest); + assert('--test-names-the-surface-and-the-file', /GOVERNED/.test(governedTestText) && governedTestText.includes('AGENTS.md') && governedTestText.includes('the repo-root agent instruction file'), governedTestText); + assert('--test-prescribes-the-draft-remedy', /DRAFT/.test(governedTestText) && /human merge IS the review record/.test(governedTestText), governedTestText); + assert('--test-names-the-check-a-queue-build-would-run', governedTestText.includes(CHECK_CONTEXT_NAME), governedTestText); + assert('--test-on-a-clear-list-says-the-normal-route-applies', /NOT GOVERNED/.test(renderTestVerdict(clearTest)), renderTestVerdict(clearTest)); + // Empty input is BAD USAGE, not a clearance: `--test` with no paths would + // otherwise print "not governed" about a question nobody asked. + assert('--test-with-no-paths-is-BAD-USAGE-not-a-clearance', runTestMode(['node', 'x', '--test']) === EXIT_BAD_USAGE); + + // -- the WIRING pins: the workflow and the required-context register -------- + // + // Without these, renaming the job detaches the context this repository has + // written down as blocking, and the name declared here becomes a name nothing + // publishes. Read from disk on purpose: a constant asserting against itself + // proves nothing. + try { + const wf = readFileSync(join(repoRoot, '.github', 'workflows', CHECK_WORKFLOW), 'utf8'); + assert('the-workflow-exists-and-declares-the-job-id-this-file-names', wf.includes(`\n ${CHECK_JOB_ID}:\n`), CHECK_JOB_ID); + assert('the-workflow-publishes-EXACTLY-the-context-name-this-file-declares', wf.includes(`name: ${CHECK_CONTEXT_NAME}\n`), CHECK_CONTEXT_NAME); + assert('the-workflow-triggers-on-merge_group-the-leg-that-actually-refuses', /^ {2}merge_group:\s*$/m.test(wf), 'merge_group trigger absent'); + assert('the-workflow-triggers-on-pull_request-the-early-warning-leg', /^ {2}pull_request:\s*$/m.test(wf), 'pull_request trigger absent'); + // ⭐ `ready_for_review` is the addition that matters: flipping a governed + // draft to ready is the first move of the exact sequence #6183 took, and it + // is NOT in GitHub's default `types:` set. Naming `types:` REPLACES the + // default set rather than extending it, so all four are restated. + assert('the-workflow-re-fires-on-ready_for_review-the-first-move-of-the-incident', /types:\s*\[opened, synchronize, reopened, ready_for_review\]/.test(wf), 'ready_for_review absent from types'); + assert('the-workflow-invokes-THIS-script', wf.includes('scripts/check-governed-queue-guard.mjs'), 'invocation absent'); + assert('the-workflow-runs-the-self-test-before-the-live-judgment', wf.indexOf('--self-test') < wf.indexOf('GITHUB_TOKEN'), 'the self-test does not precede the live step'); + assert('the-workflow-checks-out-full-history-a-truncated-diff-answers-with-silence', /fetch-depth:\s*0/.test(wf), 'fetch-depth: 0 absent'); + assert('the-workflow-declares-pull-requests-read-the-only-scope-the-review-read-needs', /pull-requests:\s*read/.test(wf), 'pull-requests: read absent'); + // ⛔ A skipped job counts as SUCCESS in branch protection, so a path filter + // would hand the queue a green verdict for a diff the filter mis-scoped — + // on the one check whose entire job is to refuse. The path test belongs + // INSIDE the script, where "nothing governed" costs zero API calls. + assert('the-workflow-carries-no-paths-filter-a-skipped-guard-counts-as-SUCCESS', !/^\s*paths(-ignore)?:/m.test(wf), 'a paths filter would make this guard skippable'); + } catch (error) { + assert('the-workflow-file-is-readable', false, String(error?.message ?? error).split('\n')[0]); + } + try { + const gate = readFileSync(join(repoRoot, 'scripts', 'dependabot-merge-gate.mjs'), 'utf8'); + assert( + 'this-repository-has-written-the-context-down-as-blocking', + gate.includes(`'${CHECK_CONTEXT_NAME}'`), + `${CHECK_CONTEXT_NAME} is absent from scripts/dependabot-merge-gate.mjs — REQUIRED_CONTEXTS is this repo's own answer to "which checks are blocking", and a guard outside it is a guard the queue floor cannot derive`, + ); + } catch (error) { + assert('the-required-context-register-is-readable', false, String(error?.message ?? error).split('\n')[0]); + } + + for (const f of failures) console.error(` x ${f}`); + if (failures.length > 0) { + console.error(`FAIL check-governed-queue-guard self-test: ${failures.length} of ${checked} case(s) failed.`); + return 1; + } + console.log( + `OK check-governed-queue-guard self-test: ${checked} cases pass ` + + '(the five ruled surfaces pinned in both directions including the widenings NOT taken, the queue/PR ' + + 'event split, latest-decisive approval reduction, the authorized-approval-pinned-to-head predicate on ' + + 'the queue leg — pass, stale, unauthorized, dismissed/superseded, none — with the PR leg head-read-free, ' + + 'multi-PR group decomposition, four replayed governed shapes, the zero-API ordering guarantee measured ' + + 'with throwing spies, the head-then-reviews read order with both unreadable refusals, the seat-side ' + + '--test predicate, and the workflow + required-context wiring pins).', + ); + return 0; +} + +if (isEntrypoint(import.meta.url) && process.argv.includes('--self-test')) { + process.exit(await selfTest()); +} diff --git a/scripts/dependabot-merge-gate.mjs b/scripts/dependabot-merge-gate.mjs index 7aede0f0f0..550816745f 100644 --- a/scripts/dependabot-merge-gate.mjs +++ b/scripts/dependabot-merge-gate.mjs @@ -131,6 +131,19 @@ import { isEntrypoint } from './invoked-as.mjs'; * shell-escape-residue.yml Shell Escape Residue Scan * readme-exports.yml README Export Check * docs-route-eager-closure.yml Docs Route Eager Closure Check + * governed-surface-guard.yml Governed Surface Queue Guard + * + * `Governed Surface Queue Guard` is the newest and the one whose reading here + * differs from every other row, so it is worth a sentence. On a PULL REQUEST it + * is deliberately green whatever it finds — a governed pull request parked as a + * draft for the maintainer is the healthy case, and a check red on the healthy + * case is a permanently red check (objectui#6596). Its refusal lives on the + * `merge_group` leg. Listing it here is still correct and still load-bearing: + * this list is what `merge-queue-reporting.test.ts` derives the `merge_group` + * subscription floor from, and a guard whose whole point is the queue build must + * be inside that floor. It also costs a Dependabot bump nothing — such a diff + * touches no governed path, so the check reports CLEAR without making a single + * API call. * * The four shards are spelled out individually on purpose. A single `Test` * entry, or any pattern match, would be satisfied by whichever shard happened @@ -158,6 +171,7 @@ export const REQUIRED_CONTEXTS = Object.freeze([ 'Shell Escape Residue Scan', 'README Export Check', 'Docs Route Eager Closure Check', + 'Governed Surface Queue Guard', ]); /**