Skip to content

Commit 0b5ab8b

Browse files
fix(pm): the queue guard's pull_request leg attributes to the payload PR and reads the net diff (#26568)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8QLWm3LrvmyaxGxb5T2J2Qf)_ Fixes #26316 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:`scripts/pm/check-governed-queue-guard.mjs` 的 `pull_request` 腿不再按提交拆行:整段 diff 读成一行「合并基点 → head 的净差异」,归到事件载荷里的 PR 号。`merge_group` 腿(按首父提交拆、按标题 `(#N)` 归属、各种拒绝)一字不改。自测加 8 例,两组电池下限按实测抬高。 - **为什么改**:提交标题写的是 issue 号时,预警去读那个 issue 的 reviews(404),报「review list could NOT be read」;分支合过 `origin/main` 时,合并提交的首父 diff 把 main 已落地的几十个路径算进预警,且每行都是「无归属」。预警本来就不会变红,但读的人会被误导。 - **风险与回滚**:只改 PR 上的预警输出,队列腿的输出在两个夹具上逐字节相同(下方附哈希);回滚即 revert 本 PR。 - **先看**:① `enumerateRows`(L2386 起)新的 `pull_request` 分支;② `readEventDiff` 那一行调用(L2794)把事件传进去;③ 自测 decomposition 电池里的 7 个新用例。 - **两分钟验证**:`node scripts/pm/check-governed-queue-guard.mjs --self-test` → 末行 `✓ check-governed-queue-guard self-test: 401 cases pass`(改前 393)。 - **需要你做的**:无,席位落地。 ## What changes Card #26316's 「Wanted」, and nothing wider. One file, `scripts/pm/check-governed-queue-guard.mjs`: 1. **`enumerateRows` reads by event.** The `merge_group` reading is the old one, byte for byte: one row per first-parent commit, attributed by `pullNumberFromSubject`, with the `head_ref` fallback only for a one-commit group. On the `pull_request` leg it now returns ONE row: `diff-tree baseSha headSha` (the caller already hands in the merge-base), attributed to the payload's own pull request number, carrying `commits` = the range's first-parent count. Shape chosen: the single net row, because it makes both negatives provable with no intersection logic. The subject is not read for attribution on this leg at all; a subject naming an issue cannot redirect the review read, and a merge of `origin/main` cannot add a path the net diff does not hold. 2. **`readEventDiff` passes `{ event: context.event }`** to it. The merge-base call it already made (L2793) is unchanged; the premise reading in the claim held: the missing piece was the per-commit enumeration, not a merge-base. 3. **`commitsInRange(rows)`** keeps the header line honest: `pull request #N — 3 commit(s) in range` still counts the range's commits. On `merge_group` rows carry no `commits`, so it equals `rows.length`, the old count. 4. **A `runGit(args)` seam** on `enumerateRows`, default `null` (production reads the checkout as before), so the self-test drives the enumeration offline, as its header promises (no network, no git). 5. A header paragraph under *Multi-PR merge groups* states that the `pull_request` leg is not decomposed, and why. Not touched: `.github/workflows/governed-surface-guard.yml`, `REQUIRED_CONTEXTS`, the register, any refusal text, `decomposeGovernedWork`, `queuedPullsInGroup`, `runSizeGuard`. ## Tests Self-test 393 → 401 cases, exit 0. Eight new cases, all driven through the `runGit` seam: - **Battery `decomposition, and the multi-PR group trap`** (floor 6 → 13, measured 13): - `⭐ pull_request-leg: a-subject-naming-an-ISSUE-is-attributed-to-the-PAYLOAD-pr-never-the-issue`: the PR #25965 shape (one commit, subject ends `(#25770)`, payload PR 25965) gives one row with PR 25965, governed entry 25965, zero unattributed. - `⛔ CONTROL merge_group-leg: the-same-subject-still-attributes-by-its-trailing-number-unchanged`: the same range on `merge_group` still reads 25770 from the subject. - `⭐ pull_request-leg: a-branch-that-merged-origin-main-carries-ONLY-its-net-rows-against-the-merge-base`: the PR #26311 shape (own commit, merge of main carrying `.github/workflows/lint.yml`, `AGENTS.md`, `packages/core/src/x.ts`, own commit) gives one governed entry, PR 26311, paths exactly `.github/workflows/governed-surface-guard.yml`. - `and-it-reads-the-net-diff-base-to-head-never-a-per-commit-first-parent-diff`: the recorded git reads hold the two-tree `diff-tree` and no `--first-parent` `diff-tree`. - `and-the-header-still-counts-the-ranges-three-commits`. - `⛔ CONTROL merge_group-leg: the-same-range-is-still-one-row-per-first-parent-commit-unattributed-without-a-subject-number`: three rows, all PR null, the merge row still carries `AGENTS.md`. - `and-an-empty-pull_request-range-is-no-rows`. - **Battery `the pull_request leg is an EARLY WARNING and never reddens`** (floor 5 → 6, measured 6): - `⭐ the-early-warning-names-the-payloads-pull-request-and-never-the-issue-its-subject-names`: the rendered warning carries `#25965 — governed` and no `#25770`. Floor binding checked once: with the decomposition floor at 14 the run exits 1 naming `registered 13 case(s), below its pinned floor of 14`. The self-test's success line is otherwise identical before and after; only the count moves (393 → 401). `SELF_TEST_BATTERY_FLOOR` (29) is unchanged: no battery added. ## Acceptance notes The card's three acceptance lines: - *A `pull_request` run on a PR whose commit subjects name an issue number prints the PR's own review state*: yes, the fixture run below prints `#25965 — governed` where it printed `#25770 — governed` before. - *A PR whose branch merged `origin/main` prints only the PR's net rows*: yes, 3 unattributed rows (one of them the merge commit's 7 workflow paths) became 1 governed entry with 1 path. - *`--self-test` green; the `merge_group` cases unchanged*: 401 pass; every pre-existing case passes unedited; the live `merge_group` output on both fixtures is byte-identical before and after (below). This PR's own `pull_request` run is a live case of the second line: its branch carries a merge of `origin/main` (25 commits of main), so the guard's check-run on this PR should read `pull request #THIS — 2 commit(s) in range` and name no governed path (the diff touches none). ## Verification Fixtures: a throwaway worktree off `d2ca7079` with two branches built to the card's shapes; payloads point the guard at them; reviews unreachable on purpose (API URL at a closed local port) so the output is deterministic. "Before" = the guard at `d2ca7079`; "after" = this branch. <details><summary>PR #25965 shape: one commit, subject names issue 25770</summary> Before: ```text pull request #25965 — 1 commit(s) in range Governed Surface Queue Guard — pull_request — 1 governed pull request(s), 0 unattributed governed commit(s), 1 review lookup(s). #25770 — governed: AGENTS.md ×1 — the repo-root agent instruction file ``` After: ```text pull request #25965 — 1 commit(s) in range Governed Surface Queue Guard — pull_request — 1 governed pull request(s), 0 unattributed governed commit(s), 1 review lookup(s). #25965 — governed: AGENTS.md ×1 — the repo-root agent instruction file ``` </details> <details><summary>PR #26311 shape: own commit, merge of main, own commit</summary> Before: ```text pull request #26311 — 3 commit(s) in range Governed Surface Queue Guard — pull_request — 0 governed pull request(s), 3 unattributed governed commit(s), 0 review lookup(s). ⛔ UNATTRIBUTED — commit 18743a82d8c2 … the workflow commit - .github/workflows/governed-surface-guard.yml ⛔ UNATTRIBUTED — commit d08da36b536b … Merge commit 'd2ca7079…' into fx-b - .github/workflows/board-snapshot.yml - .github/workflows/fleet-patrols.yml - .github/workflows/lint.yml - .github/workflows/platform-checklist-watchdog.yml - .github/workflows/release-coverage-patrol.yml - .github/workflows/required-set-patrol.yml - .github/workflows/governed-surface-guard.yml ⛔ UNATTRIBUTED — commit 57cfc2aac768 … pin the self-test step - .github/workflows/governed-surface-guard.yml ``` After: ```text pull request #26311 — 3 commit(s) in range Governed Surface Queue Guard — pull_request — 1 governed pull request(s), 0 unattributed governed commit(s), 1 review lookup(s). #26311 — governed: .github/workflows/** ×1 — CI, deploy and release workflows: what runs on every push, what deploys, what gates the queue - .github/workflows/governed-surface-guard.yml ``` </details> <details><summary>merge_group leg, same two ranges: byte-identical</summary> `cmp` of before and after, each run with a `merge_group` payload over the same range (`head_ref` naming the PR): ```text merge_group fixture a: byte-identical (7529 bytes, sha256 04261d5a4b8e70cb) merge_group fixture b: byte-identical (7666 bytes, sha256 dd83c48111ca9ab5) ``` Fixture b on the queue leg still reads `3 commit(s) in range` and `3 unattributed governed commit(s)`: the queue's decomposition and its refusal are untouched. </details> <details><summary>--toolchain-needed on the pull_request leg</summary> ```text Governed Surface Queue Guard — generator toolchain not needed: 1 commit(s) in range touch no register path the lift recomputes Governed Surface Queue Guard — generator toolchain not needed: 3 commit(s) in range touch no register path the lift recomputes ``` </details> <details><summary>Local gates on 64d0d4f: 32 derived, 32 run, all exit 0</summary> Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `64d0d4fd` (branch with `origin/main` at `a3009cbf` merged in, so the tree is not stale against the families). Each run's exit code captured before any pipe; `check:pm-dispatch-gates` ran under `os-verify-lock.sh` (slot `issue-26316`): `VERDICT command-exit 0 · held the lock 380s`. - `node scripts/check-closing-keyword-parity.mjs` → exit 0 - `node scripts/check-closing-keyword-parity.mjs --self-test` → exit 0 - `node scripts/check-comment-mask-corpus.mjs` → exit 0 - `node scripts/check-declaration-mirrors.mjs` → exit 0 - `node scripts/check-declaration-mirrors.mjs --self-test` → exit 0 - `node scripts/check-self-test-wired.mjs` → exit 0 - `node scripts/check-self-test-wired.mjs --self-test` → exit 0 - `node scripts/check-self-test-workflow-commands.mjs` → exit 0 - `node scripts/check-self-test-workflow-commands.mjs --self-test` → exit 0 - `node scripts/check-whole-set-label-write.mjs` → exit 0 - `node scripts/check-whole-set-label-write.mjs --self-test` → exit 0 - `node scripts/ci/plan-test-core-shards.mjs --self-test` → exit 0 - `node scripts/pm/bare-root-worklist.mjs --self-test` → exit 0 - `node scripts/pm/check-governed-queue-guard.mjs --self-test` → exit 0 - `pnpm check:agent-test-spelling` → exit 0 - `pnpm check:bash32-floor` → exit 0 - `pnpm check:ci-filter-parity` → exit 0 - `pnpm check:cli-command-ids` → exit 0 - `pnpm check:closing-target-claim` → exit 0 - `pnpm check:cross-package-test-inputs` → exit 0 - `pnpm check:driver-memory-census` → exit 0 - `pnpm check:entry-guard` → exit 0 - `pnpm check:gitlink-declared` → exit 0 - `pnpm check:nul-bytes` → exit 0 - `pnpm check:parse-guard` → exit 0 - `pnpm check:pm-dispatch-gates` → exit 0 - `pnpm check:pnpm-filter-targets` → exit 0 - `pnpm check:pnpm-flag-spellings` → exit 0 - `pnpm check:ratchet-remedy-authority` → exit 0 - `pnpm check:refd-timer-probe` → exit 0 - `pnpm check:scripts-symbol-anchors` → exit 0 - `pnpm check:watch-hint-literal` → exit 0 `--ran` reconciliation (exit codes recorded): `Run reconciliation — 32 derived, 32 run, 0 NOT-MEASURED, 0 UNRUN.` · `✓ dispatch-gates --ran: 32 derived famil(ies) accounted for — 32 run, 0 NOT-MEASURED (a DERIVED zero — all 32 recorded an exit code and none of them is 3).` That reading was taken with `origin/main` at `1c632311`, 2 commits past the merge; the one derived-from file those moved is `scripts/engine-double-contract.pinned.json`, which no family on this card's list reads. Left to CI, as the derivation names: the workflow-payload families (`check:closing-target-claim`'s live leg), `ci.yml · Plan Test Core shards`, the type-check program steps, the repo-wide `pnpm lint`. No package was touched, so there is no build closure and no package test or typecheck to run; this script has no `*.test.ts` beside it, and its own `--self-test` is the suite. </details> ## Human Minimum Each item is one rule from [CONTRIBUTING.md › Human Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum). - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema change.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` is CI's run (repo-wide scan). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. Nothing publishes (`scripts/pm/**`, `skip-changeset`); both declaration lines are filled in above. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the required exception). --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxba3WdR4pYYA7CdMgAJ)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5d2935a commit 0b5ab8b

