diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index 38a53a8aeee..59974207874 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -181,8 +181,8 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - **ACCEPT 之后的路径分叉**:翻 ready / 挂 auto-merge / 入队前先取一次 PR 的路径面。 - 路径面用 `get_files` 取,⛔ 不看报告自述;动手之前先分,不是事后对照。 -- governed 面统一定义:`docs/adr/**` + `.claude/**`(全量)+ `skills/**` + `docs/NORTH-STAR.md`。 -- governed 面同含 `AGENTS.md` + `CLAUDE.md`;`GOVERNED_REPOS` 各仓同治理待遇,执行席恒随落地仓车道。 +- governed 面统一定义:`docs/adr/**`+`.claude/**`(全量)+`skills/**`+`docs/NORTH-STAR.md`+`.github/workflows/**`。 +- governed 面同含 `AGENTS.md`+`CLAUDE.md`;`GOVERNED_REPOS` 同治理、行可限仓,执行席恒随落地仓车道。 - 路径面命中 Tier H ⇒ ACCEPT 换终局四件套,混合 diff ⛔ 不按比例判;要拆让 dev 单独开 PR。 - 改动 >3000 行(含生成物)同换终局四件套,⛔ 无 Tier S 例外;读数 = PR additions+deletions。 - ① 复核结论照常写在 issue 上;技能面 hunk 须由契约复审档的席复核,档外席先交 skills 席。 diff --git a/AGENTS.md b/AGENTS.md index 2eb940c600a..1f36e48e565 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -263,9 +263,9 @@ localStorage / auth gotchas. 14. **⛔ A governed surface lands only the way its tier allows — Tier H by the maintainer's hand or by an authorized approval, Tier S by the owning seat on a contract-tier review of record; before that record no AI seat merges, queues, or arms auto-merge on a PR whose diff touches one.** The governed surfaces are `docs/adr/**`, - `docs/NORTH-STAR.md`, `.claude/**` (agents, hooks and settings — not only skills), `skills/**`, `AGENTS.md` and - `CLAUDE.md` — the file you are reading is one — and a mixed diff is governed whole on a single path hit. The - register is the `GOVERNED_SURFACES` table in `scripts/pm/check-governed-merges.mjs`, each row carrying its tier; + `docs/NORTH-STAR.md`, `.claude/**` (agents, hooks and settings — not only skills), `skills/**`, `AGENTS.md`, + `CLAUDE.md` — this file is one — and `.github/workflows/**`; a mixed diff is governed whole on one path hit. + The register is the `GOVERNED_SURFACES` table in `scripts/pm/check-governed-merges.mjs`, each row carrying its tier; adding a surface is an edit *there*, never here, and `pnpm check:pm-governed-prose` reds per-PR when this paragraph names fewer surfaces than the register — or more. When it reds, name the surface here. A fork PR (head repo ≠ base repo) is a proposal, never a delivery, whatever its paths: no AI seat readies, queues, arms @@ -276,7 +276,7 @@ localStorage / auth gotchas. auto-merge, ⛔ never flip it out of draft to make any of those possible — judged on the PR's **file list**, not its description; a **mixed diff is not a proportion question**, one path hit is enough; to land the rest, split off the governed files. **The landing is tiered by the register; one Tier H path makes the whole PR Tier H.** **Tier H** - (人合: `docs/adr/**`, `docs/NORTH-STAR.md`, `skills/**`, `AGENTS.md`, `CLAUDE.md`): those four lift only for an + (人合: every register row except `.claude/**`): those four lift only for an authorized APPROVED review by an account in `GOVERNED_APPROVERS`, on ANY commit and not dismissed; that word is spent once per PR — the OWNING seat then lands it, later pushes included; the director seat requests the word as ONE batch of at most five rows, and the click stays the maintainer's. **Tier S** (席内达档复核落地: all of diff --git a/scripts/pm/check-governed-merges.mjs b/scripts/pm/check-governed-merges.mjs index 6d8be7178d5..90f9af157e1 100644 --- a/scripts/pm/check-governed-merges.mjs +++ b/scripts/pm/check-governed-merges.mjs @@ -105,6 +105,32 @@ * 5 of that battery's 7 cases and prescribed a hunt through the generator * for deleted code that was never deleted. * + * ## The register is scoped per row (ruled B2, 2026-10-13) + * + * The maintainer's word 「两仓加治理行」 put `.github/workflows/**` on the + * governed surface of the two public repositories; this register is read for + * five, so a plain row would have governed `objectos` and `hotcrm` too, which + * no ruling names. The ruling (letter B2, 「同意」) gives the register a + * per-row repository scope instead: a row's `repos` key names the ids it + * governs, a row with none governs every repository in `GOVERNED_REPOS`, and + * the `workflows` row lists `objectstack`, `objectui` and `cloud` — the two + * ruled, plus cloud because its own table already carries the row, so the + * central audit and cloud's table agree. + * + * Consequence for every reader: the register's question has a repository in + * it. `governedPathsIn(paths, repo)`, `governedTierFor`, `testVerdict` and + * `classifyCommit` all REQUIRE one and refuse a call without it + * (`assertGovernedRepo`) — ⛔ never a silent "all": a scoped row answers + * differently per repository, so a default is a wrong answer in one of them. + * Where each caller gets its repository: the sweep iterates `GOVERNED_REPOS` + * and has it; `--pr` names one inline and resolves it (`repoIdForSlug`); + * `--test` and `--branch` read the checkout at `--root` (`checkoutRepoFor`) — + * the same "this checkout's own repository" a bare `--pr ` already means — + * and print which one they judged; the queue guard resolves the repository + * its workflow runs in; the half-states patrol binds its swept board at load. + * The verdict carries `repo`, and `surfacesChecked` counts the rows that + * govern it rather than the whole table. + * * ## The argv is closed (#21675) * * Each mode reads a closed set of flags (`ARGV_MODES`) and refuses every other @@ -388,9 +414,13 @@ * them. What moved is the LANDING each row waits for, carried as `tier` on the * row and derived per PR by `governedTierFor`: * - * H 人合 — `docs/adr/**`, `docs/NORTH-STAR.md`, `skills/**`, `AGENTS.md`, - * `CLAUDE.md`: the maintainer's hand, or an authorized APPROVED review by - * `GOVERNED_APPROVERS` and then the owning seat lands it. Unchanged. + * H 人合 — every register row except `.claude/**` (today: `docs/adr/**`, + * `docs/NORTH-STAR.md`, `skills/**`, `AGENTS.md`, `CLAUDE.md`, and since + * the B2 ruling of 2026-10-13 the `workflows` row, `.github/workflows/**`, + * scoped to objectstack, objectui and cloud): the maintainer's hand, or + * an authorized APPROVED review by `GOVERNED_APPROVERS` and then the + * owning seat lands it. The tier ruling left this landing as it was; the + * list itself grew by one row on 2026-10-13. * S 席内达档复核落地 — the whole `.claude/**` tree: the owning seat lands it * through the queue once the PR thread (or its card) carries a * `## Contract review` record for the PR's CURRENT head with @@ -514,16 +544,21 @@ * * The same day, asked whether the rule reaches the sibling repos — 「任何对 * agents.md 等文件的修改…包括 objectui cloud仓库」 — the maintainer answered - * 「同意」. So the surface register above is REPO-AGNOSTIC: the same five - * surface globs are governed in `objectstack`, `objectui`, `cloud`, - * `objectos` and `hotcrm`, and this sweep covers all five repos in one - * invocation regardless of the working directory it is run from. + * 「同意」. So the surface register above reaches every governed repository: + * by default each row governs all five — `objectstack`, `objectui`, `cloud`, + * `objectos` and `hotcrm` — and since the B2 ruling of 2026-10-13 (header + * section "The register is scoped per row") a row may be scoped to a subset: + * the `workflows` row names `objectstack`, `objectui` and `cloud`. This sweep + * still covers all five repos in one invocation, judging each against the + * rows that govern it; `--test` and `--branch` answer for ONE repository, the + * checkout at `--root` (`checkoutRepoFor`), and say which one on stderr. * * ⚠️ The 2026-08-18 quotation above names two sibling repos and the register * below holds five, so read that quote as the repos IN VIEW that day — an * enumeration, never an exclusion, and never the register. The definition it - * states (agent instruction files, judged the same across repos) already - * covered `hotcrm`, a repo agents edit daily; only the CONFIGURATION lagged, + * states (agent instruction files, judged the same across repos — still true + * of every row that declares no `repos` scope) already covered `hotcrm`, a + * repo agents edit daily; only the CONFIGURATION lagged, * and it lagged in the direction that reads as safety. Measured cost of the * lag, #14867: the `repo:hotcrm` seat treated that repo's `AGENTS.md` chain * as governed and paid for hand-merges on it, while this sweep printed no row @@ -994,11 +1029,12 @@ const SELF_TEST_BATTERIES = Object.freeze({ '⭐ #19036: the sweep lists an OVERSIZED landing beside the governed ones': 20, '⭐ the FORK predicate: head repo ≠ base repo is a proposal, never a delivery': 6, '⭐ the argv is CLOSED: a flag the mode does not read, or a second repository beside a bare --pr, is refused': 22, + 'the per-row repository scope (ruled B2, 2026-10-13)': 20, }); // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 32; +const SELF_TEST_BATTERY_FLOOR = 33; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -1123,8 +1159,19 @@ export const HUMAN_MERGE_LINE_THRESHOLD = 3000; * his word as the law above `AGENTS.md`; the PM skill cites it by section). One * path hit governs a whole PR — 「混合 diff 一条命中即整 PR 分叉」; proportion is * never a question, and `tier` decides only what the landing waits for, never - * whether the path is governed. The register is repo-agnostic: it applies in - * all of `GOVERNED_REPOS`. + * whether the path is governed. + * + * The register is scoped PER ROW (maintainer ruling B2, 2026-10-13, header + * section "The register is scoped per row"): by default a row governs every + * repository in `GOVERNED_REPOS`, and a row may be scoped by a `repos` key + * naming the subset it governs — ids from `GOVERNED_REPOS`, never slugs. A row + * that declares no `repos` key carries the default; `surfaceRepos` is the one + * reader of that default. The `workflows` row is scoped to the two public + * repositories the ruling 「两仓加治理行」 names plus `cloud`, whose own table + * already carries the same row: `objectos` and `hotcrm` are NOT governed by + * it — no ruling names them, and a new gate defaults to no. Every reader of + * this table therefore asks for ONE repository (`governedPathsIn(paths, + * repo)`), and a call that names none is refused, never answered for "all". */ export const GOVERNED_SURFACES = Object.freeze([ Object.freeze({ id: 'adr', prefix: 'docs/adr/', glob: 'docs/adr/**', tier: GOVERNED_TIER_H, what: 'architecture decision records' }), @@ -1133,6 +1180,7 @@ export const GOVERNED_SURFACES = Object.freeze([ Object.freeze({ id: 'agents-md', exact: 'AGENTS.md', glob: 'AGENTS.md', tier: GOVERNED_TIER_H, what: 'the repo-root agent instruction file' }), Object.freeze({ id: 'claude-md', exact: 'CLAUDE.md', glob: 'CLAUDE.md', tier: GOVERNED_TIER_H, what: 'the repo-root Claude instruction file' }), Object.freeze({ id: 'north-star', exact: 'docs/NORTH-STAR.md', glob: 'docs/NORTH-STAR.md', tier: GOVERNED_TIER_H, what: "the maintainer's North Star — the law above AGENTS.md" }), + Object.freeze({ id: 'workflows', prefix: '.github/workflows/', glob: '.github/workflows/**', tier: GOVERNED_TIER_H, repos: Object.freeze(['objectstack', 'objectui', 'cloud']), what: 'CI, deploy and release workflows: what runs on every push, what deploys, what gates the queue' }), ]); /** @@ -1340,13 +1388,67 @@ export const GOVERNED_REPOS = Object.freeze([ export const SELF_REPO_ID = 'objectstack'; +/** The `GOVERNED_REPOS` id whose `slug` is `owner/name` (case-insensitive), or null for a repository outside the register. Pure. */ +export function repoIdForSlug(slug) { + const wanted = String(slug ?? '').trim().toLowerCase(); + if (wanted === '') return null; + return GOVERNED_REPOS.find((r) => r.slug.toLowerCase() === wanted)?.id ?? null; +} + +/** + * The repositories ONE register row governs: its `repos` key when it declares + * one, else every id in `GOVERNED_REPOS` — the default the B2 ruling gives a + * row that names no scope. Pure, and the only reader of that default. + */ +export function surfaceRepos(surface) { + return Array.isArray(surface?.repos) ? [...surface.repos] : GOVERNED_REPOS.map((r) => r.id); +} + +/** + * The register's question has a repository in it, or it has no answer. Throws + * on a missing or unknown id — loudly, naming the ids this register knows — + * rather than answering for every repository: a scoped row answers differently + * per repository, so "all" is not a default, it is a wrong answer in one of + * them. `caller` names the function whose contract was missed. + */ +export function assertGovernedRepo(repo, caller = 'governedPathsIn') { + const ids = GOVERNED_REPOS.map((r) => r.id); + if (typeof repo !== 'string' || repo === '') { + throw new TypeError( + `${caller} wants the repository the paths belong to, as a GOVERNED_REPOS id (${ids.join(', ')}); it got ` + + `${JSON.stringify(repo ?? null)}. The register is scoped per row, so a call that names no repository has no ` + + `answer — pass the id of the repository the paths are in (a slug resolves through repoIdForSlug).`, + ); + } + if (!ids.includes(repo)) { + throw new TypeError( + `${caller} got repository ${JSON.stringify(repo)}, which is not a GOVERNED_REPOS id (${ids.join(', ')}). ` + + 'A slug resolves through repoIdForSlug; a repository outside the register is outside the governed regime and ' + + 'is refused, never answered as clear.', + ); + } + return repo; +} + /** - * The governed slice of a path list, grouped by surface. Surfaces with no hit - * are absent — `matched.length === 0` IS the clean path. + * The register's rows that govern `repo`, in report order — the per-repository + * view every matcher reads. Refuses a missing or unknown repository. */ -export function governedPathsIn(paths) { +export function governedSurfacesFor(repo) { + assertGovernedRepo(repo, 'governedSurfacesFor'); + return GOVERNED_SURFACES.filter((surface) => surfaceRepos(surface).includes(repo)); +} + +/** + * The governed slice of a path list in `repo`, grouped by surface. Surfaces + * with no hit are absent — `matched.length === 0` IS the clean path. `repo` + * is REQUIRED: the rows consulted are the ones that govern it, and a call + * without one is refused (`assertGovernedRepo`). + */ +export function governedPathsIn(paths, repo) { + assertGovernedRepo(repo, 'governedPathsIn'); const list = Array.isArray(paths) ? paths : []; - return GOVERNED_SURFACES.map((surface) => ({ + return governedSurfacesFor(repo).map((surface) => ({ ...surface, files: list.filter((p) => typeof p === 'string' && (surface.prefix ? p.startsWith(surface.prefix) : p === surface.exact), @@ -1377,10 +1479,11 @@ export function landingTierOf(matched) { * "which surfaces land on a record" exists anywhere (the queue guard imports * THIS function). Ungoverned riders in the list are not consulted and cannot * demote a Tier H list or promote a Tier S one; an empty or wholly ungoverned - * list answers `H` (see `landingTierOf`). + * list answers `H` (see `landingTierOf`). `repo` is required, as for + * `governedPathsIn`: the rows consulted are the ones that govern it. */ -export function governedTierFor(paths) { - return landingTierOf(governedPathsIn(paths)); +export function governedTierFor(paths, repo) { + return landingTierOf(governedPathsIn(paths, assertGovernedRepo(repo, 'governedTierFor'))); } /** `owner/name` out of any git remote spelling, or null. Pure. */ @@ -1494,9 +1597,14 @@ export function parseSince(arg, now = new Date()) { * invisible to this audit. With no size handed in the size limb is NOT * MEASURED and can fire nothing, so every pre-#19036 caller gets the entry it * always got, now carrying `size.measured: false` rather than a phantom zero. + * + * `repo` is REQUIRED since the register became scoped per row: it is the + * `GOVERNED_REPOS` entry the commit landed in, and the rows consulted are the + * ones that govern that repository. A call without one is refused by the + * predicate, never classified against every row. */ -export function classifyCommit({ sha, date, subject }, changedPaths, repo = null, size = null) { - const verdict = testVerdict(changedPaths, { size }); +export function classifyCommit({ sha, date, subject }, changedPaths, repo, size = null) { + const verdict = testVerdict(changedPaths, { repo: assertGovernedRepo(repo?.id, 'classifyCommit'), size }); if (!landsByHumanMerge(verdict)) return null; return { repoId: repo?.id ?? null, @@ -1514,23 +1622,28 @@ export function classifyCommit({ sha, date, subject }, changedPaths, repo = null // ── the --test predicate (#9550): "would a PR touching these be governed?" ─── /** - * The pre-arm answer, as data. Pure and repo-agnostic — the register is the - * same in all five governed repos, so a seat can run this from anywhere with - * the file list of any PR in any of them. + * The pre-arm answer, as data. Pure, and answered for ONE repository: `repo` + * (a `GOVERNED_REPOS` id) is required, because the register is scoped per row + * and a scoped row answers differently per repository. A seat can still run + * this from anywhere with the file list of any PR in any governed repository — + * it names which one, and the verdict carries it (`repo`, and + * `surfacesChecked` counts the rows that govern it, not the whole table). */ -export function testVerdict(paths, { size = null, fork = null, checks = null, approval = null } = {}) { +export function testVerdict(paths, { repo, size = null, fork = null, checks = null, approval = null } = {}) { + assertGovernedRepo(repo, 'testVerdict'); const list = (Array.isArray(paths) ? paths : []).filter((p) => typeof p === 'string' && p !== ''); - const matched = governedPathsIn(list); + const matched = governedPathsIn(list, repo); const hit = new Set(matched.flatMap((s) => s.files)); const sized = sizeVerdict(size, approval); const forked = fork && typeof fork === 'object' ? fork : forkVerdict(null); // the FORK limb, or NOT MEASURED return { + repo, governed: matched.length > 0, // The landing tier (#19133): `H` / `S` while governed, `null` when nothing // is — a `--json` reader never sees a tier for a diff that has none. tier: matched.length > 0 ? landingTierOf(matched) : null, checked: list.length, - surfacesChecked: GOVERNED_SURFACES.length, + surfacesChecked: governedSurfacesFor(repo).length, matched, hitPaths: [...hit], clearPaths: list.filter((p) => !hit.has(p)), @@ -1656,7 +1769,7 @@ export function renderTestVerdict(verdict) { const head = `governed-surface predicate: ${verdict.hitPaths.length} of ${verdict.checked} path(s) hit the register` + (liftedCount > 0 ? ` after ${liftedCount} generated-artifact lift(s)` : '') + - ` (${verdict.surfacesChecked} surfaces, repo-agnostic).`; + ` (${verdict.surfacesChecked} surfaces govern ${verdict.repo}).`; const size = verdict.size ?? sizeVerdict(null); const intake = renderForkLines(verdict.fork, verdict.checks); if (!verdict.governed) { @@ -1802,8 +1915,9 @@ export const CALLER_DERIVED_NOTE = /** * `--pr`'s argument, as data. `16997` means this checkout's own repo; the - * qualified spelling names any of the governed repos, because the register is - * repo-agnostic and the PM audits five of them. Pure. + * qualified spelling names any of the governed repos, because the PM audits + * five of them from one checkout — and since the register is scoped per row, + * the repository named here is also the one the verdict answers for. Pure. */ export function parsePullTarget(value, selfSlug = null) { const raw = typeof value === 'string' ? value.trim() : ''; @@ -3430,8 +3544,8 @@ function rootFromArgs(args) { * leave two readings of the same register in circulation, which is the shape * the card is about. `--self-test` pins the identity end to end. */ -async function emitVerdict(paths, args, derivation = null, size = null, intake = null) { - let verdict = testVerdict(paths, { size, ...(intake ?? {}) }); +async function emitVerdict(paths, args, derivation = null, size = null, intake = null, repo = undefined) { + let verdict = testVerdict(paths, { repo, size, ...(intake ?? {}) }); // The provenance-aware exceptions (#9866 + #11705): recompute only when a // registered path is actually among the hits, so every other run stays the // zero-git, zero-cost read it always was. The driver applies each row's @@ -3495,6 +3609,42 @@ function refuseSizeFlagsIn(args, mode, reads) { return EXIT_CANNOT_SWEEP; } +/** + * The repository a `--test` / `--branch` run answers for: the one the checkout + * at `--root` belongs to, read off its `origin` remote and resolved to a + * `GOVERNED_REPOS` id — the same reading a bare `--pr ` already makes of + * "this checkout's own repository". The register is scoped per row, so the + * question has a repository in it; this tool takes none from a flag (the + * closed-argv ruling: one spelling, `--pr /#`, no `--repo`) + * and none from the environment, so the checkout is the only honest source. A + * path list from another governed repository is judged from THAT repository's + * checkout (`--root `); an origin that parses to no governed + * repository is a refusal, never a default. Pure given `origin` (the + * `remote.origin.url` read, or null). Returns `{ repo, slug }` or `{ error }`. + */ +export function checkoutRepoFor(origin, mode) { + const slug = slugFromRemote(origin); + const repo = slug ? repoIdForSlug(slug) : null; + if (repo) return { repo, slug }; + const ids = GOVERNED_REPOS.map((r) => r.id).join(', '); + const read = typeof origin === 'string' && origin.trim() !== '' ? JSON.stringify(origin.trim()) : 'no origin remote'; + return { + error: + `${mode} answers for the repository the checkout at --root belongs to, and that checkout's origin (${read}) ` + + `${slug ? `names ${slug}, which is not a governed repository` : 'parses to no owner/name'} — the governed ` + + `repositories are ${ids}. The register is scoped per row, so the question has a repository in it: run from a ` + + 'checkout of the repository the paths are in, or point --root at one.', + }; +} + +/** The stderr line naming the repository a checkout-derived verdict answers for, beside the caller notes. */ +export function checkoutRepoNote({ repo, slug }) { + return ( + `ℹ️ judged for ${repo} (${slug}, the checkout at --root): the register is scoped per row, so a list from another\n` + + ' governed repository is judged from that repository\'s own checkout — `--root `.' + ); +} + async function runTestMode(args) { const i = args.indexOf('--test'); const paths = []; @@ -3521,13 +3671,22 @@ async function runTestMode(args) { ); return EXIT_CANNOT_SWEEP; } + // The repository (ruling B2): the checkout at --root, or a refusal. + const root = rootFromArgs(args); + const origin = tryGit(root, ['config', '--get', 'remote.origin.url']); + const where = checkoutRepoFor(origin.ok ? origin.out : null, '--test'); + if (where.error) { + console.error(`❌ ${where.error}`); + return EXIT_CANNOT_SWEEP; + } // #17003: on STDERR, so this mode's STDOUT stays byte-identical in both // renderings for any consumer that parses it (the `--json` form is a // machine-read surface). A predicate that changes what it hands its callers in // order to warn its humans has broken something in order to say something. console.error(CALLER_DERIVED_NOTE); if (flags.size) console.error(CALLER_SIZED_NOTE); - return await emitVerdict(paths, args, null, flags.size); + console.error(checkoutRepoNote(where)); + return await emitVerdict(paths, args, null, flags.size, null, where.repo); } /** `--branch ` (#17003): the three-dot list from a local checkout, or a refusal. */ @@ -3540,6 +3699,13 @@ async function runBranchMode(args) { const refusedFlags = refuseSizeFlagsIn(args, `--branch ${ref}`, '`git diff --numstat` on the merge-base range'); if (refusedFlags !== null) return refusedFlags; const root = rootFromArgs(args); + // The repository (ruling B2): the checkout the branch is read from, or a refusal. + const origin = tryGit(root, ['config', '--get', 'remote.origin.url']); + const where = checkoutRepoFor(origin.ok ? origin.out : null, `--branch ${ref}`); + if (where.error) { + console.error(`❌ ${where.error}`); + return EXIT_CANNOT_SWEEP; + } const derived = deriveBranchPaths({ ref, run: (argv) => tryGit(root, argv) }); if (!derived.ok) { console.error(renderDerivationRefusal({ mode: `--branch ${ref}`, reason: derived.reason, remedy: derived.remedy })); @@ -3553,7 +3719,8 @@ async function runBranchMode(args) { return EXIT_CANNOT_SWEEP; } if (!args.includes('--json')) console.log(renderDerivation(derived.derivation)); - return await emitVerdict(derived.paths, args, derived.derivation, derived.size); + console.error(checkoutRepoNote(where)); + return await emitVerdict(derived.paths, args, derived.derivation, derived.size, null, where.repo); } /** `--pr ` (#17003): the API's changed-files list — three-dot by construction — or a refusal. */ @@ -3576,6 +3743,18 @@ async function runPullMode(args) { console.error(ambiguous); return EXIT_CANNOT_SWEEP; } + // The repository (ruling B2): the one `--pr` names, resolved to its register + // id. A slug outside GOVERNED_REPOS has no rows scoped to it, so the answer + // is a refusal — never "not governed", which reads as a clearance. + const repo = repoIdForSlug(target.slug); + if (repo === null) { + console.error( + `❌ --pr ${target.slug}#${target.pull} names a repository outside the governed register ` + + `(${GOVERNED_REPOS.map((r) => r.id).join(', ')}). The register is scoped per row and has no row for it, so ` + + 'there is no governed answer for that pull request — this predicate judges the governed repositories only.', + ); + return EXIT_CANNOT_SWEEP; + } const derived = await fetchPullFiles({ ...apiContext(process.env), slug: target.slug, @@ -3596,7 +3775,7 @@ async function runPullMode(args) { return EXIT_CANNOT_SWEEP; } if (!args.includes('--json')) console.log(renderDerivation(derived.derivation)); - return await emitVerdict(derived.paths, args, derived.derivation, derived.size, { fork: derived.fork, checks: derived.checks }); + return await emitVerdict(derived.paths, args, derived.derivation, derived.size, { fork: derived.fork, checks: derived.checks }, repo); } /** @@ -3709,9 +3888,11 @@ const PREDICATE_MODES = ['--test', '--pr', '--branch']; // through to that refusal rather than replacing it. // // ⛔ No mode reads a repository from anywhere but its own spelling: `--pr` -// names one inline (`/#`), the sweep narrows by `--repos`. -// There is no `--repo` here, by ruling — one spelling, the documented one — -// and the environment's half is `bareTargetRefusal`. +// names one inline (`/#`), the sweep narrows by `--repos`, +// and `--test` / `--branch` answer for the checkout at `--root` (its origin — +// `checkoutRepoFor`), which is what the per-row scope of the register makes +// them need. There is no `--repo` here, by ruling — one spelling, the +// documented one — and the environment's half is `bareTargetRefusal`. export const ARGV_MODES = Object.freeze({ sweep: Object.freeze({ name: 'the sweep', @@ -4179,6 +4360,10 @@ async function selfTest() { checked++; if (!cond) failures.push(`${name}: ${detail ?? ''}`); }; + // The repository every register question below is asked for, unless a case + // is ABOUT the scope and names another one: this checkout's own. The scope + // battery pins that the matchers refuse a call that names none. + const REPO = SELF_REPO_ID; // Report fixtures take a REAL window from the real resolver, never a // hand-built object: a shape drifting from `resolveWindow`'s would render // green here and print nothing an operator could read. @@ -4186,8 +4371,8 @@ async function selfTest() { // ── the governed predicate: the 2026-08-18 unified list, exactly ────────── battery('the governed predicate: the 2026-08-18 unified list, exactly'); - const ids = (paths) => governedPathsIn(paths).map((s) => s.id); - assert('all-six-surfaces-declared-in-order', GOVERNED_SURFACES.map((s) => s.id).join(',') === 'adr,claude-tree,skills-catalog,agents-md,claude-md,north-star', GOVERNED_SURFACES.map((s) => s.id).join(',')); + const ids = (paths) => governedPathsIn(paths, REPO).map((s) => s.id); + assert('all-seven-surfaces-declared-in-order', GOVERNED_SURFACES.map((s) => s.id).join(',') === 'adr,claude-tree,skills-catalog,agents-md,claude-md,north-star,workflows', GOVERNED_SURFACES.map((s) => s.id).join(',')); assert('adr-prefix', ids(['docs/adr/0001-x.md']).join() === 'adr'); assert('whole-claude-tree-not-only-skills', ids(['.claude/hooks/guard-main-checkout.sh', '.claude/agents/os-dev.md', '.claude/settings.json']).join() === 'claude-tree'); assert('published-skills-catalog-is-governed', ids(['skills/objectstack-ui/SKILL.md']).join() === 'skills-catalog'); @@ -4199,6 +4384,48 @@ async function selfTest() { assert('near-misses-stay-out', ids(['docs/adrs/z.md', '.claude-x/y.md', 'skillsx/a.md', 'examples/AGENTS.md', 'packages/create-objectstack/src/templates/AGENTS.md', 'apps/CLAUDE.md.bak', 'docs/north-star.md', 'docs/NORTH-STAR.md.bak', 'examples/docs/NORTH-STAR.md', 'docs/NORTH-STAR.mdx', 'content/docs/concepts/north-star.mdx']).length === 0, JSON.stringify(ids(['examples/AGENTS.md']))); assert('a-mixed-diff-groups-by-surface', ids(['docs/adr/0001.md', 'AGENTS.md', 'package.json']).join() === 'adr,agents-md'); + // ── the per-row repository scope (maintainer ruling B2, 2026-10-13) ────── + // + // The workflows row is the first SCOPED row, and the scope is a new axis of + // the register, so every direction is pinned: which rows carry the default + // and which a scope, that a scope is a subset of the roster, that each of + // the three named repositories answers GOVERNED Tier H and each of the two + // unnamed ones answers NOT governed, and that every front door REFUSES a + // call naming no repository rather than answering for all of them. + battery('the per-row repository scope (ruled B2, 2026-10-13)'); + const everyRepo = GOVERNED_REPOS.map((r) => r.id); + const workflowsRow = GOVERNED_SURFACES.find((s) => s.id === 'workflows'); + assert('the-six-pre-existing-rows-declare-no-scope-and-so-govern-every-repository', GOVERNED_SURFACES.filter((s) => s.id !== 'workflows').every((s) => s.repos === undefined && surfaceRepos(s).join() === everyRepo.join()), JSON.stringify(GOVERNED_SURFACES.map((s) => [s.id, s.repos ?? null]))); + assert('the-workflows-row-is-scoped-to-objectstack-objectui-and-cloud-exactly', surfaceRepos(workflowsRow).join() === 'objectstack,objectui,cloud' && workflowsRow.tier === GOVERNED_TIER_H && workflowsRow.glob === '.github/workflows/**', JSON.stringify(workflowsRow)); + assert('every-declared-scope-is-a-non-empty-duplicate-free-subset-of-GOVERNED_REPOS-ids', GOVERNED_SURFACES.every((s) => s.repos === undefined || (Array.isArray(s.repos) && s.repos.length > 0 && new Set(s.repos).size === s.repos.length && s.repos.every((id) => everyRepo.includes(id)))), JSON.stringify(GOVERNED_SURFACES.map((s) => [s.id, s.repos ?? null]))); + assert('the-workflows-row-is-a-prefix-row-with-a-separator-so-it-reaches-dispatch-gates-as-written-and-declares-no-hint', workflowsRow.prefix === '.github/workflows/' && !ROOT_FILE_WATCH_HINTS.some((h) => h.startsWith('.github/'))); + const wf = '.github/workflows/lint.yml'; + assert('an-objectstack-workflow-path-is-governed-Tier-H', ids([wf]).join() === 'workflows' && governedTierFor([wf], 'objectstack') === GOVERNED_TIER_H && testVerdict([wf], { repo: 'objectstack' }).tier === GOVERNED_TIER_H); + assert('an-objectui-workflow-path-is-governed-Tier-H', governedPathsIn([wf], 'objectui').map((s) => s.id).join() === 'workflows' && governedTierFor([wf], 'objectui') === GOVERNED_TIER_H); + assert('a-cloud-workflow-path-is-governed-Tier-H', governedPathsIn([wf], 'cloud').map((s) => s.id).join() === 'workflows' && governedTierFor([wf], 'cloud') === GOVERNED_TIER_H && testVerdict([wf], { repo: 'cloud' }).governed === true); + assert('an-objectos-workflow-path-is-NOT-governed', governedPathsIn([wf], 'objectos').length === 0 && testVerdict([wf], { repo: 'objectos' }).governed === false && testVerdict([wf], { repo: 'objectos' }).clearPaths.join() === wf); + assert('a-hotcrm-workflow-path-is-NOT-governed', governedPathsIn([wf], 'hotcrm').length === 0 && testVerdict([wf], { repo: 'hotcrm' }).governed === false); + assert('the-default-rows-still-govern-objectos-and-hotcrm-the-scope-narrows-one-row-not-the-register', ['objectos', 'hotcrm'].every((r) => governedPathsIn(['AGENTS.md', 'docs/adr/0001-x.md', '.claude/agents/os-dev.md'], r).map((s) => s.id).join() === 'adr,claude-tree,agents-md')); + assert('the-near-misses-for-the-workflows-row-stay-out', ids(['.github/ISSUE_TEMPLATE/bug.md', '.github/workflows.bak/x.yml', '.github/workflowsx/y.yml', 'github/workflows/z.yml', '.github/CODEOWNERS', 'packages/x/.github/workflows/w.yml']).length === 0); + assert('surfacesChecked-counts-the-rows-that-govern-the-repository-not-the-table', testVerdict(['README.md'], { repo: 'objectstack' }).surfacesChecked === 7 && testVerdict(['README.md'], { repo: 'objectos' }).surfacesChecked === 6 && governedSurfacesFor('cloud').length === 7 && governedSurfacesFor('hotcrm').map((s) => s.id).join() === 'adr,claude-tree,skills-catalog,agents-md,claude-md,north-star'); + assert('the-verdict-carries-the-repository-it-answered-for', testVerdict([wf], { repo: 'cloud' }).repo === 'cloud' && JSON.parse(JSON.stringify(testVerdict([wf], { repo: 'objectui' }))).repo === 'objectui'); + const refuses = (fn) => { try { fn(); return null; } catch (error) { return error; } }; + const noRepo = refuses(() => governedPathsIn([wf])); + assert('⛔ no-repository-is-an-ERROR-never-an-answer-for-every-repository', noRepo instanceof TypeError && /governedPathsIn wants the repository/.test(noRepo.message) && /objectstack, objectui, cloud, objectos, hotcrm/.test(noRepo.message), String(noRepo?.message)); + assert('⛔ and-every-front-door-refuses-alike', [() => governedTierFor([wf]), () => testVerdict([wf]), () => testVerdict([wf], {}), () => classifyCommit({ sha: 'a'.repeat(40), date: 'd', subject: 'x (#1)' }, [wf]), () => governedSurfacesFor(undefined), () => governedPathsIn([wf], ''), () => governedPathsIn([wf], null)].every((fn) => refuses(fn) instanceof TypeError)); + const unknownRepo = refuses(() => governedPathsIn([wf], 'objectstack-ai/objectstack')); + assert('⛔ a-slug-or-any-unknown-id-is-refused-and-the-words-point-at-repoIdForSlug', unknownRepo instanceof TypeError && /not a GOVERNED_REPOS id/.test(unknownRepo.message) && /repoIdForSlug/.test(unknownRepo.message) && refuses(() => governedTierFor([wf], 'Objectstack')) instanceof TypeError, String(unknownRepo?.message)); + assert('the-refusal-names-the-caller-whose-contract-was-missed', /^testVerdict wants/.test(refuses(() => testVerdict([wf])).message) && /^classifyCommit wants/.test(refuses(() => classifyCommit({ sha: 'a'.repeat(40), date: 'd', subject: 'x' }, [wf])).message) && /^governedTierFor wants/.test(refuses(() => governedTierFor([wf])).message)); + assert('repoIdForSlug-resolves-the-roster-case-insensitively-and-nothing-else', repoIdForSlug('objectstack-ai/cloud') === 'cloud' && repoIdForSlug('ObjectStack-AI/ObjectUI') === 'objectui' && repoIdForSlug(' objectstack-ai/hotcrm ') === 'hotcrm' && repoIdForSlug('someone/objectstack') === null && repoIdForSlug('objectstack') === null && repoIdForSlug('') === null && repoIdForSlug(null) === null); + assert('every-GOVERNED_REPOS-slug-resolves-to-its-own-id', GOVERNED_REPOS.every((r) => repoIdForSlug(r.slug) === r.id)); + const fromThisCheckout = checkoutRepoFor('https://github.com/objectstack-ai/objectstack.git', '--test'); + assert('--test-and---branch-answer-for-the-checkout-at---root-its-origin-resolved-to-an-id', fromThisCheckout.repo === 'objectstack' && fromThisCheckout.slug === 'objectstack-ai/objectstack' && checkoutRepoFor('git@github.com:objectstack-ai/cloud.git', '--branch x').repo === 'cloud', JSON.stringify(fromThisCheckout)); + const foreign = checkoutRepoFor('https://github.com/someone/fork.git', '--test'); + const noOrigin = checkoutRepoFor(null, '--branch x'); + assert('⛔ a-checkout-outside-the-roster-or-with-no-origin-is-REFUSED-never-defaulted', typeof foreign.error === 'string' && /someone\/fork, which is not a governed repository/.test(foreign.error) && /point --root at one/.test(foreign.error) && typeof noOrigin.error === 'string' && /no origin remote/.test(noOrigin.error) && /parses to no owner\/name/.test(noOrigin.error), `${foreign.error}\n${noOrigin.error}`); + assert('the-checkout-note-names-the-repository-judged-and-the---root-remedy', /judged for cloud \(objectstack-ai\/cloud, the checkout at --root\)/.test(checkoutRepoNote({ repo: 'cloud', slug: 'objectstack-ai/cloud' })) && /--root /.test(checkoutRepoNote({ repo: 'cloud', slug: 'objectstack-ai/cloud' }))); + assert('the-verdict-head-names-the-repository-and-counts-its-rows', renderTestVerdict(testVerdict([wf], { repo: 'cloud' })).split('\n')[0] === 'governed-surface predicate: 1 of 1 path(s) hit the register (7 surfaces govern cloud).' && renderTestVerdict(testVerdict([wf], { repo: 'objectos' })).split('\n')[0] === 'governed-surface predicate: 0 of 1 path(s) hit the register (6 surfaces govern objectos).', renderTestVerdict(testVerdict([wf], { repo: 'objectos' })).split('\n')[0]); + // ── the dispatch-gates declaration (#9979) ─────────────────────────────── // // Enforcement cannot hold any of these: the declaration is read by another @@ -4236,33 +4463,33 @@ async function selfTest() { assert('every-row-carries-a-tier-and-it-is-H-or-S', GOVERNED_SURFACES.every((s) => s.tier === GOVERNED_TIER_H || s.tier === GOVERNED_TIER_S), JSON.stringify(GOVERNED_SURFACES.map((s) => [s.id, s.tier]))); assert('the-tier-values-are-distinct-single-letters-a-reader-greps', GOVERNED_TIER_H === 'H' && GOVERNED_TIER_S === 'S' && Object.keys(GOVERNED_TIERS).join() === 'H,S'); assert('Tier-S-is-exactly-the-whole-claude-tree-row', GOVERNED_SURFACES.filter((s) => s.tier === GOVERNED_TIER_S).map((s) => s.id).join() === 'claude-tree'); - assert('Tier-H-is-the-law-adr-north-star-skills-catalog-agents-md-claude-md', GOVERNED_SURFACES.filter((s) => s.tier === GOVERNED_TIER_H).map((s) => s.id).join() === 'adr,skills-catalog,agents-md,claude-md,north-star'); - assert('settings-and-hooks-are-Tier-S-by-the-amendment', governedTierFor(['.claude/settings.json', '.claude/hooks/guard-main-checkout.sh']) === GOVERNED_TIER_S); - assert('skills-agents-and-the-fact-layer-are-Tier-S', governedTierFor(['.claude/skills/pm-dispatch/SKILL.md', '.claude/agents/os-dev.md', '.claude/skills/pm-dispatch/references/contract-review.md']) === GOVERNED_TIER_S); - assert('each-Tier-H-surface-answers-H-alone', ['docs/adr/0001-x.md', 'docs/NORTH-STAR.md', 'skills/objectstack-ui/SKILL.md', 'AGENTS.md', 'CLAUDE.md'].every((p) => governedTierFor([p]) === GOVERNED_TIER_H)); - assert('ALL-not-ANY-one-Tier-H-path-makes-the-whole-list-Tier-H', governedTierFor(['.claude/agents/os-dev.md', '.claude/settings.json', 'AGENTS.md']) === GOVERNED_TIER_H); - assert('an-ungoverned-rider-neither-demotes-a-Tier-H-list-nor-promotes-a-Tier-S-one', governedTierFor(['README.md', 'AGENTS.md']) === GOVERNED_TIER_H && governedTierFor(['README.md', '.claude/agents/os-dev.md']) === GOVERNED_TIER_S); - assert('an-EMPTY-or-UNGOVERNED-list-answers-H-fail-closed', governedTierFor([]) === GOVERNED_TIER_H && governedTierFor(undefined) === GOVERNED_TIER_H && governedTierFor(['README.md']) === GOVERNED_TIER_H); + assert('Tier-H-is-the-law-adr-north-star-skills-catalog-agents-md-claude-md-and-the-workflows-row', GOVERNED_SURFACES.filter((s) => s.tier === GOVERNED_TIER_H).map((s) => s.id).join() === 'adr,skills-catalog,agents-md,claude-md,north-star,workflows'); + assert('settings-and-hooks-are-Tier-S-by-the-amendment', governedTierFor(['.claude/settings.json', '.claude/hooks/guard-main-checkout.sh'], REPO) === GOVERNED_TIER_S); + assert('skills-agents-and-the-fact-layer-are-Tier-S', governedTierFor(['.claude/skills/pm-dispatch/SKILL.md', '.claude/agents/os-dev.md', '.claude/skills/pm-dispatch/references/contract-review.md'], REPO) === GOVERNED_TIER_S); + assert('each-Tier-H-surface-answers-H-alone', ['docs/adr/0001-x.md', 'docs/NORTH-STAR.md', 'skills/objectstack-ui/SKILL.md', 'AGENTS.md', 'CLAUDE.md'].every((p) => governedTierFor([p], REPO) === GOVERNED_TIER_H)); + assert('ALL-not-ANY-one-Tier-H-path-makes-the-whole-list-Tier-H', governedTierFor(['.claude/agents/os-dev.md', '.claude/settings.json', 'AGENTS.md'], REPO) === GOVERNED_TIER_H); + assert('an-ungoverned-rider-neither-demotes-a-Tier-H-list-nor-promotes-a-Tier-S-one', governedTierFor(['README.md', 'AGENTS.md'], REPO) === GOVERNED_TIER_H && governedTierFor(['README.md', '.claude/agents/os-dev.md'], REPO) === GOVERNED_TIER_S); + assert('an-EMPTY-or-UNGOVERNED-list-answers-H-fail-closed', governedTierFor([], REPO) === GOVERNED_TIER_H && governedTierFor(undefined, REPO) === GOVERNED_TIER_H && governedTierFor(['README.md'], REPO) === GOVERNED_TIER_H); assert('the-published-skills-catalog-stays-Tier-H-the-seat-did-not-propose-moving-it', tierOf('skills-catalog') === GOVERNED_TIER_H); - assert('--test-carries-the-tier-and-null-when-nothing-is-governed', testVerdict(['.claude/agents/os-dev.md']).tier === GOVERNED_TIER_S && testVerdict(['AGENTS.md']).tier === GOVERNED_TIER_H && testVerdict(['README.md']).tier === null); - const tierSCase = testVerdict(['packages/spec/src/index.ts', '.claude/agents/os-dev.md']); - const tierHCase = testVerdict(['packages/spec/src/index.ts', '.claude/agents/os-dev.md', 'AGENTS.md']); + assert('--test-carries-the-tier-and-null-when-nothing-is-governed', testVerdict(['.claude/agents/os-dev.md'], { repo: REPO }).tier === GOVERNED_TIER_S && testVerdict(['AGENTS.md'], { repo: REPO }).tier === GOVERNED_TIER_H && testVerdict(['README.md'], { repo: REPO }).tier === null); + const tierSCase = testVerdict(['packages/spec/src/index.ts', '.claude/agents/os-dev.md'], { repo: REPO }); + const tierHCase = testVerdict(['packages/spec/src/index.ts', '.claude/agents/os-dev.md', 'AGENTS.md'], { repo: REPO }); const tierSText = renderTestVerdict(tierSCase); const tierHText = renderTestVerdict(tierHCase); assert('a-Tier-S-verdict-renders-its-OWN-block-naming-the-record-on-thread-landing', /Tier S/.test(tierSText) && /## Contract review/.test(tierSText) && /CONTRACT_REVIEW_TIER/.test(tierSText) && !/--pair/.test(tierSText), tierSText); assert('and-still-says-GOVERNED-with-the-three-verbs-so-every-grep-reader-keeps-its-answer', /GOVERNED/.test(tierSText) && /arms auto-merge/.test(tierSText) && !/human merge is the review record/.test(tierSText), tierSText); assert('a-Tier-H-verdict-keeps-its-wording-word-for-word-and-names-its-tier', /a human merge is the review record for this PR/.test(tierHText) && /landing tier: H/.test(tierHText) && !/Tier S/.test(tierHText), tierHText); assert('a-mixed-list-renders-H-the-one-Tier-H-path-decides', tierHCase.tier === GOVERNED_TIER_H && tierHCase.hitPaths.join() === '.claude/agents/os-dev.md,AGENTS.md', JSON.stringify(tierHCase.hitPaths)); - assert('exit-3-is-SHARED-by-both-tiers-the-tier-lives-in-the-words-and-the-json-never-the-status', runTestModeExitFor(['.claude/agents/os-dev.md']) === EXIT_TEST_GOVERNED && runTestModeExitFor(['AGENTS.md']) === EXIT_TEST_GOVERNED && runTestModeExitFor(['README.md']) === EXIT_TEST_NOT_GOVERNED); + assert('exit-3-is-SHARED-by-both-tiers-the-tier-lives-in-the-words-and-the-json-never-the-status', runTestModeExitFor(['.claude/agents/os-dev.md'], REPO) === EXIT_TEST_GOVERNED && runTestModeExitFor(['AGENTS.md'], REPO) === EXIT_TEST_GOVERNED && runTestModeExitFor(['README.md'], REPO) === EXIT_TEST_NOT_GOVERNED); // A lift can change the tier: a diff whose only Tier H hit is a certified // regeneration under `skills/**` is, post-lift, a Tier S diff — the tier is // recomputed on the lifted slice, never read off the pre-lift one. const liftPath = 'skills/objectstack-ui/references/_index.md'; const lifted = new Map([[liftPath, { pureRegeneration: true, reason: 'byte-equal to the generator' }]]); - const preLift = testVerdict([liftPath, '.claude/agents/os-dev.md']); + const preLift = testVerdict([liftPath, '.claude/agents/os-dev.md'], { repo: REPO }); const postLift = applyGeneratedExceptions(preLift, lifted); assert('the-tier-is-recomputed-on-the-LIFTED-slice', preLift.tier === GOVERNED_TIER_H && postLift.tier === GOVERNED_TIER_S && postLift.governed === true, JSON.stringify([preLift.tier, postLift.tier])); - const allLifted = applyGeneratedExceptions(testVerdict([liftPath]), lifted); + const allLifted = applyGeneratedExceptions(testVerdict([liftPath], { repo: REPO }), lifted); assert('and-null-once-every-hit-is-lifted', allLifted.governed === false && allLifted.tier === null, JSON.stringify(allLifted.tier)); assert('the-tier-words-name-the-landing-each-waits-for', /GOVERNED_APPROVERS/.test(GOVERNED_TIERS.H.landing) && /Contract review/.test(GOVERNED_TIERS.S.landing) && !/--pair/.test(GOVERNED_TIERS.S.landing)); @@ -4309,7 +4536,7 @@ async function selfTest() { assert('QS-7-regression-pin-the-budgeted-window-LISTS-the-skewed-governed-entry', qs7.commits.some((c) => c.sha === qs7Governed.sha) && qs7.anchorAtEdge === false, JSON.stringify(qs7.commits.map((c) => c.sha.slice(0, 4)))); assert('and-the-entry-classifies-as-governed-once-it-is-in-the-window', - classifyCommit(qs7.commits.find((c) => c.sha === qs7Governed.sha), ['docs/adr/0029-kernel-object-ownership.md']) !== null); + classifyCommit(qs7.commits.find((c) => c.sha === qs7Governed.sha), ['docs/adr/0029-kernel-object-ownership.md'], GOVERNED_REPOS[0]) !== null); // The second mechanism, free of the budget: an entry dated BELOW the boundary // that landed ABOVE something inside it is carried by the topological close, // whatever the skew. This is the inversion class the budget cannot bound. @@ -4449,9 +4676,9 @@ async function selfTest() { // ── classification + replay fixtures ───────────────────────────────────── battery('classification + replay fixtures'); - assert('ungoverned-commit-classifies-null', classifyCommit({ sha: 'a'.repeat(40), date: '2026-08-18T00:00:00Z', subject: 'fix: x (#1)' }, ['packages/spec/src/index.ts']) === null); + assert('ungoverned-commit-classifies-null', classifyCommit({ sha: 'a'.repeat(40), date: '2026-08-18T00:00:00Z', subject: 'fix: x (#1)' }, ['packages/spec/src/index.ts'], GOVERNED_REPOS[0]) === null); for (const replay of REPLAYS) { - const entry = classifyCommit({ sha: 'b'.repeat(40), date: '2026-08-18T00:00:00Z', subject: replay.subject }, replay.files); + const entry = classifyCommit({ sha: 'b'.repeat(40), date: '2026-08-18T00:00:00Z', subject: replay.subject }, replay.files, GOVERNED_REPOS[0]); assert(`replay-lists: ${replay.name}`, entry !== null && entry.pr === replay.pr, JSON.stringify(entry)); } const uiEntry = classifyCommit({ sha: 'f'.repeat(40), date: '2026-08-18T00:00:00Z', subject: 'docs: seat protocol (#5188)' }, ['AGENTS.md', 'skills/x/SKILL.md'], GOVERNED_REPOS[1]); @@ -5147,26 +5374,26 @@ async function selfTest() { // ── the --test pre-arm predicate (#9550) ────────────────────────────────── battery('the --test pre-arm predicate (#9550)'); - const governedCase = testVerdict(['AGENTS.md']); + const governedCase = testVerdict(['AGENTS.md'], { repo: REPO }); assert('--test-on-the-#9527-file-list-answers-GOVERNED', governedCase.governed === true && governedCase.hitPaths.join() === 'AGENTS.md', JSON.stringify(governedCase)); assert('--test-governed-renders-the-no-flip-no-enqueue-no-arm-instruction', renderTestVerdict(governedCase).includes('GOVERNED') && renderTestVerdict(governedCase).includes('arms auto-merge'), renderTestVerdict(governedCase)); - const mixedCase = testVerdict(['packages/spec/src/index.ts', '.claude/agents/os-dev.md', 'README.md']); + const mixedCase = testVerdict(['packages/spec/src/index.ts', '.claude/agents/os-dev.md', 'README.md'], { repo: REPO }); assert('--test-on-a-mixed-diff-answers-GOVERNED-on-one-hit', mixedCase.governed === true && mixedCase.hitPaths.join() === '.claude/agents/os-dev.md', JSON.stringify(mixedCase.hitPaths)); assert('--test-lists-the-paths-that-are-NOT-on-the-register-too', mixedCase.clearPaths.join() === 'packages/spec/src/index.ts,README.md', JSON.stringify(mixedCase.clearPaths)); - const clearCase = testVerdict(['packages/spec/src/index.ts', 'scripts/pm/check-governed-merges.mjs']); + const clearCase = testVerdict(['packages/spec/src/index.ts', 'scripts/pm/check-governed-merges.mjs'], { repo: REPO }); assert('--test-on-an-ordinary-diff-answers-NOT-governed', clearCase.governed === false && clearCase.hitPaths.length === 0, JSON.stringify(clearCase)); - assert('this-very-file-is-not-itself-a-governed-surface', testVerdict(['scripts/pm/check-governed-merges.mjs']).governed === false); + assert('this-very-file-is-not-itself-a-governed-surface', testVerdict(['scripts/pm/check-governed-merges.mjs'], { repo: REPO }).governed === false); assert('--test-not-governed-renders-the-re-run-on-the-final-list-warning', renderTestVerdict(clearCase).includes('NOT governed') && renderTestVerdict(clearCase).includes('recall'), renderTestVerdict(clearCase)); // Near misses on the predicate, the class the incident turns on: the seat // reasoned about a name, not a register. - const nearMiss = testVerdict(['examples/AGENTS.md', 'docs/adrs/x.md', '.claude-x/y', 'skillsx/a.md', 'apps/CLAUDE.md.bak', 'packages/create-objectstack/src/templates/AGENTS.md']); + const nearMiss = testVerdict(['examples/AGENTS.md', 'docs/adrs/x.md', '.claude-x/y', 'skillsx/a.md', 'apps/CLAUDE.md.bak', 'packages/create-objectstack/src/templates/AGENTS.md'], { repo: REPO }); assert('--test-near-misses-answer-NOT-governed', nearMiss.governed === false && nearMiss.clearPaths.length === 6, JSON.stringify(nearMiss.hitPaths)); - assert('--test-with-an-empty-path-list-is-never-a-not-governed-answer', testVerdict([]).checked === 0 && testVerdict([]).governed === false && runTestModeExitFor([]) === EXIT_CANNOT_SWEEP); + assert('--test-with-an-empty-path-list-is-never-a-not-governed-answer', testVerdict([], { repo: REPO }).checked === 0 && testVerdict([], { repo: REPO }).governed === false && runTestModeExitFor([], REPO) === EXIT_CANNOT_SWEEP); // Every governed surface answers 3 through its own glob — an uninvoked // surface is the phantom shape this self-test exists to refuse. for (const surface of GOVERNED_SURFACES) { const sample = surface.prefix ? `${surface.prefix}sample.md` : surface.exact; - assert(`--test-answers-governed-for-${surface.id}`, testVerdict([sample]).governed === true, sample); + assert(`--test-answers-governed-for-${surface.id}`, testVerdict([sample], { repo: REPO }).governed === true, sample); } // ── the generated-artifact exception register (#9866 · #11084 · #11705) ── @@ -5197,7 +5424,7 @@ async function selfTest() { !(e.candidate?.test('.claude/workflows/docs-accuracy-audit.js') ?? false)), JSON.stringify(GENERATED_SURFACE_EXCEPTIONS.map((e) => [e.id, e.path ?? String(e.candidate)]))); assert('the-audit-workflow-is-plainly-governed-again-with-no-provenance-consulted', - testVerdict(['.claude/workflows/docs-accuracy-audit.js']).governed === true && + testVerdict(['.claude/workflows/docs-accuracy-audit.js'], { repo: REPO }).governed === true && generatedExceptionFor('.claude/workflows/docs-accuracy-audit.js') === null); assert('a-row-matches-either-an-exact-path-or-a-narrowing-candidate-never-neither-and-never-both', GENERATED_SURFACE_EXCEPTIONS.every((e) => (typeof e.path === 'string') !== (e.candidate instanceof RegExp)), @@ -5206,7 +5433,7 @@ async function selfTest() { GENERATED_SURFACE_EXCEPTIONS.every((e) => !e.candidate || !e.candidate.global)); assert('the-exception-names-a-path-its-surface-actually-governs', GENERATED_SURFACE_EXCEPTIONS.every((e) => { - const m = governedPathsIn([e.path ?? REGISTER_SAMPLES[e.id]]); + const m = governedPathsIn([e.path ?? REGISTER_SAMPLES[e.id]], REPO); return m.length === 1 && m[0].id === e.surfaceId; }), JSON.stringify(GENERATED_SURFACE_EXCEPTIONS.map((e) => e.path ?? REGISTER_SAMPLES[e.id]))); assert('every-row-names-its-generator-command-and-its-trusted-instrument-tree', @@ -5229,28 +5456,28 @@ async function selfTest() { // what the register DOES with a verdict, not how a verdict is reached. const regPath = REGISTER_SAMPLES['spec-skill-refs']; const verified = new Map([[regPath, { pureRegeneration: true, reason: 'byte-equal (fixture)' }]]); - const liftedVerdict = applyGeneratedExceptions(testVerdict(['content/docs/x.mdx', regPath]), verified); + const liftedVerdict = applyGeneratedExceptions(testVerdict(['content/docs/x.mdx', regPath], { repo: REPO }), verified); assert('a-verified-pure-regen-lifts-the-only-hit-and-the-pr-is-not-governed', liftedVerdict.governed === false && liftedVerdict.hitPaths.length === 0 && liftedVerdict.matched.length === 0, JSON.stringify(liftedVerdict.hitPaths)); assert('a-lifted-path-is-recorded-as-an-exception-never-as-a-clear-path', !liftedVerdict.clearPaths.includes(regPath) && liftedVerdict.exceptions.length === 1 && liftedVerdict.exceptions[0].pureRegeneration === true, JSON.stringify(liftedVerdict.exceptions)); - const rejectedVerdict = applyGeneratedExceptions(testVerdict([regPath]), new Map([[regPath, { pureRegeneration: false, reason: 'differs (fixture)' }]])); + const rejectedVerdict = applyGeneratedExceptions(testVerdict([regPath], { repo: REPO }), new Map([[regPath, { pureRegeneration: false, reason: 'differs (fixture)' }]])); assert('a-failed-provenance-keeps-the-path-governed', rejectedVerdict.governed === true && rejectedVerdict.hitPaths.join() === regPath); - const absentProvenance = applyGeneratedExceptions(testVerdict([regPath]), new Map()); + const absentProvenance = applyGeneratedExceptions(testVerdict([regPath], { repo: REPO }), new Map()); assert('absent-provenance-fails-closed-never-open', absentProvenance.governed === true && /fail closed/.test(absentProvenance.exceptions[0].reason), JSON.stringify(absentProvenance.exceptions)); // The mixed-diff rule is untouched: one hit on any OTHER governed path // still forks the whole PR, provenance verified or not. - const mixedGoverned = applyGeneratedExceptions(testVerdict([regPath, '.claude/skills/x/SKILL.md']), verified); + const mixedGoverned = applyGeneratedExceptions(testVerdict([regPath, '.claude/skills/x/SKILL.md'], { repo: REPO }), verified); assert('one-hit-on-any-OTHER-governed-path-still-forks-the-whole-pr', mixedGoverned.governed === true && mixedGoverned.hitPaths.join() === '.claude/skills/x/SKILL.md', JSON.stringify(mixedGoverned.hitPaths)); assert('and-the-lifted-path-is-still-reported-as-lifted-on-a-mixed-diff', mixedGoverned.exceptions[0].pureRegeneration === true); // NAMED rows, not a class: a file the register does not cover never consults // provenance, even provenance that claims to be verified. const sibling = '.claude/workflows/some-other-workflow.js'; - const siblingVerdict = applyGeneratedExceptions(testVerdict([sibling]), new Map([[sibling, { pureRegeneration: true, reason: 'x' }]])); + const siblingVerdict = applyGeneratedExceptions(testVerdict([sibling], { repo: REPO }), new Map([[sibling, { pureRegeneration: true, reason: 'x' }]])); assert('an-unregistered-file-is-not-excepted-named-rows-not-a-class', siblingVerdict.governed === true && siblingVerdict.exceptions.length === 0, JSON.stringify(siblingVerdict.exceptions)); - assert('a-verdict-that-never-consulted-the-exception-carries-no-exceptions-field', testVerdict(['AGENTS.md']).exceptions === undefined); + assert('a-verdict-that-never-consulted-the-exception-carries-no-exceptions-field', testVerdict(['AGENTS.md'], { repo: REPO }).exceptions === undefined); // ── the generator co-edit fence (#11084), pinned in BOTH directions ────── // @@ -5271,7 +5498,7 @@ async function selfTest() { (coEdit?.reason ?? '').includes(`${fencePrefix}build-skill-references.ts`), String(coEdit?.reason)); // A neutered fence must fail LOUD and READABLE here, not crash the run: the // absent answer is itself the finding, so it is asserted, never dereferenced. - const coEditVerdict = applyGeneratedExceptions(testVerdict(coEditPaths), new Map(coEdit ? [[regPath, coEdit]] : [])); + const coEditVerdict = applyGeneratedExceptions(testVerdict(coEditPaths, { repo: REPO }), new Map(coEdit ? [[regPath, coEdit]] : [])); assert('a-generator-co-edit-keeps-the-artifact-governed-the-exception-does-not-lift', coEditVerdict.governed === true && coEditVerdict.hitPaths.join() === regPath && coEditVerdict.exceptions[0].pureRegeneration === false, JSON.stringify(coEditVerdict.hitPaths)); @@ -5287,12 +5514,12 @@ async function selfTest() { const regenOnlyPaths = [regPath, 'packages/spec/src/ui/responsive.zod.ts']; assert('an-untouched-generator-abstains-so-the-recompute-runs-exactly-as-before', generatorCoEditProvenance(regenOnlyPaths, fenceRow) === null); - const regenOnlyVerdict = applyGeneratedExceptions(testVerdict(regenOnlyPaths), verified); + const regenOnlyVerdict = applyGeneratedExceptions(testVerdict(regenOnlyPaths, { repo: REPO }), verified); assert('a-pure-regen-with-an-untouched-generator-still-lifts-and-the-pr-is-not-governed', regenOnlyVerdict.governed === false && regenOnlyVerdict.hitPaths.length === 0 && regenOnlyVerdict.exceptions[0].pureRegeneration === true, JSON.stringify(regenOnlyVerdict)); assert('a-docs-only-pr-that-never-hits-the-register-is-untouched-by-the-fence', - generatorCoEditProvenance(['content/docs/a.mdx'], fenceRow) === null && testVerdict(['content/docs/a.mdx']).governed === false); + generatorCoEditProvenance(['content/docs/a.mdx'], fenceRow) === null && testVerdict(['content/docs/a.mdx'], { repo: REPO }).governed === false); // A prefix on the real directory, not a substring: a sibling directory whose // name merely starts the same way is not the trusted generator. assert('a-near-miss-sibling-directory-is-not-mistaken-for-the-generator-tree', @@ -5400,13 +5627,13 @@ async function selfTest() { } // Applied to a verdict: the #11685 shape lifts, and the ruled limit holds. - const specPr = applyGeneratedExceptions(testVerdict(['packages/spec/src/ui/responsive.zod.ts', genIndex]), new Map([[genIndex, genuine]])); + const specPr = applyGeneratedExceptions(testVerdict(['packages/spec/src/ui/responsive.zod.ts', genIndex], { repo: REPO }), new Map([[genIndex, genuine]])); assert('the-#11685-shape-a-spec-pr-carrying-its-own-regenerated-index-is-NOT-governed', specPr.governed === false && specPr.hitPaths.length === 0 && specPr.exceptions[0].pureRegeneration === true, JSON.stringify(specPr.hitPaths)); - const withHandAuthored = applyGeneratedExceptions(testVerdict([genIndex, handAuthored]), new Map([[genIndex, genuine]])); + const withHandAuthored = applyGeneratedExceptions(testVerdict([genIndex, handAuthored], { repo: REPO }), new Map([[genIndex, genuine]])); assert('but-one-hand-authored-skills-file-beside-it-still-forks-the-whole-pr', withHandAuthored.governed === true && withHandAuthored.hitPaths.join() === handAuthored, JSON.stringify(withHandAuthored.hitPaths)); - const fabricated = applyGeneratedExceptions(testVerdict([handAuthored]), new Map([[handAuthored, { pureRegeneration: true, reason: 'claims to be verified' }]])); + const fabricated = applyGeneratedExceptions(testVerdict([handAuthored], { repo: REPO }), new Map([[handAuthored, { pureRegeneration: true, reason: 'claims to be verified' }]])); assert('provenance-that-claims-to-be-verified-cannot-lift-an-unregistered-skills-file', fabricated.governed === true && fabricated.exceptions.length === 0, JSON.stringify(fabricated.exceptions)); const grouped = groupHitsByException([genIndex, 'skills/objectstack-data/references/_index.md', 'skills/objectstack-ui/references/react-blocks.md', handAuthored]); @@ -5450,7 +5677,7 @@ async function selfTest() { const rejectedRender = renderTestVerdict(rejectedVerdict); assert('a-rejected-render-stays-GOVERNED-and-says-why-the-exception-did-not-lift', rejectedRender.includes('GOVERNED') && rejectedRender.includes('did NOT lift') && rejectedRender.includes('differs (fixture)'), rejectedRender); - assert('a-verdict-without-exceptions-renders-exactly-as-before', renderExceptionLines(testVerdict(['AGENTS.md'])) === '' && !renderTestVerdict(testVerdict(['AGENTS.md'])).includes('#9866')); + assert('a-verdict-without-exceptions-renders-exactly-as-before', renderExceptionLines(testVerdict(['AGENTS.md'], { repo: REPO })) === '' && !renderTestVerdict(testVerdict(['AGENTS.md'], { repo: REPO })).includes('#9866')); // ── the live battery's prerequisite, and the floor it was misread as ───── // @@ -5663,19 +5890,19 @@ async function selfTest() { // The `--test` head line, both directions. `hitPaths` is the post-lift set, // so the count itself is right; what was missing is the reason it moved. const liftedTest = applyGeneratedExceptions( - testVerdict(pr15284Files), + testVerdict(pr15284Files, { repo: REPO }), new Map([[pr15284ReactBlocks, { pureRegeneration: true, reason: 'byte-equal (fixture)' }]]), ); const liftedHead = renderTestVerdict(liftedTest).split('\n')[0]; assert( '⭐ the-test-head-no-longer-reports-a-post-lift-zero-as-if-nothing-had-hit-the-register', - liftedHead === 'governed-surface predicate: 0 of 4 path(s) hit the register after 1 generated-artifact lift(s) (6 surfaces, repo-agnostic).', + liftedHead === 'governed-surface predicate: 0 of 4 path(s) hit the register after 1 generated-artifact lift(s) (7 surfaces govern objectstack).', liftedHead, ); - const plainHead = renderTestVerdict(testVerdict(['packages/spec/src/ui/view.zod.ts'])).split('\n')[0]; + const plainHead = renderTestVerdict(testVerdict(['packages/spec/src/ui/view.zod.ts'], { repo: REPO })).split('\n')[0]; assert( 'and-a-verdict-with-no-lift-keeps-its-head-line-byte-for-byte', - plainHead === 'governed-surface predicate: 0 of 1 path(s) hit the register (6 surfaces, repo-agnostic).', + plainHead === 'governed-surface predicate: 0 of 1 path(s) hit the register (7 surfaces govern objectstack).', plainHead, ); @@ -5711,8 +5938,8 @@ async function selfTest() { assert('a-rename-yields-the-new-path-AND-the-old-one', renameRead.paths.join() === 'docs/AGENTS.md,AGENTS.md' && renameRead.entries === 1, JSON.stringify(renameRead)); assert('⭐ a-rename-OUT-of-a-governed-path-is-still-a-change-TO-that-path', - testVerdict(renameRead.paths).governed === true && testVerdict(['docs/AGENTS.md']).governed === false, - JSON.stringify(testVerdict(renameRead.paths).hitPaths)); + testVerdict(renameRead.paths, { repo: REPO }).governed === true && testVerdict(['docs/AGENTS.md'], { repo: REPO }).governed === false, + JSON.stringify(testVerdict(renameRead.paths, { repo: REPO }).hitPaths)); assert('and-the-pair-is-reported-so-the-operator-sees-which-hit-came-from-a-rename', renameRead.renames.length === 1 && renameRead.renames[0].from === 'AGENTS.md' && renameRead.renames[0].to === 'docs/AGENTS.md', JSON.stringify(renameRead.renames)); @@ -5730,8 +5957,9 @@ async function selfTest() { assert('and-a-walk-still-pending-pages-at-the-cap-refuses-on-the-cap-itself', /hit its 40-page cap/.test(String(pullTruncationReason({ collected: 4000, changedFiles: 4000, pages: 40, pageCap: 40, morePages: true })))); - // `--pr`'s argument. The qualified spelling exists because the register is - // repo-agnostic and the PM audits five repos from one checkout. + // `--pr`'s argument. The qualified spelling exists because the PM audits + // five repos from one checkout; the repository it names is also the one the + // scoped register answers for. assert('a-bare-number-means-this-checkouts-own-repo', JSON.stringify(parsePullTarget('16997', 'objectstack-ai/objectstack')) === JSON.stringify({ slug: 'objectstack-ai/objectstack', pull: 16997 })); assert('a-qualified-spelling-names-any-governed-repo', @@ -5871,7 +6099,7 @@ async function selfTest() { fetchImpl: async (url) => (/\/files/.test(url) ? respond(renamedOut, null) : respond({ changed_files: 1, additions: 1, deletions: 0 }, null)), }); assert('and-a-renamed-governed-file-reaches-the-predicate-as-BOTH-of-its-paths', - renamedPr.ok === true && testVerdict(renamedPr.paths).governed === true && + renamedPr.ok === true && testVerdict(renamedPr.paths, { repo: REPO }).governed === true && renderDerivation(renamedPr.derivation).includes('AGENTS.md → docs/AGENTS.md'), JSON.stringify(renamedPr.paths)); } @@ -5888,6 +6116,9 @@ async function selfTest() { { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); const repo = join(derFx, 'repo'); g2(derFx, 'init', '-q', repo); + // The fixture is a checkout OF a governed repository: `--branch` / `--test` + // answer for the checkout at --root, read off its origin (ruling B2). + g2(repo, 'remote', 'add', 'origin', 'https://github.com/objectstack-ai/objectstack.git'); const put = (rel, text) => { mkdirSync(dirname(join(repo, rel)), { recursive: true }); writeFileSync(join(repo, rel), text, 'utf8'); @@ -5927,9 +6158,9 @@ async function selfTest() { const twoDot = g2(repo, 'diff', '--name-only', 'origin/main', 'feature').trim().split('\n').filter(Boolean); const threeDot = deriveBranchPaths({ ref: 'feature', run: (argv) => tryGit(repo, argv) }); assert('⭐ the-cards-reproduction-two-dot-really-does-carry-mains-own-governed-path', - twoDot.includes('.claude/hooks/guard.sh') && testVerdict(twoDot).governed === true, JSON.stringify(twoDot)); + twoDot.includes('.claude/hooks/guard.sh') && testVerdict(twoDot, { repo: REPO }).governed === true, JSON.stringify(twoDot)); assert('⭐ and-the-three-dot-derivation-of-the-same-branch-is-NOT-governed', - threeDot.ok === true && threeDot.paths.join() === 'src/a.ts' && testVerdict(threeDot.paths).governed === false, + threeDot.ok === true && threeDot.paths.join() === 'src/a.ts' && testVerdict(threeDot.paths, { repo: REPO }).governed === false, JSON.stringify(threeDot.paths ?? threeDot)); const branchRun = run('--branch', 'feature', '--root', repo); assert('⭐ and-the-CLI-itself-answers-NOT-governed-and-says-how-it-derived-the-list', @@ -5950,7 +6181,7 @@ async function selfTest() { // path, so the same branch reads as ungoverned through the obvious command. const renameDefault = g2(repo, 'diff', '--name-only', `${forkPoint}`, 'rename-out').trim().split('\n').filter(Boolean); assert('⭐ gits-own-rename-detection-hides-the-old-path-so-the-obvious-command-under-reports', - renameDefault.join() === 'docs-AGENTS.md' && testVerdict(renameDefault).governed === false, JSON.stringify(renameDefault)); + renameDefault.join() === 'docs-AGENTS.md' && testVerdict(renameDefault, { repo: REPO }).governed === false, JSON.stringify(renameDefault)); const renameRun = run('--branch', 'rename-out', '--root', repo); assert('⭐ while---branch-reads-both-paths-and-answers-GOVERNED', renameRun.status === EXIT_TEST_GOVERNED && renameRun.out.includes('AGENTS.md'), @@ -5963,7 +6194,7 @@ async function selfTest() { !lonelyRun.out.includes('governed-surface predicate'), `status=${lonelyRun.status} out=${lonelyRun.out.slice(0, 300)} err=${lonelyRun.err.slice(0, 300)}`); assert('⭐ and-the-collapsed-shell-recipe-it-refuses-to-imitate-really-is-a-superset', - collapsed.length > 0 && testVerdict(collapsed).governed === true, JSON.stringify(collapsed)); + collapsed.length > 0 && testVerdict(collapsed, { repo: REPO }).governed === true, JSON.stringify(collapsed)); // Silence is not a verdict, in this mode either. const emptyRun = run('--branch', 'main', '--root', repo); assert('a-branch-that-changes-nothing-is-a-failure-never-a-not-governed-answer', @@ -6002,11 +6233,11 @@ async function selfTest() { sizeVerdict({ additions: -1, deletions: 0 }).measured === false && sizeVerdict({ additions: 1.5, deletions: 0 }).measured === false && sizeVerdict({ additions: '5001', deletions: 0 }).measured === false); // Through the verdict: the exit code is the GOVERNED one, the words say SIZE. - const bigClean = testVerdict(['packages/spec/src/index.ts'], { size: { additions: 6000, deletions: 0, source: 'a fixture' } }); + const bigClean = testVerdict(['packages/spec/src/index.ts'], { repo: REPO, size: { additions: 6000, deletions: 0, source: 'a fixture' } }); assert('⭐ an-oversized-PR-with-no-governed-path-is-NOT-governed-by-path-and-lands-by-HUMAN-MERGE', bigClean.governed === false && bigClean.size.exceeds === true && bigClean.humanMerge === true, JSON.stringify(bigClean)); assert('and-it-exits-on-the-GOVERNED-code-so-every-existing-caller-routes-it-to-the-same-terminal', - runTestModeExitFor(['packages/spec/src/index.ts'], bigClean.size) === EXIT_TEST_GOVERNED); + runTestModeExitFor(['packages/spec/src/index.ts'], REPO, bigClean.size) === EXIT_TEST_GOVERNED); const bigWords = renderTestVerdict(bigClean); assert('the-words-say-HUMAN-MERGE-the-number-the-threshold-and-that-generated-files-are-INCLUDED', bigWords.includes('HUMAN MERGE') && bigWords.includes('6000 changed line(s)') && bigWords.includes(`> ${HUMAN_MERGE_LINE_THRESHOLD}`) && @@ -6014,26 +6245,26 @@ async function selfTest() { assert('and-the-provenance-of-the-number-stays-OFF-the-verdict-it-lives-on-the-derivation-line-or-the-stderr-note', bigClean.size.source === 'a fixture' && !renderSizeLines(bigClean.size).includes('fixture') && CALLER_SIZED_NOTE.includes('--additions'), renderSizeLines(bigClean.size)); assert('and-they-never-say-ordinary-queue-landing-applies', !bigWords.includes('ordinary queue landing applies'), bigWords); - const smallClean = testVerdict(['packages/spec/src/index.ts'], { size: { additions: 10, deletions: 2 } }); + const smallClean = testVerdict(['packages/spec/src/index.ts'], { repo: REPO, size: { additions: 10, deletions: 2 } }); assert('an-ordinary-sized-ordinary-diff-still-answers-NOT-governed-and-says-the-size-is-under', - smallClean.humanMerge === false && runTestModeExitFor(['packages/spec/src/index.ts'], smallClean.size) === EXIT_TEST_NOT_GOVERNED && + smallClean.humanMerge === false && runTestModeExitFor(['packages/spec/src/index.ts'], REPO, smallClean.size) === EXIT_TEST_NOT_GOVERNED && renderTestVerdict(smallClean).includes('under the human-merge threshold') && renderTestVerdict(smallClean).includes('12 changed line(s)'), renderTestVerdict(smallClean)); - const unmeasured = testVerdict(['packages/spec/src/index.ts']); + const unmeasured = testVerdict(['packages/spec/src/index.ts'], { repo: REPO }); assert('⭐ with-no-size-handed-in-the-verdict-says-NOT-MEASURED-on-stdout-and-names-the-modes-that-read-it', unmeasured.size.measured === false && unmeasured.humanMerge === false && renderTestVerdict(unmeasured).includes('NOT MEASURED') && renderTestVerdict(unmeasured).includes('--pr') && renderTestVerdict(unmeasured).includes('--additions'), renderTestVerdict(unmeasured)); assert('and-testVerdict-with-no-size-keeps-its-PATH-answer-byte-for-byte-the-queue-guard-imports-it', unmeasured.governed === false && JSON.stringify(unmeasured.hitPaths) === '[]' && - JSON.stringify(unmeasured.clearPaths) === '["packages/spec/src/index.ts"]' && testVerdict(['AGENTS.md']).governed === true); - const bigGoverned = testVerdict(['AGENTS.md'], { size: { additions: 6000, deletions: 0 } }); + JSON.stringify(unmeasured.clearPaths) === '["packages/spec/src/index.ts"]' && testVerdict(['AGENTS.md'], { repo: REPO }).governed === true); + const bigGoverned = testVerdict(['AGENTS.md'], { repo: REPO, size: { additions: 6000, deletions: 0 } }); assert('a-governed-path-AND-an-oversized-diff-renders-both-limbs-and-stays-on-the-governed-code', bigGoverned.governed === true && bigGoverned.humanMerge === true && renderTestVerdict(bigGoverned).includes('GOVERNED') && renderTestVerdict(bigGoverned).includes('HUMAN MERGE'), renderTestVerdict(bigGoverned)); // ⛔ No exemption for generated files: the register lifts a PATH, never a line count. const regenPath = REGISTER_SAMPLES['spec-skill-refs']; const liftedBig = applyGeneratedExceptions( - testVerdict([regenPath], { size: { additions: 237706, deletions: 0 } }), + testVerdict([regenPath], { repo: REPO, size: { additions: 237706, deletions: 0 } }), new Map([[regenPath, { pureRegeneration: true, reason: 'byte-equal to the generator (fixture)' }]]), ); assert('⭐ a-certified-pure-regeneration-lifts-the-PATH-and-lifts-NOTHING-from-the-size', @@ -6052,7 +6283,7 @@ async function selfTest() { state: approvals.length > 0 ? 'approved' : 'unapproved', approvers: approvals.map((a) => a.login), approvals, approvalsOnEarlierCommits: [], unauthorizedApprovers: [], changesRequestedBy: [], reviewsRead: 1, headSha: LIFT_SHA, ...extra, }); - const liftedByApproval = testVerdict(['packages/spec/src/index.ts'], { size: { additions: 10764, deletions: 999, source: 'a fixture' }, approval: authorizedVerdict([{ login: 'os-zhuang', commitId: LIFT_SHA }]) }); + const liftedByApproval = testVerdict(['packages/spec/src/index.ts'], { repo: REPO, size: { additions: 10764, deletions: 999, source: 'a fixture' }, approval: authorizedVerdict([{ login: 'os-zhuang', commitId: LIFT_SHA }]) }); assert('⭐ an-authorized-APPROVED-review-LIFTS-the-size-limb-exceeds-stays-TRUE-and-the-PR-no-longer-lands-by-human-merge', liftedByApproval.size.exceeds === true && liftedByApproval.size.changedLines === 11763 && liftedByApproval.size.lifted !== null && liftedByApproval.size.lifted.approvers.join() === 'os-zhuang' && liftedByApproval.size.lifted.approvals[0].commitId === LIFT_SHA && @@ -6066,7 +6297,7 @@ async function selfTest() { liftedWords.includes('SIZE LIFTED') && liftedWords.includes('os-zhuang (on e4ead7480000)') && liftedWords.includes('objectstack#20153') && liftedWords.includes('2026-09-27') && liftedWords.includes('OWNING seat lands this PR through the queue') && !liftedWords.includes('HUMAN MERGE OR') && liftedWords.includes('NOT governed'), liftedWords); - assert('and-it-exits-on-the-NOT-governed-code-the-queue-lands-it', runTestModeExitFor(['packages/spec/src/index.ts'], { additions: 10764, deletions: 999 }, authorizedVerdict([{ login: 'os-zhuang', commitId: LIFT_SHA }])) === EXIT_TEST_NOT_GOVERNED); + assert('and-it-exits-on-the-NOT-governed-code-the-queue-lands-it', runTestModeExitFor(['packages/spec/src/index.ts'], REPO, { additions: 10764, deletions: 999 }, authorizedVerdict([{ login: 'os-zhuang', commitId: LIFT_SHA }])) === EXIT_TEST_NOT_GOVERNED); const overSize = { additions: 6000, deletions: 0 }; assert('⛔ NO-approval-handed-in-still-lands-by-human-merge-or-authorized-approval-the-2026-09-18-reading-unchanged', sizeLimbFires(sizeVerdict(overSize, null)) === true && sizeVerdict(overSize, null).lifted === null && sizeVerdict(overSize, null).approvalState === null); @@ -6082,12 +6313,12 @@ async function selfTest() { sizeLiftFrom({ state: 'approved', approvers: ['os-zhuang'], approvals: [] }) === null && sizeLiftFrom({ state: 'approved', approvals: [{ commitId: 'x' }] }) === null); assert('an-approval-under-the-line-lifts-nothing-because-there-is-nothing-to-lift', sizeVerdict({ additions: 10, deletions: 2 }, authorizedVerdict([{ login: 'hotlong', commitId: LIFT_SHA }])).lifted === null && sizeVerdict(null, authorizedVerdict([{ login: 'hotlong', commitId: LIFT_SHA }])).lifted === null); - const liftedGoverned = testVerdict(['AGENTS.md'], { size: overSize, approval: authorizedVerdict([{ login: 'hotlong', commitId: LIFT_SHA }]) }); + const liftedGoverned = testVerdict(['AGENTS.md'], { repo: REPO, size: overSize, approval: authorizedVerdict([{ login: 'hotlong', commitId: LIFT_SHA }]) }); assert('⭐ the-lift-moves-the-SIZE-limb-only-a-governed-PATH-still-lands-per-its-tier-and-a-FORK-head-is-untouched', liftedGoverned.governed === true && landsByHumanMerge(liftedGoverned) === true && liftedGoverned.size.lifted !== null && - landsByHumanMerge(testVerdict(['x.ts'], { size: overSize, approval: authorizedVerdict([{ login: 'hotlong', commitId: LIFT_SHA }]), fork: forkVerdict({ base: { repo: { full_name: 'o/r' } }, head: { repo: { full_name: 'f/r' } } }) })) === true); + landsByHumanMerge(testVerdict(['x.ts'], { repo: REPO, size: overSize, approval: authorizedVerdict([{ login: 'hotlong', commitId: LIFT_SHA }]), fork: forkVerdict({ base: { repo: { full_name: 'o/r' } }, head: { repo: { full_name: 'f/r' } } }) })) === true); assert('⭐ the-post-merge-sweep-hands-in-NO-approval-so-an-oversized-landing-is-STILL-listed', - classifyCommit({ sha: 'b'.repeat(40), date: '2026-09-27T02:02:33Z', subject: 'feat!: retire the saved-report stack (#20125)' }, ['packages/x.ts'], null, { additions: 10764, deletions: 999 }) !== null); + classifyCommit({ sha: 'b'.repeat(40), date: '2026-09-27T02:02:33Z', subject: 'feat!: retire the saved-report stack (#20125)' }, ['packages/x.ts'], GOVERNED_REPOS[0], { additions: 10764, deletions: 999 }) !== null); assert('and-the-not-lifted-words-name-BOTH-landings-the-authorized-approval-and-the-maintainers-merge', bigWords.includes('until an account in') && bigWords.includes('GOVERNED_APPROVERS has APPROVED it, on ANY commit') && bigWords.includes('objectstack#20153') && bigWords.includes("the maintainer's own merge is the other landing") && !bigWords.includes('lands only by a human merge'), bigWords); @@ -6143,6 +6374,7 @@ async function selfTest() { { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); const repo = join(sizeFx, 'repo'); g3(sizeFx, 'init', '-q', repo); + g3(repo, 'remote', 'add', 'origin', 'https://github.com/objectstack-ai/objectstack.git'); // the checkout's repository (ruling B2) const put = (rel, data) => { mkdirSync(dirname(join(repo, rel)), { recursive: true }); writeFileSync(join(repo, rel), data); @@ -6209,19 +6441,19 @@ async function selfTest() { battery('⭐ the FORK predicate: head repo ≠ base repo is a proposal, never a delivery'); const pullFrom = (headRepo, sha = 'feedface0001') => ({ head: { sha, repo: headRepo === null ? null : { full_name: headRepo } }, base: { repo: { full_name: 'o/r' } } }); const forkOf = (headRepo) => forkVerdict(pullFrom(headRepo)); - const wordsOf = (paths, intake) => renderTestVerdict(testVerdict(paths, intake)); - const forkPlain = testVerdict(['packages/spec/src/index.ts'], { fork: forkOf('someone/r') }); + const wordsOf = (paths, intake) => renderTestVerdict(testVerdict(paths, { repo: REPO, ...intake })); + const forkPlain = testVerdict(['packages/spec/src/index.ts'], { repo: REPO, fork: forkOf('someone/r') }); const forkWords = renderTestVerdict(forkPlain); assert('⭐ a-fork-head-on-ordinary-paths-is-NOT-governed-by-path-and-lands-by-HUMAN-MERGE-on-the-fork-limb-the-GOVERNED-exit-with-the-fork-sentence-a-PROPOSAL-never-a-delivery-adopted-onto-an-internal-branch', forkPlain.governed === false && forkPlain.fork.measured === true && forkPlain.fork.isFork === true && forkPlain.humanMerge === true && landsByHumanMerge(forkPlain) === true && forkWords.includes('FORK PR — head someone/r ≠ base o/r') && forkWords.includes('PROPOSAL, never a delivery') && forkWords.includes('arms auto-merge') && forkWords.includes('approves it') && forkWords.includes('Co-authored-by') && forkWords.includes('HEAD REPO decides') && !forkWords.includes('ordinary queue landing applies'), forkWords); assert('⭐ head.repo-null-a-deleted-fork-is-a-FORK-fail-closed', - forkOf(null).isFork === true && landsByHumanMerge(testVerdict(['x.ts'], { fork: forkOf(null) })) && wordsOf(['x.ts'], { fork: forkOf(null) }).includes('(deleted fork repo) ≠ base o/r'), wordsOf(['x.ts'], { fork: forkOf(null) })); + forkOf(null).isFork === true && landsByHumanMerge(testVerdict(['x.ts'], { repo: REPO, fork: forkOf(null) })) && wordsOf(['x.ts'], { fork: forkOf(null) }).includes('(deleted fork repo) ≠ base o/r'), wordsOf(['x.ts'], { fork: forkOf(null) })); const sameRepo = wordsOf(['packages/spec/src/index.ts'], { fork: forkOf('o/r'), size: { additions: 1, deletions: 0 } }); assert('a-same-repo-head-on-ordinary-paths-is-unchanged-NOT-governed-ordinary-queue-landing-and-no-fork-line', - forkOf('o/r').isFork === false && forkOf('o/r').measured === true && landsByHumanMerge(testVerdict(['x.ts'], { fork: forkOf('o/r') })) === false && sameRepo.includes('ordinary queue landing applies') && !sameRepo.includes('head repo') && !sameRepo.includes('FORK'), sameRepo); - const forkTierOf = (path, headRepo) => testVerdict([path], { fork: forkOf(headRepo) }); + forkOf('o/r').isFork === false && forkOf('o/r').measured === true && landsByHumanMerge(testVerdict(['x.ts'], { repo: REPO, fork: forkOf('o/r') })) === false && sameRepo.includes('ordinary queue landing applies') && !sameRepo.includes('head repo') && !sameRepo.includes('FORK'), sameRepo); + const forkTierOf = (path, headRepo) => testVerdict([path], { repo: REPO, fork: forkOf(headRepo) }); const S_PATH = '.claude/agents/os-dev.md'; assert('a-same-repo-governed-head-keeps-its-tier-H-or-S-word-for-word-while-a-fork-head-on-a-Tier-S-path-still-prints-the-fork-sentence-the-seat-adopts-it-never-lands-it', forkTierOf('AGENTS.md', 'o/r').tier === GOVERNED_TIER_H && !wordsOf(['AGENTS.md'], { fork: forkOf('o/r') }).includes('FORK') && wordsOf(['AGENTS.md'], { fork: forkOf('o/r') }).includes('H(人合)') && @@ -6276,7 +6508,7 @@ async function selfTest() { // would abort the whole run with a TypeError — a self-test that crashes hides // every other failure it had already collected. A red case must be a NAMED one. assert('⭐ classifyCommit-IS-the-landed-predicate-landsByHumanMerge-on-testVerdict-never-a-restated-comparison', - [bigPlain, govSmall, govLegacy, govBig].every((e) => e !== null && landsByHumanMerge(testVerdict(e.surfaces.flatMap((s) => s.files), { size: e.size.measured ? e.size : null })) === true)); + [bigPlain, govSmall, govLegacy, govBig].every((e) => e !== null && landsByHumanMerge(testVerdict(e.surfaces.flatMap((s) => s.files), { repo: REPO, size: e.size.measured ? e.size : null })) === true)); // The report an operator reads. const bigEntry = bigPlain === null ? null @@ -6719,10 +6951,10 @@ async function selfTest() { return SELF_TEST_VERDICT; } -/** The exit code `--test` would return for a path list — pinned without spawning. */ -function runTestModeExitFor(paths, size = null, approval = null) { +/** The exit code `--test` would return for a path list in `repo` — pinned without spawning. */ +function runTestModeExitFor(paths, repo, size = null, approval = null) { if (paths.length === 0) return EXIT_CANNOT_SWEEP; - return landsByHumanMerge(testVerdict(paths, { size, approval })) ? EXIT_TEST_GOVERNED : EXIT_TEST_NOT_GOVERNED; + return landsByHumanMerge(testVerdict(paths, { repo, size, approval })) ? EXIT_TEST_GOVERNED : EXIT_TEST_NOT_GOVERNED; } // `invokedDirectly` for the same reason the main-invocation guard above diff --git a/scripts/pm/check-governed-prose.mjs b/scripts/pm/check-governed-prose.mjs index c4e3a8d6c38..4120106972e 100644 --- a/scripts/pm/check-governed-prose.mjs +++ b/scripts/pm/check-governed-prose.mjs @@ -138,7 +138,14 @@ const UNATTRIBUTED_BATTERY = '(no battery open)'; // the argv of whatever imported THIS file too, for the duration of the import. // The static form stays a dynamic `import()` only because // `scripts/pm/dispatch-gates.mjs` reads this edge. -const { GOVERNED_SURFACES } = await import('./check-governed-merges.mjs'); +// +// The register is scoped PER ROW (maintainer ruling B2, 2026-10-13): a row may +// name the repositories it governs, and the instruction files this gate reads +// are THIS repository's. So the globs the prose must contain are the rows that +// govern `SELF_REPO_ID` — never the whole table, which would demand that this +// repository's prose claim a surface governed only elsewhere (an over-claim in +// the other direction). +const { SELF_REPO_ID, governedSurfacesFor } = await import('./check-governed-merges.mjs'); const REPO_ROOT = new URL('../../', import.meta.url); @@ -228,7 +235,7 @@ export function verdict(regionText, registerGlobs) { } function registerGlobs() { - return GOVERNED_SURFACES.map((surface) => surface.glob); + return governedSurfacesFor(SELF_REPO_ID).map((surface) => surface.glob); } function runGate() { @@ -237,8 +244,8 @@ function runGate() { if (globs.length === 0) { console.error( - '✗ check-governed-prose: GOVERNED_SURFACES is empty — red, not a pass. An empty register makes ' + - 'containment vacuously true, which is the false green this check exists to remove.', + `✗ check-governed-prose: no GOVERNED_SURFACES row governs ${SELF_REPO_ID} — red, not a pass. An empty ` + + 'register makes containment vacuously true, which is the false green this check exists to remove.', ); return 1; } @@ -289,7 +296,7 @@ function runGate() { console.log( `✓ check-governed-prose: ${PROSE_SURFACES.length} instruction surface(s) name all ${globs.length} ` + - `registered governed surfaces (${globs.join(' · ')}) and claim no others.`, + `registered governed surfaces that govern ${SELF_REPO_ID} (${globs.join(' · ')}) and claim no others.`, ); return 0; } diff --git a/scripts/pm/check-governed-queue-guard.mjs b/scripts/pm/check-governed-queue-guard.mjs index 34e7724dc25..9b59f22c8eb 100644 --- a/scripts/pm/check-governed-queue-guard.mjs +++ b/scripts/pm/check-governed-queue-guard.mjs @@ -493,13 +493,17 @@ import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { GENERATED_SURFACE_EXCEPTIONS, + GOVERNED_REPOS, GOVERNED_SURFACES, GOVERNED_TIER_H, GOVERNED_TIER_S, HUMAN_MERGE_LINE_THRESHOLD, + SELF_REPO_ID, applyGeneratedExceptions, + assertGovernedRepo, generatedExceptionFor, governedPathsIn, + governedSurfacesFor, governedTierFor, groupHitsByException, landsByHumanMerge, @@ -507,6 +511,7 @@ import { pullSizeFrom, recomputeProvenanceFor, renderLiftApprovals, + repoIdForSlug, testVerdict, } from './check-governed-merges.mjs'; import { isEntrypoint } from '../invoked-as.mjs'; @@ -552,6 +557,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ '⭐ #19344: the remedy names a path the ruleset actually offers': 5, '⛔ the queued pull-back: the remedy names no dequeue act a seat has': 4, '⭐ the 2026-09-20 ruling: a certified PURE REGENERATION carries the record to the queued head': 11, + 'the per-row repository scope (ruling B2, 2026-10-13)': 14, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -559,7 +565,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ // contract-review carrier battery left with the label it read (ruling record // 5770886272 on #19061, letter B): the ordinary direction, one battery, one row. // Raised 23 → 24 with the queued pull-back battery: one battery, one row. -const SELF_TEST_BATTERY_FLOOR = 24; +const SELF_TEST_BATTERY_FLOOR = 25; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -904,6 +910,30 @@ export function resolveEventContext({ eventName, payload }) { }; } +/** + * The repository this guard judges — the one its workflow runs in, read off + * `GITHUB_REPOSITORY` and resolved to a `GOVERNED_REPOS` id; this file's own + * repository when the variable is unset (a local run). The register is scoped + * per row, so every question below carries it: a sibling copy of this guard + * running in `objectui` judges objectui's rows, and a run in a repository the + * register does not know REFUSES (exit 1) rather than judging it against some + * other repository's rows — a wrong scope reads exactly like a clear diff. + * Pure. Returns `{ repo, slug, source }` or `{ error }`. + */ +export function guardRepository(env = {}) { + const raw = String(env?.GITHUB_REPOSITORY ?? '').trim(); + const self = GOVERNED_REPOS.find((r) => r.id === SELF_REPO_ID); + const slug = raw === '' ? self.slug : raw; + const repo = repoIdForSlug(slug); + if (repo !== null) return { repo, slug, source: raw === '' ? 'this file\'s own repository' : 'GITHUB_REPOSITORY' }; + return { + error: + `GITHUB_REPOSITORY names ${slug}, which is not a governed repository (${GOVERNED_REPOS.map((r) => r.id).join(', ')}). ` + + 'The register is scoped per row, so this guard judges a diff against the rows that govern ONE repository and ' + + 'has none for this one — it refuses rather than judging the diff against another repository\'s rows.', + }; +} + /** * 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. @@ -914,11 +944,12 @@ export function resolveEventContext({ eventName, payload }) { * 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) { +export function decomposeGovernedWork(rows, repo) { + assertGovernedRepo(repo, 'decomposeGovernedWork'); const byPull = new Map(); const unattributed = []; for (const row of Array.isArray(rows) ? rows : []) { - const surfaces = governedPathsIn(row?.paths ?? []); + const surfaces = governedPathsIn(row?.paths ?? [], repo); if (surfaces.length === 0) continue; const paths = surfaces.flatMap((s) => s.files); if (typeof row.pr !== 'number' || !Number.isInteger(row.pr) || row.pr <= 0) { @@ -936,7 +967,7 @@ export function decomposeGovernedWork(rows) { // The landing TIER travels with the entry (#18020), derived from the // SAME governed path list the verdict is judged on — never from the raw // diff, which carries files the register does not govern at all. - return { pr: entry.pr, shas: entry.shas, paths, surfaces: governedPathsIn(paths), tier: governedTierFor(paths) }; + return { pr: entry.pr, shas: entry.shas, paths, surfaces: governedPathsIn(paths, repo), tier: governedTierFor(paths, repo) }; }) .sort((a, b) => a.pr - b.pr); return { governed, unattributed }; @@ -1089,7 +1120,8 @@ export function entryUnreadable(entry) { * 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(), records = new Map(), apiCalls = 0, headNotes = [], lifted = [] }) { +export function guardVerdict({ event, repo, governed = [], unattributed = [], approvals = new Map(), records = new Map(), apiCalls = 0, headNotes = [], lifted = [] }) { + assertGovernedRepo(repo, 'guardVerdict'); const entries = governed.map((entry) => ({ ...entry, approval: approvals.get(entry.pr) ?? unreadableApproval('no review reading was recorded for this pull request'), @@ -1101,7 +1133,7 @@ export function guardVerdict({ event, governed = [], unattributed = [], approval ? { record: records.get(entry.pr) ?? { state: 'unreadable', reason: 'no record reading was recorded for this pull request' } } : {}), })); - const base = { event, entries, unattributed, apiCalls, headNotes, lifted, contextName: CHECK_CONTEXT_NAME }; + const base = { event, repo, entries, unattributed, apiCalls, headNotes, lifted, contextName: CHECK_CONTEXT_NAME }; if (entries.length === 0 && unattributed.length === 0) { return { ...base, conclusion: 'clear', exitCode: EXIT_CLEAR, refusalKind: null }; @@ -1180,7 +1212,7 @@ export function renderGuardVerdict(verdict) { if (lifted.length === 0) { lines.push( ' ✅ CLEAR — the diff touches no governed surface, so this guard has nothing to judge.', - ` Derived from GOVERNED_SURFACES in scripts/pm/check-governed-merges.mjs (${GOVERNED_SURFACES.length} surfaces),`, + ` Derived from GOVERNED_SURFACES in scripts/pm/check-governed-merges.mjs (${governedSurfacesFor(verdict.repo).length} surfaces govern ${verdict.repo}),`, ' 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.', ); @@ -1208,7 +1240,7 @@ export function renderGuardVerdict(verdict) { ' and the lift line(s) printed above are that recompute\'s record. Lifted here:', ...lifted.slice(0, 12).map((p) => ` - ${p}`), ...(lifted.length > 12 ? [` … and ${lifted.length - 12} more`] : []), - ` Derived from GOVERNED_SURFACES in scripts/pm/check-governed-merges.mjs (${GOVERNED_SURFACES.length} surfaces),`, + ` Derived from GOVERNED_SURFACES in scripts/pm/check-governed-merges.mjs (${governedSurfacesFor(verdict.repo).length} surfaces govern ${verdict.repo}),`, ' never from a restated list. ⛔ ZERO review lookups were made: the approval predicate is reached only', ' by a governed path the register did not lift, and there was none.', ); @@ -1509,10 +1541,10 @@ export function renderGuardVerdict(verdict) { * still governs everything the verdict is derived FROM; it never governed * things the verdict merely mentions. */ -export async function runGuard({ event, rows, fetchReviews, fetchPull, fetchComments, loadRecognisers = loadRecordRecognisers, lifted = [], runGit = null, baseRef = null }) { - const { governed, unattributed } = decomposeGovernedWork(rows); +export async function runGuard({ event, repo, rows, fetchReviews, fetchPull, fetchComments, loadRecognisers = loadRecordRecognisers, lifted = [], runGit = null, baseRef = null }) { + const { governed, unattributed } = decomposeGovernedWork(rows, assertGovernedRepo(repo, 'runGuard')); if (governed.length === 0 && unattributed.length === 0) { - return guardVerdict({ event, governed, unattributed, apiCalls: 0, lifted }); + return guardVerdict({ event, repo, governed, unattributed, apiCalls: 0, lifted }); } const approvals = new Map(); const records = new Map(); @@ -1627,7 +1659,7 @@ export async function runGuard({ event, rows, fetchReviews, fetchPull, fetchComm records.set(entry.pr, recordVerdict({ pair, recognisers, cardNote })); } } - return guardVerdict({ event, governed, unattributed, approvals, records, apiCalls, headNotes, lifted }); + return guardVerdict({ event, repo, governed, unattributed, approvals, records, apiCalls, headNotes, lifted }); } // ── every queued pull request: the enumeration the SIZE leg reads ────────── @@ -1681,8 +1713,8 @@ export const SIZE_LEG_NAME = 'Human-Merge Size Line (2026-09-18 ruling)'; * exceeds: false` for it, which `landsByHumanMerge` would read as clear, and * that is precisely the fail-open reading this leg exists to refuse. */ -export function sizeReading(pr, size, approval = null) { - const verdict = testVerdict([], { size, approval }); +export function sizeReading(pr, size, repo, approval = null) { + const verdict = testVerdict([], { repo, size, approval }); if (verdict.size?.measured !== true) { return { pr, @@ -1712,8 +1744,9 @@ export function sizeReading(pr, size, approval = null) { * never `within`. A `lifted` reading (over the line, authorized approval on * record — 2026-09-27) is a CLEAR, and the entry keeps saying it was lifted. */ -export function sizeGuardVerdict({ event, pulls = [], readings = new Map(), apiCalls = 0 }) { - const base = { event, entries: [], apiCalls, legName: SIZE_LEG_NAME }; +export function sizeGuardVerdict({ event, repo, pulls = [], readings = new Map(), apiCalls = 0 }) { + assertGovernedRepo(repo, 'sizeGuardVerdict'); + const base = { event, repo, entries: [], apiCalls, legName: SIZE_LEG_NAME }; if (event !== EVENT_MERGE_GROUP) { return { ...base, conclusion: 'not-applicable', exitCode: EXIT_CLEAR, refusalKind: null }; } @@ -1725,7 +1758,7 @@ export function sizeGuardVerdict({ event, pulls = [], readings = new Map(), apiC readings.get(pr) ?? { pr, state: 'unreadable', - verdict: testVerdict([], { size: null }), + verdict: testVerdict([], { repo, size: null }), reason: 'no size reading was recorded for this pull request', }, ); @@ -1751,8 +1784,9 @@ export function sizeGuardVerdict({ event, pulls = [], readings = new Map(), apiC * count is printed and pinned. A throw is caught into an `unreadable` reading, * never a pass and never an escaping rejection. */ -export async function runSizeGuard({ event, rows, namedPull = null, fetchPull, fetchReviews = null, approvals = new Map() }) { - if (event !== EVENT_MERGE_GROUP) return sizeGuardVerdict({ event }); +export async function runSizeGuard({ event, repo, rows, namedPull = null, fetchPull, fetchReviews = null, approvals = new Map() }) { + assertGovernedRepo(repo, 'runSizeGuard'); + if (event !== EVENT_MERGE_GROUP) return sizeGuardVerdict({ event, repo }); const pulls = queuedPullsInGroup(rows, namedPull); const readings = new Map(); let apiCalls = 0; @@ -1760,7 +1794,7 @@ export async function runSizeGuard({ event, rows, namedPull = null, fetchPull, f try { apiCalls += 1; const pull = await fetchPull(pr); - let reading = sizeReading(pr, pull?.size ?? null); + let reading = sizeReading(pr, pull?.size ?? null, repo); if (reading.state === 'oversized') { // ⭐ THE LIFT (2026-09-27, #20153): over the line, so the approval is // consulted — the SAME verdict the governed leg computed on this build @@ -1781,19 +1815,19 @@ export async function runSizeGuard({ event, rows, namedPull = null, fetchPull, f } } } - reading = sizeReading(pr, pull?.size ?? null, approval); + reading = sizeReading(pr, pull?.size ?? null, repo, approval); } readings.set(pr, reading); } catch (error) { readings.set(pr, { pr, state: 'unreadable', - verdict: testVerdict([], { size: null }), + verdict: testVerdict([], { repo, size: null }), reason: String(error?.message ?? error).split('\n')[0], }); } } - return sizeGuardVerdict({ event, pulls, readings, apiCalls }); + return sizeGuardVerdict({ event, repo, pulls, readings, apiCalls }); } // ── #19344: the remedy's terminal is a RULESET fact, so it is READ ───────── @@ -2035,7 +2069,8 @@ export function enumerateRows(root, baseSha, headSha, fallbackPull = null) { * GOVERNED — the note it pushes states the reason in full rather than implying * it. */ -export async function liftGeneratedExceptions(root, baseSha, rows, notes, recompute = recomputeProvenanceFor) { +export async function liftGeneratedExceptions(root, baseSha, rows, notes, recompute = recomputeProvenanceFor, repo = undefined) { + assertGovernedRepo(repo, 'liftGeneratedExceptions'); const registered = (row) => row.paths.filter((p) => generatedExceptionFor(p) !== null); if (!rows.some((row) => registered(row).length > 0)) return rows; const cache = new Map(); @@ -2051,7 +2086,7 @@ export async function liftGeneratedExceptions(root, baseSha, rows, notes, recomp baseRef: baseSha, cache, }); - const lifted = applyGeneratedExceptions(testVerdict(row.paths), provenance); + const lifted = applyGeneratedExceptions(testVerdict(row.paths, { repo }), provenance); for (const e of lifted.exceptions ?? []) { notes.push( e.pureRegeneration @@ -2286,6 +2321,16 @@ async function main() { } } + // The repository this guard judges (ruling B2): resolved BEFORE the diff is + // read, because the lift step below already asks the register a scoped + // question. A repository the register does not know is a refusal here. + const where = guardRepository(env); + if (where.error) { + console.error(`⛔ ${CHECK_CONTEXT_NAME}: ${where.error}`); + return EXIT_CANNOT_RUN; + } + const repo = where.repo; + const notes = []; let rows; let lifted = []; @@ -2293,17 +2338,16 @@ async function main() { const mergeBase = git(repoRoot, ['merge-base', context.baseSha, context.headSha]).trim(); rows = enumerateRows(repoRoot, mergeBase, context.headSha, context.namedPull); const beforeLift = rows; - rows = await liftGeneratedExceptions(repoRoot, mergeBase, rows, notes); + rows = await liftGeneratedExceptions(repoRoot, mergeBase, rows, notes, recomputeProvenanceFor, repo); lifted = liftedPathsBetween(beforeLift, rows); } 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/objectstack'; const reader = { apiUrl: (env.GITHUB_API_URL ?? 'https://api.github.com').replace(/\/+$/, ''), - slug, + slug: where.slug, token: env.GITHUB_TOKEN || env.GH_TOKEN || null, }; const fetchReviews = makeReviewReader(reader); @@ -2311,7 +2355,7 @@ async function main() { const fetchComments = makeCommentReader(reader); const runGit = (args, input) => git(repoRoot, args, input); - const verdict = await runGuard({ event: context.event, rows, fetchReviews, fetchPull, fetchComments, lifted, runGit, baseRef: context.baseSha ?? null }); + const verdict = await runGuard({ event: context.event, repo, rows, fetchReviews, fetchPull, fetchComments, lifted, runGit, baseRef: context.baseSha ?? null }); // The SIZE leg (#19036): every queued pull request, through the same pull // reader the governed leg reads heads with. `merge_group` only, '' on the // other leg, so the `pull_request` output is byte-identical to what it was. @@ -2320,6 +2364,7 @@ async function main() { // oversized pull request gets one review read through the same reader. const size = await runSizeGuard({ event: context.event, + repo, rows, namedPull: context.namedPull, fetchPull, @@ -2438,6 +2483,9 @@ export async function selfTest() { if (!cond) failures.push(`${name}${detail ? `: ${detail}` : ''}`); }; const row = (pr, files, sha = 'a'.repeat(40), subject = `x (#${pr})`) => ({ sha, subject, pr, paths: files }); + // The repository every register question below is asked for, unless a case + // is ABOUT the scope and names another one: this guard's own (ruling B2). + const REPO = SELF_REPO_ID; const approved = (...logins) => logins.map((login) => ({ state: 'APPROVED', user: { login } })); // The queue-predicate fixtures: a head sha, an older sha, and a review // carrying the `commit_id` GitHub stamps at submission time. `commit_id` no @@ -2457,8 +2505,8 @@ export async function selfTest() { // the retired sha pin this exact fixture was the REFUSAL case. const authorizedPassOnOlder = (login = GOVERNED_APPROVERS[0]) => authorizedApprovalVerdict([approvedAt(login, OLD)], HEAD); const run = (event, rows, approvals = new Map(), records = new Map()) => { - const { governed, unattributed } = decomposeGovernedWork(rows); - return guardVerdict({ event, governed, unattributed, approvals, records, apiCalls: governed.length }); + const { governed, unattributed } = decomposeGovernedWork(rows, REPO); + return guardVerdict({ repo: REPO, event, governed, unattributed, approvals, records, apiCalls: governed.length }); }; // The record reading a Tier S entry gets when the thread was READ and held // nothing — the incident shape (#19133): "no record" is a refusal of its own @@ -2473,10 +2521,44 @@ export async function selfTest() { battery('the register is READ, never restated (#9840)'); for (const surface of GOVERNED_SURFACES) { const sample = surface.prefix ? `${surface.prefix}sample.md` : surface.exact; - const { governed } = decomposeGovernedWork([row(1, [sample])]); + const { governed } = decomposeGovernedWork([row(1, [sample])], REPO); assert(`the-register-drives-the-verdict-for-${surface.id}`, governed.length === 1 && governed[0].paths.includes(sample), sample); } - assert('this-file-restates-no-surface-list', GOVERNED_SURFACES.length >= 5 && governedPathsIn(['docs/adrs/z.md', 'examples/AGENTS.md']).length === 0); + assert('this-file-restates-no-surface-list', GOVERNED_SURFACES.length >= 5 && governedPathsIn(['docs/adrs/z.md', 'examples/AGENTS.md'], REPO).length === 0); + + // ── the per-row repository scope (ruling B2, 2026-10-13) ────────────────── + // + // The guard asks the register for ONE repository — its own — and the first + // scoped row makes that visible: a workflow path is governed Tier H in this + // queue and in objectui's and cloud's, not in objectos's or hotcrm's; and + // every front door of this file REFUSES a call that names no repository, + // because a wrong scope reads exactly like a clear diff. + battery('the per-row repository scope (ruling B2, 2026-10-13)'); + const WF = '.github/workflows/lint.yml'; + const wfWork = decomposeGovernedWork([row(23043, [WF])], REPO); + assert('a-workflow-path-in-this-queue-decomposes-to-ONE-governed-entry-Tier-H', wfWork.governed.length === 1 && wfWork.governed[0].tier === TIER_H && wfWork.governed[0].surfaces.map((s) => s.id).join() === 'workflows' && wfWork.governed[0].paths.join() === WF, JSON.stringify(wfWork)); + assert('the-same-path-is-governed-in-objectui-and-cloud', ['objectui', 'cloud'].every((r) => decomposeGovernedWork([row(1, [WF])], r).governed.length === 1 && governedTierFor([WF], r) === TIER_H)); + assert('and-NOT-in-objectos-or-hotcrm-the-scope-is-the-rows-not-the-guards', ['objectos', 'hotcrm'].every((r) => decomposeGovernedWork([row(1, [WF])], r).governed.length === 0 && governedPathsIn([WF], r).length === 0)); + assert('a-github-tree-sibling-outside-workflows-is-clear', decomposeGovernedWork([row(2, ['.github/ISSUE_TEMPLATE/bug.md', '.github/CODEOWNERS', '.github/workflowsx/y.yml'])], REPO).governed.length === 0); + const wfRefused = run('merge_group', [row(23043, [WF])], new Map([[23043, authorizedApprovalVerdict([], HEAD)]])); + assert('an-unapproved-workflow-PR-is-REFUSED-at-the-queue-on-the-Tier-H-terminal', wfRefused.exitCode === EXIT_REFUSED_UNAPPROVED && renderGuardVerdict(wfRefused).includes(WF) && /landing tier: H/.test(renderGuardVerdict(wfRefused)), renderGuardVerdict(wfRefused)); + assert('and-an-authorized-approval-on-the-head-clears-it', run('merge_group', [row(23043, [WF])], new Map([[23043, authorizedPass()]])).exitCode === EXIT_CLEAR); + const wfClearText = renderGuardVerdict(run('merge_group', [row(3, ['.github/ISSUE_TEMPLATE/bug.md'])])); + assert('the-clear-words-count-the-rows-that-govern-THIS-repository-and-name-it', /7 surfaces govern objectstack/.test(wfClearText), wfClearText); + const threw = (fn) => { try { fn(); return null; } catch (error) { return error; } }; + assert('⛔ decomposeGovernedWork-without-a-repository-is-an-ERROR-never-a-clear', threw(() => decomposeGovernedWork([row(1, [WF])])) instanceof TypeError && /decomposeGovernedWork wants the repository/.test(threw(() => decomposeGovernedWork([row(1, [WF])])).message)); + const liftRejects = await liftGeneratedExceptions('/w', 'base', [], []).then(() => null, (error) => error); + assert('⛔ and-so-are-the-verdict-builders-the-size-leg-and-the-lift', [() => guardVerdict({ event: 'merge_group' }), () => sizeGuardVerdict({ event: EVENT_MERGE_GROUP, pulls: [1] }), () => sizeReading(1, { additions: 1, deletions: 1 })].every((fn) => threw(fn) instanceof TypeError) && liftRejects instanceof TypeError, String(liftRejects?.message)); + const guardRejects = await runGuard({ event: 'merge_group', rows: [row(1, [WF])], fetchReviews: async () => [], fetchPull: async () => pull() }).then(() => null, (error) => error); + const sizeRejects = await runSizeGuard({ event: EVENT_MERGE_GROUP, rows: [row(1, [WF])], fetchPull: async () => pull() }).then(() => null, (error) => error); + assert('⛔ runGuard-and-runSizeGuard-REJECT-without-a-repository', guardRejects instanceof TypeError && sizeRejects instanceof TypeError, `${guardRejects?.message}\n${sizeRejects?.message}`); + assert('an-unknown-repository-is-refused-too-with-the-roster-in-the-words', threw(() => decomposeGovernedWork([row(1, [WF])], 'objectstack-ai/objectstack')) instanceof TypeError && /not a GOVERNED_REPOS id/.test(threw(() => decomposeGovernedWork([row(1, [WF])], 'objectstack-ai/objectstack')).message)); + // Where main() gets the repository: GITHUB_REPOSITORY, resolved; unset means this file's own. + assert('guardRepository: an unset GITHUB_REPOSITORY is this file\'s own repository', JSON.stringify(guardRepository({})) === JSON.stringify({ repo: 'objectstack', slug: 'objectstack-ai/objectstack', source: "this file's own repository" }) && guardRepository({ GITHUB_REPOSITORY: ' ' }).repo === 'objectstack'); + assert('guardRepository: a sibling copy of this guard judges the sibling', guardRepository({ GITHUB_REPOSITORY: 'objectstack-ai/objectui' }).repo === 'objectui' && guardRepository({ GITHUB_REPOSITORY: 'ObjectStack-AI/Cloud' }).repo === 'cloud' && guardRepository({ GITHUB_REPOSITORY: 'objectstack-ai/cloud' }).source === 'GITHUB_REPOSITORY'); + const foreignGuard = guardRepository({ GITHUB_REPOSITORY: 'someone/fork' }); + assert('⛔ guardRepository: a repository the register does not know is a REFUSAL naming the roster', typeof foreignGuard.error === 'string' && /someone\/fork, which is not a governed repository/.test(foreignGuard.error) && /objectstack, objectui, cloud, objectos, hotcrm/.test(foreignGuard.error) && foreignGuard.repo === undefined, foreignGuard.error); + assert('the-verdict-objects-carry-the-repository-they-were-judged-for', wfRefused.repo === REPO && run('pull_request', [row(1, [WF])]).repo === REPO && sizeGuardVerdict({ event: EVENT_MERGE_GROUP, repo: 'cloud', pulls: [1], readings: new Map([[1, sizeReading(1, { additions: 1, deletions: 0 }, 'cloud')]]) }).repo === 'cloud'); // ── the exit contract as a table ────────────────────────────────────────── battery('the exit contract as a table'); @@ -2617,16 +2699,16 @@ export async function selfTest() { // ── decomposition, and the multi-PR group trap ──────────────────────────── battery('decomposition, and the multi-PR group trap'); const clearRows = [row(1, ['packages/spec/src/index.ts', 'content/docs/x.mdx'])]; - 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/spec/src/index.ts'])]); + assert('a-clear-diff-decomposes-to-nothing', decomposeGovernedWork(clearRows, REPO).governed.length === 0 && decomposeGovernedWork(clearRows, REPO).unattributed.length === 0); + const mixed = decomposeGovernedWork([row(5, ['AGENTS.md', 'packages/spec/src/index.ts'])], REPO); 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/0120-x.md'], 'a'.repeat(40)), row(12, ['packages/core/src/x.ts'], 'b'.repeat(40))]); + const batched = decomposeGovernedWork([row(11, ['docs/adr/0120-x.md'], 'a'.repeat(40)), row(12, ['packages/core/src/x.ts'], 'b'.repeat(40))], REPO); 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/x/SKILL.md'], 'b'.repeat(40))]); + const twoGoverned = decomposeGovernedWork([row(11, ['AGENTS.md']), row(12, ['skills/x/SKILL.md'], 'b'.repeat(40))], REPO); 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'] }]); + const unattributed = decomposeGovernedWork([{ sha: 'c'.repeat(40), subject: 'chore: direct work', pr: null, paths: ['CLAUDE.md'] }], REPO); 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); + assert('an-UNGOVERNED-commit-naming-no-pr-is-simply-not-our-business', decomposeGovernedWork([{ sha: 'd'.repeat(40), subject: 'x', pr: null, paths: ['README.md'] }], REPO).unattributed.length === 0); // ── the verdict table, both events ──────────────────────────────────────── battery('the verdict table, both events'); @@ -2691,7 +2773,7 @@ export async function selfTest() { // thread carried no record — the reading is ABSENT, and the refusal is the // same "unapproved" the incident deserved. A Tier S entry with NO reading // at all refuses as UNREADABLE instead: "could not find out" never clears. - const tier = governedTierFor(replay.files); + const tier = governedTierFor(replay.files, REPO); const queued = run('merge_group', rows, new Map([[replay.pr, authorizedApprovalVerdict([], HEAD)]]), tier === TIER_S ? absentRecord(replay.pr) : new Map()); assert(`replay-REFUSES-at-the-queue: ${replay.name}`, queued.exitCode === EXIT_REFUSED_UNAPPROVED, JSON.stringify([queued.conclusion, tier])); if (tier === TIER_S) { @@ -2701,7 +2783,7 @@ export async function selfTest() { 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); + assert(`replay-names-its-governed-paths: ${replay.name}`, replay.files.filter((f) => governedPathsIn([f], REPO).length > 0).every((f) => text.includes(f)), text); } // ── ⭐ the ordering guarantee, measured with a spy that THROWS ──────────── @@ -2722,7 +2804,7 @@ export async function selfTest() { let orderedClear = null; let orderedThrow = null; try { - orderedClear = await runGuard({ event: 'merge_group', rows: clearRows, fetchReviews: explode, fetchPull: explode }); + orderedClear = await runGuard({ repo: REPO, event: 'merge_group', rows: clearRows, fetchReviews: explode, fetchPull: explode }); } catch (error) { orderedThrow = String(error?.message ?? error); } @@ -2735,6 +2817,7 @@ export async function selfTest() { // first — so the case above is proving an ordering, not a dead code path. const trace = []; const traced = await runGuard({ + repo: REPO, event: 'merge_group', rows: [row(1, ['AGENTS.md'])], fetchPull: () => { @@ -2753,6 +2836,7 @@ export async function selfTest() { JSON.stringify({ conclusion: traced.conclusion, apiCalls: traced.apiCalls }), ); const tracedOlder = await runGuard({ + repo: REPO, event: 'merge_group', rows: [row(1, ['AGENTS.md'])], fetchPull: () => pull(), @@ -2768,6 +2852,7 @@ export async function selfTest() { // same rendered line. let prHeadReads = 0; const prLeg = await runGuard({ + repo: REPO, event: 'pull_request', rows: [row(1, ['AGENTS.md'])], fetchPull: () => { @@ -2791,6 +2876,7 @@ export async function selfTest() { let thrownEscaped = null; try { thrown = await runGuard({ + repo: REPO, event: 'merge_group', rows: [row(1, ['AGENTS.md'])], fetchPull: () => pull(), @@ -2812,6 +2898,7 @@ export async function selfTest() { // both halves are measured — the reviews ARE read, and the verdict is theirs. let reviewsAfterHeadFailure = 0; const headFailed = await runGuard({ + repo: REPO, event: 'merge_group', rows: [row(1, ['AGENTS.md'])], fetchPull: () => { @@ -2957,16 +3044,17 @@ export async function selfTest() { [{ ...row(1, ['packages/spec/src/ui/responsive.zod.ts', genIndex]) }], liftNotes, verified(), + REPO, ); assert('a-certified-regeneration-inside-skills-leaves-the-row-with-nothing-governed', - governedPathsIn(liftedRows[0].paths).length === 0, JSON.stringify(liftedRows[0].paths)); + governedPathsIn(liftedRows[0].paths, REPO).length === 0, JSON.stringify(liftedRows[0].paths)); assert('and-the-note-cites-the-ruling-that-lifted-it', liftNotes.some((n) => n.includes('#11705') && n.includes('LIFTED')), JSON.stringify(liftNotes)); const mixedNotes = []; - const mixedRows = await liftGeneratedExceptions('/w', 'base', [{ ...row(2, [genIndex, handAuthored]) }], mixedNotes, verified()); + const mixedRows = await liftGeneratedExceptions('/w', 'base', [{ ...row(2, [genIndex, handAuthored]) }], mixedNotes, verified(), REPO); assert('but-hand-authored-skill-content-in-the-same-commit-still-governs-the-row', - governedPathsIn(mixedRows[0].paths).map((s) => s.files).flat().join() === handAuthored, JSON.stringify(mixedRows[0].paths)); + governedPathsIn(mixedRows[0].paths, REPO).map((s) => s.files).flat().join() === handAuthored, JSON.stringify(mixedRows[0].paths)); const refusedNotes = []; - const refusedRows = await liftGeneratedExceptions('/w', 'base', [{ ...row(3, [genIndex]) }], refusedNotes, refused()); + const refusedRows = await liftGeneratedExceptions('/w', 'base', [{ ...row(3, [genIndex]) }], refusedNotes, refused(), REPO); assert('a-refused-provenance-keeps-the-generated-path-governed-here-too', refusedRows[0].paths.join() === genIndex && refusedNotes.some((n) => n.includes('did NOT lift')), JSON.stringify(refusedNotes)); // The DEGRADED environment — the job installs dependencies now (#14063), but @@ -2976,14 +3064,14 @@ export async function selfTest() { // be the one a reader sees stated. const noToolNotes = []; const noToolRows = await liftGeneratedExceptions('/w', 'base', [{ ...row(4, [genIndex]) }], noToolNotes, - refused('could not run `pnpm --filter @objectstack/spec gen:skill-refs` on this tree (spawn pnpm ENOENT) — the generator toolchain is not available in this environment')); + refused('could not run `pnpm --filter @objectstack/spec gen:skill-refs` on this tree (spawn pnpm ENOENT) — the generator toolchain is not available in this environment'), REPO); assert('with-no-generator-toolchain-this-guard-keeps-the-path-governed-and-says-why', noToolRows[0].paths.join() === genIndex && noToolNotes.some((n) => /toolchain is not available/.test(n)), JSON.stringify(noToolNotes)); // A commit that touches no registered path never consults provenance — the // recompute is handed a spy that throws if it is called at all. const untouched = await liftGeneratedExceptions('/w', 'base', [{ ...row(5, ['AGENTS.md']) }], [], async () => { throw new Error('the recompute must not run for a diff with no registered path'); - }); + }, REPO); assert('an-ordinary-governed-diff-never-pays-for-a-recompute', untouched[0].paths.join() === 'AGENTS.md'); // ── ⭐ #14063 END TO END: what the dependency install actually buys ──────── @@ -3012,8 +3100,8 @@ export async function selfTest() { }); const endToEnd = async (recompute, rows, { head = () => pull(), reviews = () => [] } = {}) => { const notes = []; - const lifted = await liftGeneratedExceptions('/w', 'base', rows, notes, recompute); - return { verdict: await runGuard({ event: 'merge_group', rows: lifted, fetchPull: head, fetchReviews: reviews }), notes }; + const lifted = await liftGeneratedExceptions('/w', 'base', rows, notes, recompute, REPO); + return { verdict: await runGuard({ repo: REPO, event: 'merge_group', rows: lifted, fetchPull: head, fetchReviews: reviews }), notes }; }; // ⭐ Zero approvals AND zero API calls: once the row is lifted the diff // touches nothing governed, so the guard returns before a request exists — @@ -3075,7 +3163,7 @@ export async function selfTest() { try { await liftGeneratedExceptions('/w', 'base', [regenRow()], [], async () => { throw new Error('spawn pnpm EACCES'); - }); + }, REPO); } catch (error) { liftThrew = String(error?.message ?? error); } @@ -3127,9 +3215,10 @@ export async function selfTest() { // those two, so a helper that skips it cannot see it. const asMainDoes = async (recompute, rows, io = {}) => { const notes = []; - const after = await liftGeneratedExceptions('/w', 'base', rows, notes, recompute); + const after = await liftGeneratedExceptions('/w', 'base', rows, notes, recompute, REPO); const lifted = liftedPathsBetween(rows, after); const verdict = await runGuard({ + repo: REPO, event: 'merge_group', rows: after, fetchPull: io.head ?? (() => pull()), @@ -3151,8 +3240,8 @@ export async function selfTest() { ); assert( '⭐ the-mixed-diff-rule-DID-match-15284-before-the-lift-one-hit-out-of-eleven-paths', - governedPathsIn(pr15284Paths).flatMap((s) => s.files).join() === pr15284ReactBlocks, - JSON.stringify(governedPathsIn(pr15284Paths)), + governedPathsIn(pr15284Paths, REPO).flatMap((s) => s.files).join() === pr15284ReactBlocks, + JSON.stringify(governedPathsIn(pr15284Paths, REPO)), ); const certified15284 = await asMainDoes( verified('byte-equal to the react-blocks generator recomputed on this tree (fixture)'), @@ -3206,7 +3295,7 @@ export async function selfTest() { // And the zero-cost clear is untouched, to the byte, on BOTH legs — the // 2026-08-27 byte-identity constraint on the pull_request leg included. for (const event of [EVENT_MERGE_GROUP, EVENT_PULL_REQUEST]) { - const nothing = renderGuardVerdict(guardVerdict({ event, governed: [], unattributed: [], apiCalls: 0 })); + const nothing = renderGuardVerdict(guardVerdict({ repo: REPO, event, governed: [], unattributed: [], apiCalls: 0 })); assert( `a-clear-with-nothing-lifted-keeps-its-pre-15406-wording-byte-for-byte: ${event}`, nothing.includes(' ✅ CLEAR — the diff touches no governed surface, so this guard has nothing to judge.') && @@ -3289,8 +3378,8 @@ export async function selfTest() { // ⭐ #15406's constraint, re-measured for this leg: the merge_group zero-cost // clear is SCOPED (this leg reads every queued pull request there), while the // pull_request one keeps its 2026-08-27 bytes exactly. - const clearMg = renderGuardVerdict(guardVerdict({ event: EVENT_MERGE_GROUP, governed: [], unattributed: [], apiCalls: 0 })); - const clearPr = renderGuardVerdict(guardVerdict({ event: EVENT_PULL_REQUEST, governed: [], unattributed: [], apiCalls: 0 })); + const clearMg = renderGuardVerdict(guardVerdict({ repo: REPO, event: EVENT_MERGE_GROUP, governed: [], unattributed: [], apiCalls: 0 })); + const clearPr = renderGuardVerdict(guardVerdict({ repo: REPO, event: EVENT_PULL_REQUEST, governed: [], unattributed: [], apiCalls: 0 })); assert( '⭐ the-merge_group-zero-cost-clear-SCOPES-its-outage-promise-because-the-size-leg-reads-every-queued-PR', clearMg.includes('Scoped to the GOVERNED-SURFACE leg') && clearMg.includes('An outage there DOES refuse this merge group.'), @@ -3323,15 +3412,15 @@ export async function selfTest() { JSON.stringify(unsizedRow), ); // The reading: the sibling's predicate, size limb alone. - const overRead = sizeReading(1, sizePair(LINE, 1)); - const atRead = sizeReading(1, sizePair(LINE, 0)); - const underRead = sizeReading(1, sizePair(LINE - 1, 0)); + const overRead = sizeReading(1, sizePair(LINE, 1), REPO); + const atRead = sizeReading(1, sizePair(LINE, 0), REPO); + const underRead = sizeReading(1, sizePair(LINE - 1, 0), REPO); assert('⭐ threshold-plus-one-changed-line-is-OVERSIZED', overRead.state === 'oversized' && overRead.verdict.size.changedLines === LINE + 1, JSON.stringify(overRead.verdict.size)); assert('⭐ exactly-the-threshold-is-WITHIN-the-comparison-is-strictly-greater', atRead.state === 'within' && atRead.verdict.size.changedLines === LINE, JSON.stringify(atRead.verdict.size)); assert('one-under-is-WITHIN', underRead.state === 'within'); assert( '⛔ no-pair-is-UNREADABLE-never-within', - sizeReading(1, null).state === 'unreadable' && sizeReading(1, { additions: 3 }).state === 'unreadable' && /never a size of zero/.test(sizeReading(1, null).reason), + sizeReading(1, null, REPO).state === 'unreadable' && sizeReading(1, { additions: 3 }, REPO).state === 'unreadable' && /never a size of zero/.test(sizeReading(1, null, REPO).reason), ); assert( '⭐ the-reading-IS-the-landed-predicate-landsByHumanMerge-agrees-on-every-branch-and-the-path-limb-stays-out-of-it', @@ -3345,11 +3434,11 @@ export async function selfTest() { readings.set( n, state === 'unreadable' - ? { pr: n, state, verdict: testVerdict([], { size: null }), reason: 'HTTP 403 (fixture)' } - : sizeReading(n, state === 'oversized' ? sizePair(LINE, 1) : sizePair(LINE, 0)), + ? { pr: n, state, verdict: testVerdict([], { repo: REPO, size: null }), reason: 'HTTP 403 (fixture)' } + : sizeReading(n, state === 'oversized' ? sizePair(LINE, 1) : sizePair(LINE, 0), REPO), ); } - return sizeGuardVerdict({ event: EVENT_MERGE_GROUP, pulls, readings, apiCalls: pulls.length }); + return sizeGuardVerdict({ repo: REPO, event: EVENT_MERGE_GROUP, pulls, readings, apiCalls: pulls.length }); }; const overOne = sizeRun([5], { 5: 'oversized' }); assert('an-OVERSIZED-queued-PR-REFUSES-with-code-8', overOne.exitCode === EXIT_REFUSED_OVERSIZED && overOne.conclusion === 'refused' && overOne.refusalKind === 'oversized'); @@ -3359,19 +3448,20 @@ export async function selfTest() { assert('⛔ an-UNREADABLE-size-REFUSES-with-its-OWN-code-9-never-passes', unreadOne.exitCode === EXIT_REFUSED_SIZE_UNREADABLE && unreadOne.refusalKind === 'unreadable'); assert( 'a-pull-with-NO-recorded-reading-REFUSES-rather-than-defaulting-to-within', - sizeGuardVerdict({ event: EVENT_MERGE_GROUP, pulls: [5], readings: new Map(), apiCalls: 1 }).exitCode === EXIT_REFUSED_SIZE_UNREADABLE, + sizeGuardVerdict({ repo: REPO, event: EVENT_MERGE_GROUP, pulls: [5], readings: new Map(), apiCalls: 1 }).exitCode === EXIT_REFUSED_SIZE_UNREADABLE, ); - const sizeNoPull = sizeGuardVerdict({ event: EVENT_MERGE_GROUP, pulls: [], readings: new Map(), apiCalls: 0 }); + const sizeNoPull = sizeGuardVerdict({ repo: REPO, event: EVENT_MERGE_GROUP, pulls: [], readings: new Map(), apiCalls: 0 }); assert('a-merge-group-naming-NO-pull-request-REFUSES-on-9-with-kind-no-pull', sizeNoPull.exitCode === EXIT_REFUSED_SIZE_UNREADABLE && sizeNoPull.refusalKind === 'no-pull'); assert('⭐ a-within-sibling-does-NOT-carry-an-oversized-PR-through-the-group', sizeRun([11, 22], { 11: 'oversized', 22: 'within' }).exitCode === EXIT_REFUSED_OVERSIZED); // The pull_request leg: not applicable, silent, read-free. - const sizePrLeg = sizeGuardVerdict({ event: EVENT_PULL_REQUEST, pulls: [5], readings: new Map() }); + const sizePrLeg = sizeGuardVerdict({ repo: REPO, event: EVENT_PULL_REQUEST, pulls: [5], readings: new Map() }); assert( 'the-pull_request-leg-is-NOT-APPLICABLE-exits-clear-and-renders-NOTHING-so-that-legs-output-stays-byte-identical', sizePrLeg.conclusion === 'not-applicable' && sizePrLeg.exitCode === EXIT_CLEAR && renderSizeVerdict(sizePrLeg) === '', ); let sizeReadsOnPr = 0; const sizePrRun = await runSizeGuard({ + repo: REPO, event: EVENT_PULL_REQUEST, rows: [queuedRow(5)], fetchPull: () => { @@ -3383,6 +3473,7 @@ export async function selfTest() { // End to end through the orchestrator: one pull read per queued PR, the enumeration `queuedPullsInGroup`'s. const sizeReads = []; const e2eSize = await runSizeGuard({ + repo: REPO, event: EVENT_MERGE_GROUP, rows: [queuedRow(11), queuedRow(22), queuedRow(11)], fetchPull: async (pr) => { @@ -3398,6 +3489,7 @@ export async function selfTest() { ); let sizeUrl = ''; const e2eReader = await runSizeGuard({ + repo: REPO, event: EVENT_MERGE_GROUP, rows: [queuedRow(42)], fetchPull: makePullReader({ @@ -3414,6 +3506,7 @@ export async function selfTest() { sizeUrl, ); const e2eSizeThrows = await runSizeGuard({ + repo: REPO, event: EVENT_MERGE_GROUP, rows: [queuedRow(11)], fetchPull: async () => { @@ -3425,7 +3518,7 @@ export async function selfTest() { e2eSizeThrows.exitCode === EXIT_REFUSED_SIZE_UNREADABLE && /403/.test(renderSizeVerdict(e2eSizeThrows)) && /REFUSED/.test(renderSizeVerdict(e2eSizeThrows)), renderSizeVerdict(e2eSizeThrows), ); - const e2eNoPair = await runSizeGuard({ event: EVENT_MERGE_GROUP, rows: [queuedRow(11)], fetchPull: async () => pull() }); + const e2eNoPair = await runSizeGuard({ repo: REPO, event: EVENT_MERGE_GROUP, rows: [queuedRow(11)], fetchPull: async () => pull() }); assert( '⛔ a-pull-object-with-no-pair-is-exit-9-too', e2eNoPair.exitCode === EXIT_REFUSED_SIZE_UNREADABLE && /no `additions` \/ `deletions` pair/.test(renderSizeVerdict(e2eNoPair)), @@ -3438,8 +3531,9 @@ export async function selfTest() { const explodeForSize = () => { throw new Error('the governed leg must not read for an ungoverned diff'); }; - const g18971 = await runGuard({ event: EVENT_MERGE_GROUP, rows: pr18971, fetchReviews: explodeForSize, fetchPull: explodeForSize, fetchComments: explodeForSize }); + const g18971 = await runGuard({ repo: REPO, event: EVENT_MERGE_GROUP, rows: pr18971, fetchReviews: explodeForSize, fetchPull: explodeForSize, fetchComments: explodeForSize }); const s18971 = await runSizeGuard({ + repo: REPO, event: EVENT_MERGE_GROUP, rows: pr18971, fetchPull: async () => pull(HEAD, { size: sizePair(238310, 119, 'GET /repos/objectstack-ai/objectstack/pulls/18971') }), @@ -3481,8 +3575,8 @@ export async function selfTest() { // A governed path AND oversized: both limbs print, one exit — the governed // code, already the top of the table. const bothRows = [row(9527, ['AGENTS.md', 'packages/x.ts'])]; - const gBoth = await runGuard({ event: EVENT_MERGE_GROUP, rows: bothRows, fetchReviews: async () => [], fetchPull: async () => pull(HEAD, { size: sizePair(LINE, 1) }), fetchComments: async () => [] }); - const sBoth = await runSizeGuard({ event: EVENT_MERGE_GROUP, rows: bothRows, fetchPull: async () => pull(HEAD, { size: sizePair(LINE, 1) }) }); + const gBoth = await runGuard({ repo: REPO, event: EVENT_MERGE_GROUP, rows: bothRows, fetchReviews: async () => [], fetchPull: async () => pull(HEAD, { size: sizePair(LINE, 1) }), fetchComments: async () => [] }); + const sBoth = await runSizeGuard({ repo: REPO, event: EVENT_MERGE_GROUP, rows: bothRows, fetchPull: async () => pull(HEAD, { size: sizePair(LINE, 1) }) }); assert( '⭐ governed-AND-oversized-prints-BOTH-limbs-and-exits-on-the-GOVERNED-code', gBoth.exitCode === EXIT_REFUSED_UNAPPROVED && sBoth.exitCode === EXIT_REFUSED_OVERSIZED && @@ -3491,6 +3585,7 @@ export async function selfTest() { JSON.stringify([gBoth.exitCode, sBoth.exitCode]), ); const gApprovedBig = await runGuard({ + repo: REPO, event: EVENT_MERGE_GROUP, rows: bothRows, fetchReviews: async () => [approvedAt(GOVERNED_APPROVERS[0], HEAD)], @@ -3502,6 +3597,7 @@ export async function selfTest() { // throws) — and the group exits 0 on both limbs. let sizeReviewReadsReused = 0; const sApprovedBig = await runSizeGuard({ + repo: REPO, event: EVENT_MERGE_GROUP, rows: bothRows, fetchPull: async () => pull(HEAD, { size: sizePair(LINE, 1) }), @@ -3521,6 +3617,7 @@ export async function selfTest() { // The register's lift lifts a PATH, never the number: rows whose every path // the register lifted still pay the size — and with no approval, still refuse. const liftedAll = await runSizeGuard({ + repo: REPO, event: EVENT_MERGE_GROUP, rows: [{ sha: 'f'.repeat(40), subject: 'x (#77)', pr: 77, paths: [] }], fetchPull: async () => pull(HEAD, { size: sizePair(237706, 0) }), @@ -3532,6 +3629,7 @@ export async function selfTest() { const PR20125_SHA = 'e4ead748' + '1'.repeat(32); const liftRun = (reviews, { size = sizePair(10764, 999, 'GET /repos/objectstack-ai/objectstack/pulls/20125'), sha = PR20125_SHA } = {}) => runSizeGuard({ + repo: REPO, event: EVENT_MERGE_GROUP, rows: bigUngoverned(), fetchPull: async () => pull(sha, { size }), @@ -3585,7 +3683,7 @@ export async function selfTest() { /review list could NOT be read \(GET \/repos\/o\/r\/pulls\/20125\/reviews answered HTTP 403\) — fail closed, no lift/.test(renderSizeVerdict(reviewsThrow)), renderSizeVerdict(reviewsThrow), ); - const noReader = await runSizeGuard({ event: EVENT_MERGE_GROUP, rows: bigUngoverned(), fetchPull: async () => pull(PR20125_SHA, { size: sizePair(LINE, 1) }) }); + const noReader = await runSizeGuard({ repo: REPO, event: EVENT_MERGE_GROUP, rows: bigUngoverned(), fetchPull: async () => pull(PR20125_SHA, { size: sizePair(LINE, 1) }) }); assert( '⛔ a-size-leg-run-with-NO-review-reader-at-all-fails-CLOSED-on-8-too', noReader.exitCode === EXIT_REFUSED_OVERSIZED && /no review reader was supplied/.test(renderSizeVerdict(noReader)), @@ -3606,7 +3704,7 @@ export async function selfTest() { // End to end, the card's acceptance: an oversized UNGOVERNED group with the // approval on record — the governed leg clear at zero reads, the size leg // lifted, the group exits 0; the same group with no approval exits 8. - const g20125 = await runGuard({ event: EVENT_MERGE_GROUP, rows: bigUngoverned(), fetchReviews: explodeForSize, fetchPull: explodeForSize, fetchComments: explodeForSize }); + const g20125 = await runGuard({ repo: REPO, event: EVENT_MERGE_GROUP, rows: bigUngoverned(), fetchReviews: explodeForSize, fetchPull: explodeForSize, fetchComments: explodeForSize }); assert( '⭐ #20125-end-to-end-governed-CLEAR-at-zero-reads-size-LIFTED-group-exits-0-and-with-no-approval-8', g20125.conclusion === 'clear' && g20125.apiCalls === 0 && groupExitCode({ governed: g20125, size: liftOnHead }) === EXIT_CLEAR && @@ -3614,16 +3712,16 @@ export async function selfTest() { ); assert( '⭐ sizeReading-has-FOUR-states-and-lifted-is-over-the-line-never-within', - sizeReading(1, sizePair(LINE, 1), authorizedApprovalVerdict([approvedAt('hotlong', HEAD)], HEAD)).state === 'lifted' && - sizeReading(1, sizePair(LINE, 0), authorizedApprovalVerdict([approvedAt('hotlong', HEAD)], HEAD)).state === 'within' && - sizeReading(1, sizePair(LINE, 1), authorizedApprovalVerdict([], HEAD)).state === 'oversized' && - sizeReading(1, null, authorizedApprovalVerdict([approvedAt('hotlong', HEAD)], HEAD)).state === 'unreadable', + sizeReading(1, sizePair(LINE, 1), REPO, authorizedApprovalVerdict([approvedAt('hotlong', HEAD)], HEAD)).state === 'lifted' && + sizeReading(1, sizePair(LINE, 0), REPO, authorizedApprovalVerdict([approvedAt('hotlong', HEAD)], HEAD)).state === 'within' && + sizeReading(1, sizePair(LINE, 1), REPO, authorizedApprovalVerdict([], HEAD)).state === 'oversized' && + sizeReading(1, null, REPO, authorizedApprovalVerdict([approvedAt('hotlong', HEAD)], HEAD)).state === 'unreadable', ); assert( '⭐ the-authorized-verdict-carries-approvals-login-and-commit-the-generic-one-does-NOT-so-only-the-verdict-that-asked-WHO-can-lift', JSON.stringify(authorizedApprovalVerdict([approvedAt('os-zhuang', OLD), approvedAt('not-authorized', HEAD)], HEAD).approvals) === JSON.stringify([{ login: 'os-zhuang', commitId: OLD }]) && !('approvals' in approvalVerdict([approvedAt('os-zhuang', HEAD)])) && - sizeReading(1, sizePair(LINE, 1), approvalVerdict([approvedAt('os-zhuang', HEAD)])).state === 'oversized', + sizeReading(1, sizePair(LINE, 1), REPO, approvalVerdict([approvedAt('os-zhuang', HEAD)])).state === 'oversized', ); // The exit precedence, the whole table: a group naming NO pull request // answers on 9 — both blocks still print their own refusal, and the code is @@ -3659,7 +3757,7 @@ export async function selfTest() { assert('⛔ this-file-declares-NO-threshold-of-its-own', sizeOwnSource !== '' && !/HUMAN_MERGE_LINE_THRESHOLD\s*=/.test(sizeOwnSource)); assert( '⛔ and-spells-NO-size-comparison-the-limb-is-reached-through-testVerdict-and-landsByHumanMerge', - !/(changedLines|additions\s*\+\s*deletions)\s*>=?\s*/.test(sizeOwnSource) && /testVerdict\(\[\], \{ size, approval \}\)/.test(sizeOwnSource) && /landsByHumanMerge\(verdict\)/.test(sizeOwnSource), + !/(changedLines|additions\s*\+\s*deletions)\s*>=?\s*/.test(sizeOwnSource) && /testVerdict\(\[\], \{ repo, size, approval \}\)/.test(sizeOwnSource) && /landsByHumanMerge\(verdict\)/.test(sizeOwnSource), ); assert( '⛔ and-spells-NO-lift-condition-of-its-own-the-lift-is-the-siblings-sizeLiftFrom-read-through-landsByHumanMerge', @@ -3852,17 +3950,17 @@ export async function selfTest() { const RULES_PATH = 'AGENTS.md'; const TIER_H_PATHS = ['AGENTS.md', 'CLAUDE.md', 'docs/adr/0001-x.md', 'docs/NORTH-STAR.md', 'skills/objectstack-ui/SKILL.md']; - assert('a-Tier-S-only-path-set-is-Tier-S', governedTierFor([REF_A, REF_B, SKILL_PATH, AGENT_PATH, HOOK_PATH, SETTINGS_PATH]) === TIER_S); - assert('⛔ the-old-references-boundary-is-GONE-SKILL-md-and-a-references-draft-sibling-are-Tier-S-alike', governedTierFor([SKILL_PATH]) === TIER_S && governedTierFor(['.claude/skills/pm-dispatch/references-draft/x.md']) === TIER_S); - assert('ONE-Tier-H-path-makes-the-WHOLE-entry-Tier-H', governedTierFor([REF_A, REF_B, RULES_PATH]) === TIER_H); - assert('each-Tier-H-surface-alone-is-Tier-H', TIER_H_PATHS.every((p) => governedTierFor([p]) === TIER_H), TIER_H_PATHS.join()); - assert('an-EMPTY-path-set-defaults-to-Tier-H-never-to-S', governedTierFor([]) === TIER_H && governedTierFor(undefined) === TIER_H); + assert('a-Tier-S-only-path-set-is-Tier-S', governedTierFor([REF_A, REF_B, SKILL_PATH, AGENT_PATH, HOOK_PATH, SETTINGS_PATH], REPO) === TIER_S); + assert('⛔ the-old-references-boundary-is-GONE-SKILL-md-and-a-references-draft-sibling-are-Tier-S-alike', governedTierFor([SKILL_PATH], REPO) === TIER_S && governedTierFor(['.claude/skills/pm-dispatch/references-draft/x.md'], REPO) === TIER_S); + assert('ONE-Tier-H-path-makes-the-WHOLE-entry-Tier-H', governedTierFor([REF_A, REF_B, RULES_PATH], REPO) === TIER_H); + assert('each-Tier-H-surface-alone-is-Tier-H', TIER_H_PATHS.every((p) => governedTierFor([p], REPO) === TIER_H), TIER_H_PATHS.join()); + assert('an-EMPTY-path-set-defaults-to-Tier-H-never-to-S', governedTierFor([], REPO) === TIER_H && governedTierFor(undefined, REPO) === TIER_H); assert('the-tier-constants-are-the-registers-own-two-distinct-values', TIER_H === GOVERNED_TIER_H && TIER_S === GOVERNED_TIER_S && TIER_H !== TIER_S); assert( 'the-tier-travels-on-the-decomposed-entry-so-the-verdict-never-re-derives-it', - decomposeGovernedWork([row(5, [REF_A])]).governed[0].tier === TIER_S && - decomposeGovernedWork([row(5, [AGENT_PATH])]).governed[0].tier === TIER_S && - decomposeGovernedWork([row(5, [REF_A, RULES_PATH])]).governed[0].tier === TIER_H, + decomposeGovernedWork([row(5, [REF_A])], REPO).governed[0].tier === TIER_S && + decomposeGovernedWork([row(5, [AGENT_PATH])], REPO).governed[0].tier === TIER_S && + decomposeGovernedWork([row(5, [REF_A, RULES_PATH])], REPO).governed[0].tier === TIER_H, ); // The real recognisers, and the tier VALUE read from the constant's one home @@ -3920,6 +4018,7 @@ export async function selfTest() { tierApiCalls = 0; tierThreadsRead = []; return runGuard({ + repo: REPO, event, rows: [row(pr, files, 'c'.repeat(40), `x (#${pr})`)], fetchReviews: async () => { tierApiCalls += 1; return reviews; }, @@ -4143,6 +4242,7 @@ export async function selfTest() { // references PR an authorized approver approved clears on the approval, and // the thread is never read — measured with a spy that THROWS if it is. const monotone = await runGuard({ + repo: REPO, event: EVENT_MERGE_GROUP, rows: [row(71, [REF_A])], fetchReviews: async () => [approvedAt(GOVERNED_APPROVERS[1], REF_HEAD)], @@ -4196,6 +4296,7 @@ export async function selfTest() { // The `pull_request` leg is untouched: no thread read, no record key, and the // early warning still never reddens. const tierOnPr = await runGuard({ + repo: REPO, event: EVENT_PULL_REQUEST, rows: [row(72, [REF_A])], fetchReviews: async () => [], @@ -4209,6 +4310,7 @@ export async function selfTest() { ); // ⭐ The ordering guarantee, extended: a clear diff does not even LOAD them. const clearWithSpies = await runGuard({ + repo: REPO, event: EVENT_MERGE_GROUP, rows: [row(73, ['packages/spec/src/index.ts'])], fetchReviews: async () => { throw new Error('no review lookup may be made on a clear diff'); }, @@ -4278,7 +4380,7 @@ export async function selfTest() { ); assert( '⛔ and-this-leg-added-NO-surface-to-the-register-the-tier-is-a-landing-rule-not-a-membership-one', - governedPathsIn([REF_A]).length === 1 && governedPathsIn([RULES_PATH]).length === 1, + governedPathsIn([REF_A], REPO).length === 1 && governedPathsIn([RULES_PATH], REPO).length === 1, ); @@ -4399,7 +4501,7 @@ export async function selfTest() { 'throwing spies, an approved references PR never reads a thread and never loads a recogniser — and the ' + '#19036 SIZE line (the 2026-09-18 ruling, landed seat-side by #19033 and carried to the queue here): every ' + 'queued pull request\'s `additions` / `deletions` read off the same pull object the head read uses, one read ' + - 'per PR, judged by the sibling\'s IMPORTED predicate (`testVerdict([], { size })` + `landsByHumanMerge`; this ' + + 'per PR, judged by the sibling\'s IMPORTED predicate (`testVerdict([], { repo: REPO, size })` + `landsByHumanMerge`; this ' + 'file declares no threshold and spells no comparison, pinned against its own source) — the #18971 replay ' + '(over the line, nothing governed) REFUSED on exit 8 with the two numbers, the threshold and the human-merge ' + 'remedy printed; exactly the threshold clear; a governed AND oversized group printing both limbs and exiting ' + diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 42b37098cfa..a92214db878 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -9746,7 +9746,7 @@ export function governedRegisterFailure(err) { * both specifiers relative to THIS file, which is what makes the sibling-repo * absence a catchable `ERR_MODULE_NOT_FOUND` rather than a load-time death. */ -export async function loadGovernedRegister(load = (specifier) => import(specifier)) { +export async function loadGovernedRegister(load = (specifier) => import(specifier), slug = OWNER_REPO) { try { const [merges, guard] = await Promise.all([ load('./check-governed-merges.mjs'), @@ -9756,10 +9756,12 @@ export async function loadGovernedRegister(load = (specifier) => import(specifie const tierOf = merges?.landingTierOf; const recordTier = merges?.GOVERNED_TIER_S; const approvers = guard?.GOVERNED_APPROVERS; + const repoIdForSlug = merges?.repoIdForSlug; if ( typeof matcher !== 'function' || typeof tierOf !== 'function' || typeof recordTier !== 'string' || + typeof repoIdForSlug !== 'function' || !Array.isArray(approvers) || approvers.length === 0 ) { @@ -9769,13 +9771,35 @@ export async function loadGovernedRegister(load = (specifier) => import(specifie tierOf: null, recordTier: null, approvers: [], + repo: null, reason: 'the governed register loaded but did not export what this row reads — `governedPathsIn`, ' + - '`landingTierOf`, `GOVERNED_TIER_S` and a non-empty `GOVERNED_APPROVERS`. A rename is the ' + - 'likely cause, and this row is the reader that noticed', + '`landingTierOf`, `GOVERNED_TIER_S`, `repoIdForSlug` and a non-empty `GOVERNED_APPROVERS`. A rename ' + + 'is the likely cause, and this row is the reader that noticed', }; } - return { available: true, matcher, tierOf, recordTier, approvers, reason: null }; + // The register is scoped PER ROW (maintainer ruling B2, 2026-10-13): its + // question has a repository in it, and this patrol's is the board it + // sweeps. Bound ONCE here, so H43 and H48 ask `matcher(paths)` about the + // swept board and never default to every repository; a board the register + // does not know is NOT MEASURED, with the board named — never judged + // against another repository's rows, which would read like a clean board. + const repo = repoIdForSlug(slug); + if (repo === null) { + return { + available: false, + matcher: null, + tierOf: null, + recordTier: null, + approvers: [], + repo: null, + reason: + `the swept board ${slug} is not a governed repository in the register's GOVERNED_REPOS, so no register ` + + 'row is scoped to it and the governed question has no answer here — NOT MEASURED on this board; every ' + + 'other row is unaffected', + }; + } + return { available: true, matcher: (paths) => matcher(paths, repo), tierOf, recordTier, approvers, repo, reason: null }; } catch (err) { return { available: false, @@ -9783,6 +9807,7 @@ export async function loadGovernedRegister(load = (specifier) => import(specifie tierOf: null, recordTier: null, approvers: [], + repo: null, reason: governedRegisterFailure(err), }; } @@ -30820,6 +30845,21 @@ async function selfTest() { t('H43 register: …and says the rest of the sweep is unaffected', governedRegisterFailure({ code: 'ERR_MODULE_NOT_FOUND' }).includes('every other row is unaffected'), true); t('H43 register: any other load failure carries its own message', governedRegisterFailure(new Error('boom')).includes('boom'), true); + // The per-row scope (ruling B2, 2026-10-13): the register's question carries + // the swept board, bound ONCE at load. Driven through the real modules with + // an explicit board each, so the reading does not depend on which board this + // self-test happens to run against. + const realLoad = (specifier) => import(specifier); + const boundCloud = await loadGovernedRegister(realLoad, 'objectstack-ai/cloud'); + const boundOs = await loadGovernedRegister(realLoad, 'objectstack-ai/objectos'); + const boundForeign = await loadGovernedRegister(realLoad, 'someone/fork'); + t('H43 register: the loader binds the swept board\'s repository id, and this run\'s is the board it sweeps', + GOVERNED_REGISTER.available ? GOVERNED_REGISTER.repo : 'NOT MEASURED', + GOVERNED_REGISTER.available ? (await realLoad('./check-governed-merges.mjs')).repoIdForSlug(OWNER_REPO) : 'NOT MEASURED'); + t('H43 register: a workflow path is governed on a cloud board (the scoped row names cloud)', boundCloud.available && boundCloud.repo === 'cloud' && boundCloud.matcher(['.github/workflows/ci.yml']).length === 1, true); + t('H43 register: …and NOT on an objectos board, while the default rows still govern it', boundOs.available && boundOs.repo === 'objectos' && boundOs.matcher(['.github/workflows/ci.yml']).length === 0 && boundOs.matcher(['AGENTS.md']).length === 1, true); + t('H43 register: a board outside GOVERNED_REPOS is NOT MEASURED with the board named, never judged against another repository\'s rows', !boundForeign.available && boundForeign.matcher === null && boundForeign.repo === null && /someone\/fork/.test(boundForeign.reason) && /NOT MEASURED on this board/.test(boundForeign.reason), true); + // The TIER, read off the REAL register's own slices rather than a fixture // that restates which surfaces are which: a PR whose governed paths all lie // on the record row (`.claude/**`, Tier S) lands on the owning seat's diff --git a/scripts/pm/record-recognisers.mjs b/scripts/pm/record-recognisers.mjs index fd7fc48f9ca..ef05050a8ff 100644 --- a/scripts/pm/record-recognisers.mjs +++ b/scripts/pm/record-recognisers.mjs @@ -1309,6 +1309,9 @@ export async function selfTest() { // of them reddens. battery('#18701: ONE thread set -- what the template STATES is what the queue guard READS'); const GUARD = await import('./check-governed-queue-guard.mjs'); + // The register is scoped per row, so every guard question names its + // repository; this pin drives the guard for THIS repository's own queue. + const { SELF_REPO_ID: GUARD_REPO } = await import('./check-governed-merges.mjs'); const TEMPLATE_TEXT = contractReviewTemplateLines().join('\n'); const THREAD_SET = REVIEW_OF_RECORD_THREADS.map((thread) => thread.where).join(); @@ -1331,11 +1334,12 @@ export async function selfTest() { // below asks the register, so a row moving tiers reddens here instead of // silently driving the approval leg. const TIER_S_PATH = '.claude/skills/pm-dispatch/references/contract-review.md'; - t('⛔ CONTROL: the fixture path is Tier S under the register, so the record leg is the one being driven', GUARD.governedTierFor([TIER_S_PATH]) === GUARD.TIER_S); + t('⛔ CONTROL: the fixture path is Tier S under the register, so the record leg is the one being driven', GUARD.governedTierFor([TIER_S_PATH], GUARD_REPO) === GUARD.TIER_S); const pinRun = async (where) => { const threadsRead = []; const verdict = await GUARD.runGuard({ event: GUARD.EVENT_MERGE_GROUP, + repo: GUARD_REPO, rows: [{ sha: 'e'.repeat(40), subject: `x (#${PIN_PR})`, pr: PIN_PR, paths: [TIER_S_PATH] }], fetchReviews: async () => [], fetchPull: async () => ({ sha: PIN_HEAD, body: `Fixes #${PIN_CARD}`, headRef: `claude/issue-${PIN_CARD}-x` }),