1 file changed

Lines changed: 143 additions & 12 deletions

File tree

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

Lines changed: 143 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,13 @@
234234
* `head_ref`'s PR number is used as a fallback only when the group holds
235235
* exactly one commit, where it is unambiguous.
236236
*
237+
* The `pull_request` leg is NOT decomposed: its payload names the one pull
238+
* request being judged, so every governed path is attributed to that number
239+
* (a subject naming an issue would read the issue's reviews), and the paths
240+
* are the net diff against the merge-base (a merge of `origin/main` on the
241+
* branch would otherwise forecast every path `main` already landed). See
242+
* `enumerateRows`; the `merge_group` reading above is unchanged by it.
243+
*
237244
* ## The generated-artifact exception is honoured, and that is not optional
238245
*
239246
* A generator-owned file sitting inside a governed surface makes routine
@@ -606,9 +613,9 @@ const SELF_TEST_BATTERIES = Object.freeze({
606613
'the approval predicate': 3,
607614
'⭐ The fail-open direction a naive `.some(r => r.state === \'APPROVED\')`': 5,
608615
'the 2026-09-04 unpinned predicate (the queue leg\'s)': 12,
609-
'decomposition, and the multi-PR group trap': 6,
616+
'decomposition, and the multi-PR group trap': 13,
610617
'the verdict table, both events': 10,
611-
'the pull_request leg is an EARLY WARNING and never reddens': 5,
618+
'the pull_request leg is an EARLY WARNING and never reddens': 6,
612619
'the replay fixtures: the three incidents this guard descends from': 11,
613620
'⭐ the ordering guarantee, measured with a spy that THROWS': 7,
614621
'the words a reader acts on (requirement (e))': 26,
@@ -2376,32 +2383,72 @@ function hasRev(root, rev) {
23762383
}
23772384

23782385
/**
2379-
* The first-parent commits between `baseSha` and `headSha`, each with the
2380-
* paths it changed and the pull request its subject names. One row per PR
2381-
* landing in a merge group; see the header on why the group is decomposed
2382-
* rather than keyed to `head_ref`.
2386+
* The rows a diff is judged on, read two ways — one per event, because the two
2387+
* events ask different questions of the same range.
2388+
*
2389+
* `merge_group` (the default): the first-parent commits between `baseSha` and
2390+
* `headSha`, each with the paths it changed and the pull request its subject
2391+
* names. One row per PR landing in a merge group; see the header on why the
2392+
* group is decomposed rather than keyed to `head_ref`.
2393+
*
2394+
* `pull_request`: ONE row — the net diff `baseSha..headSha` (the caller hands
2395+
* in the merge-base), attributed to the payload's own pull request number,
2396+
* which the event names outright. A subject is no evidence here: a commit
2397+
* subject naming an ISSUE (`fix: … (#<issue>)`) would read that issue's
2398+
* reviews, and a branch that merged `origin/main` would carry, on its merge
2399+
* commit's first-parent diff, every path `main` already landed. The net diff
2400+
* against the merge-base holds only what the pull request itself changes.
2401+
* `commits` keeps the range's first-parent commit count for the header line.
2402+
*
2403+
* `runGit(args)` is the seam the self-test drives; production passes none and
2404+
* reads the checkout at `root`.
23832405
*/
2384-
export function enumerateRows(root, baseSha, headSha, fallbackPull = null) {
2385-
const log = git(root, ['log', '--first-parent', '--format=%H%x09%s', `${baseSha}..${headSha}`]);
2406+
export function enumerateRows(root, baseSha, headSha, fallbackPull = null, { event = EVENT_MERGE_GROUP, runGit = null } = {}) {
2407+
const readGit = runGit ?? ((args) => git(root, args));
2408+
const log = readGit(['log', '--first-parent', '--format=%H%x09%s', `${baseSha}..${headSha}`]);
23862409
const commits = log
23872410
.split('\n')
23882411
.filter((l) => l !== '')
23892412
.map((l) => {
23902413
const [sha, ...rest] = l.split('\t');
23912414
return { sha, subject: rest.join('\t') };
23922415
});
2416+
if (event === EVENT_PULL_REQUEST) {
2417+
if (commits.length === 0) return [];
2418+
return [
2419+
{
2420+
sha: headSha,
2421+
subject: commits[0].subject,
2422+
pr: fallbackPull,
2423+
commits: commits.length,
2424+
paths: readGit(['diff-tree', '-r', '--no-commit-id', '--no-renames', '--name-only', baseSha, headSha])
2425+
.split('\n')
2426+
.filter((p) => p !== ''),
2427+
},
2428+
];
2429+
}
23932430
return commits.map((commit) => ({
23942431
...commit,
23952432
// The fallback is only unambiguous when the range holds exactly one
23962433
// commit; in a multi-PR group `head_ref` names the LAST PR, and applying
23972434
// it to an earlier commit attributes a governed diff to the wrong PR.
23982435
pr: pullNumberFromSubject(commit.subject) ?? (commits.length === 1 ? fallbackPull : null),
2399-
paths: git(root, ['diff-tree', '-r', '--no-commit-id', '--no-renames', '--name-only', '-m', '--first-parent', commit.sha])
2436+
paths: readGit(['diff-tree', '-r', '--no-commit-id', '--no-renames', '--name-only', '-m', '--first-parent', commit.sha])
24002437
.split('\n')
24012438
.filter((p) => p !== ''),
24022439
}));
24032440
}
24042441

2442+
/**
2443+
* How many commits the read range held — the header line's count. A
2444+
* `pull_request` net row carries its range's count; a `merge_group` reading
2445+
* is one row per commit, so the row count IS the commit count there.
2446+
*/
2447+
export function commitsInRange(rows) {
2448+
const list = Array.isArray(rows) ? rows : [];
2449+
return list.reduce((n, row) => n + (Number.isInteger(row?.commits) ? row.commits : 1), 0);
2450+
}
2451+
24052452
/**
24062453
* Does any row touch a register path the lift would RECOMPUTE? Pure, and THE
24072454
* one predicate for it: `liftGeneratedExceptions` consults the recompute iff
@@ -2744,7 +2791,7 @@ function readEventDiff(env) {
27442791

27452792
try {
27462793
const mergeBase = git(repoRoot, ['merge-base', context.baseSha, context.headSha]).trim();
2747-
const rows = enumerateRows(repoRoot, mergeBase, context.headSha, context.namedPull);
2794+
const rows = enumerateRows(repoRoot, mergeBase, context.headSha, context.namedPull, { event: context.event });
27482795
return { ok: true, context, where, mergeBase, rows };
27492796
} catch (error) {
27502797
return { ok: false, message: `⛔ ${CHECK_CONTEXT_NAME}: could not read the diff (${String(error?.message ?? error).split('\n')[0]}).` };
@@ -2775,7 +2822,7 @@ export function toolchainNeededAnswer({ eventName, read }) {
27752822
const hits = [...new Set(read.rows.flatMap((r) => r.paths).filter((p) => generatedExceptionFor(p) !== null))];
27762823
return { needed: true, why: `the diff touches ${hits.length} register path(s) the lift recomputes: ${hits.join(', ')}` };
27772824
}
2778-
return { needed: false, why: `${read.rows.length} commit(s) in range touch no register path the lift recomputes` };
2825+
return { needed: false, why: `${commitsInRange(read.rows)} commit(s) in range touch no register path the lift recomputes` };
27792826
}
27802827

27812828
async function main() {
@@ -2840,7 +2887,7 @@ async function main() {
28402887
});
28412888
const sizeBlock = renderSizeVerdict(size);
28422889
const report = [
2843-
`${context.label} — ${rows.length} commit(s) in range`,
2890+
`${context.label} — ${commitsInRange(read.rows)} commit(s) in range`,
28442891
...notes,
28452892
renderGuardVerdict(verdict),
28462893
...(sizeBlock === '' ? [] : ['', sizeBlock]),
@@ -3176,6 +3223,77 @@ export async function selfTest() {
31763223
const unattributed = decomposeGovernedWork([{ sha: 'c'.repeat(40), subject: 'chore: direct work', pr: null, paths: ['CLAUDE.md'] }], REPO);
31773224
assert('a-governed-commit-naming-no-pr-is-UNATTRIBUTED-never-dropped', unattributed.unattributed.length === 1 && unattributed.governed.length === 0);
31783225
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);
3226+
// The enumeration itself, through its git seam. Two branch shapes: one
3227+
// commit whose subject names an ISSUE (the PR #25965 shape), and a branch
3228+
// that merged `origin/main` between its own two commits (the PR #26311
3229+
// shape). The fake answers only the three reads `enumerateRows` makes, and
3230+
// records them, so the pull_request leg is measured to read the NET diff.
3231+
const EB = 'b'.repeat(40);
3232+
const enumGit = (log, perCommit, net) => {
3233+
const reads = [];
3234+
const fake = (args) => {
3235+
reads.push(args.join(' '));
3236+
if (args[0] === 'log') return log.map(([sha, subject]) => `${sha}\t${subject}`).join('\n') + '\n';
3237+
if (args[0] === 'diff-tree' && args.includes('--first-parent')) return (perCommit[args[args.length - 1]] ?? []).join('\n') + '\n';
3238+
if (args[0] === 'diff-tree') return net.join('\n') + '\n';
3239+
throw new Error(`unexpected git read: ${args.join(' ')}`);
3240+
};
3241+
return { fake, reads };
3242+
};
3243+
const ISSUE_HEAD = '1'.repeat(40);
3244+
const issueBranch = () => enumGit([[ISSUE_HEAD, 'fix(pm): the main-line script reads the lock slot (#25770)']], { [ISSUE_HEAD]: ['AGENTS.md'] }, ['AGENTS.md']);
3245+
const MERGED_HEAD = '2'.repeat(40);
3246+
const MERGE_SHA = '3'.repeat(40);
3247+
const FIRST = '4'.repeat(40);
3248+
const mergedBranch = () =>
3249+
enumGit(
3250+
[[MERGED_HEAD, 'ci(governed-surface-guard): the workflow commit'], [MERGE_SHA, "Merge branch 'main' into claude/issue-26201-x"], [FIRST, 'ci(governed-surface-guard): pin the self-test step']],
3251+
{ [MERGED_HEAD]: ['.github/workflows/governed-surface-guard.yml'], [MERGE_SHA]: ['.github/workflows/lint.yml', 'AGENTS.md', 'packages/core/src/x.ts'], [FIRST]: ['.github/workflows/governed-surface-guard.yml'] },
3252+
['.github/workflows/governed-surface-guard.yml'],
3253+
);
3254+
{
3255+
const g = issueBranch();
3256+
const prRows = enumerateRows('/w', EB, ISSUE_HEAD, 25965, { event: EVENT_PULL_REQUEST, runGit: g.fake });
3257+
const prWork = decomposeGovernedWork(prRows, REPO);
3258+
assert(
3259+
'⭐ pull_request-leg: a-subject-naming-an-ISSUE-is-attributed-to-the-PAYLOAD-pr-never-the-issue',
3260+
prRows.length === 1 && prRows[0].pr === 25965 && prWork.governed.map((e) => e.pr).join() === '25965' && prWork.unattributed.length === 0,
3261+
JSON.stringify(prRows),
3262+
);
3263+
const qg = issueBranch();
3264+
const queueRows = enumerateRows('/w', EB, ISSUE_HEAD, 26000, { event: EVENT_MERGE_GROUP, runGit: qg.fake });
3265+
assert(
3266+
'⛔ CONTROL merge_group-leg: the-same-subject-still-attributes-by-its-trailing-number-unchanged',
3267+
queueRows.length === 1 && queueRows[0].pr === 25770 && queueRows[0].sha === ISSUE_HEAD,
3268+
JSON.stringify(queueRows),
3269+
);
3270+
}
3271+
{
3272+
const g = mergedBranch();
3273+
const prRows = enumerateRows('/w', EB, MERGED_HEAD, 26311, { event: EVENT_PULL_REQUEST, runGit: g.fake });
3274+
const prWork = decomposeGovernedWork(prRows, REPO);
3275+
assert(
3276+
'⭐ pull_request-leg: a-branch-that-merged-origin-main-carries-ONLY-its-net-rows-against-the-merge-base',
3277+
prRows.length === 1 && prRows[0].paths.join() === '.github/workflows/governed-surface-guard.yml' &&
3278+
prWork.governed.length === 1 && prWork.governed[0].pr === 26311 && prWork.governed[0].paths.join() === '.github/workflows/governed-surface-guard.yml' &&
3279+
prWork.unattributed.length === 0,
3280+
JSON.stringify(prRows),
3281+
);
3282+
assert(
3283+
'and-it-reads-the-net-diff-base-to-head-never-a-per-commit-first-parent-diff',
3284+
g.reads.some((r) => r === `diff-tree -r --no-commit-id --no-renames --name-only ${EB} ${MERGED_HEAD}`) && !g.reads.some((r) => r.includes('--first-parent') && r.startsWith('diff-tree')),
3285+
g.reads.join(' | '),
3286+
);
3287+
assert('and-the-header-still-counts-the-ranges-three-commits', commitsInRange(prRows) === 3, String(commitsInRange(prRows)));
3288+
const qg = mergedBranch();
3289+
const queueRows = enumerateRows('/w', EB, MERGED_HEAD, 26311, { event: EVENT_MERGE_GROUP, runGit: qg.fake });
3290+
assert(
3291+
'⛔ CONTROL merge_group-leg: the-same-range-is-still-one-row-per-first-parent-commit-unattributed-without-a-subject-number',
3292+
queueRows.length === 3 && queueRows.every((r) => r.pr === null) && queueRows[1].paths.includes('AGENTS.md') && commitsInRange(queueRows) === 3,
3293+
JSON.stringify(queueRows.map((r) => [r.sha.slice(0, 1), r.pr, r.paths.length])),
3294+
);
3295+
assert('and-an-empty-pull_request-range-is-no-rows', enumerateRows('/w', EB, EB, 1, { event: EVENT_PULL_REQUEST, runGit: enumGit([], {}, []).fake }).length === 0);
3296+
}
31793297

31803298
// ── the verdict table, both events ────────────────────────────────────────
31813299
battery('the verdict table, both events');
@@ -3230,6 +3348,19 @@ export async function selfTest() {
32303348
);
32313349
const warnedHText = renderGuardVerdict(warnedV);
32323350
assert('and-names-Tier-H-and-the-approval-it-waits-for-with-no-Tier-S-block', /landing tier: H/.test(warnedHText) && /authorized APPROVED review/.test(warnedHText) && !/Tier S \(/.test(warnedHText), warnedHText);
3351+
// The PR #25965 shape end to end on this leg: the warning names the PAYLOAD's
3352+
// pull request, whose reviews it reads, and never the issue the subject names.
3353+
{
3354+
const ISSUE_ONLY = '5'.repeat(40);
3355+
const fake = (args) => {
3356+
if (args[0] === 'log') return `${ISSUE_ONLY}\tfix(pm): the main-line script reads the lock slot (#25770)\n`;
3357+
if (args[0] === 'diff-tree') return 'AGENTS.md\n';
3358+
throw new Error(args.join(' '));
3359+
};
3360+
const issueRows = enumerateRows('/w', 'b'.repeat(40), ISSUE_ONLY, 25965, { event: EVENT_PULL_REQUEST, runGit: fake });
3361+
const issueText = renderGuardVerdict(run('pull_request', issueRows, new Map([[25965, approvalVerdict([])]])));
3362+
assert('⭐ the-early-warning-names-the-payloads-pull-request-and-never-the-issue-its-subject-names', /#25965 — governed/.test(issueText) && !/#25770/.test(issueText), issueText);
3363+
}
32333364

32343365
// ── the replay fixtures: the three incidents this guard descends from ─────
32353366
battery('the replay fixtures: the three incidents this guard descends from');

0 commit comments

Comments
 (0)