Skip to content

Commit a55efc6

Browse files
claude[bot]claude
andauthored
fix(pm): the mixed-diff diversion on PR #15284 fired and lawfully found nothing to fork — the three report-side readings that made it look like a failure (#15650)
* fix(pm): the queue guard — a CLEAR reached through a generated-artifact lift no longer reports itself as a clear that matched nothing (#15406) The merge-queue log for PR #15284 printed, one line under its own `LIFTED skills/objectstack-ui/references/react-blocks.md` note: ✅ CLEAR — the diff touches no governed surface, so this guard has nothing to judge. … ⛔ ZERO review lookups were made: the path test runs first and returns Both sentences are false for that run. The path test MATCHED (the diff's eleventh file is on the `skills/**` surface), and the register's own recompute ran and certified it. Read back from the log, a compliant landing under the 2026-09-01 generated-artifact ruling is indistinguishable from a guard that never saw the file. Report-only: `guardVerdict` now carries the paths the register lifted (default `[]`), and the `clear` rendering picks between the zero-cost clear — kept BYTE-FOR-BYTE on both legs, so the 2026-08-27 pull_request byte-identity constraint is untouched — and a clear reached through a lift, which names the lifted paths and says the recompute ran. No predicate, verdict, exit code or API cost changes. `liftedPathsBetween` derives what was lifted from the row lists on either side of `liftGeneratedExceptions`, not from its prose notes, and is deliberately conservative across rows (the #11084 fence is per-row). Self-test: 133 → 144 cases; new battery replays #15284's real 11-path file list, one commit, PR 15284, zero reviews of any kind. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * fix(pm): the post-merge audit — a governed row names the register row it does not recompute, and --test stops reporting a post-lift zero as a clean read (#15406) Two report-side readings turned a compliant landing into an incident card. 1. `renderTestVerdict`'s head counts `hitPaths`, which is the POST-lift set. On PR #15284 it printed "0 of 11 path(s) hit the register" immediately above the exception line naming the path that hit it. The count keeps its meaning (what is STILL governed) and now says when the register lifted the difference. Byte-identical when nothing was lifted. 2. The sweep classifies with `governedPathsIn` alone and never consults the exception register — deliberately: provenance is a recompute against the tree a commit landed on, and this sweep holds no such tree. The row it rendered for #15284 was therefore indistinguishable from one for a hand-authored governed merge. `registerCell` adds the missing reading: which register row the governed path belongs to, that this sweep does NOT recompute, and that certification is recorded in that landing's queue-guard log. It lifts nothing and suppresses nothing — the row is still listed and still counts as a governed merge — and it repeats the register's own doctrine rather than softening it: a candidate earns the QUESTION, never the answer. Membership is the register's own `generatedExceptionFor`, so no second mechanism is authored (#11705's ruled constraint). Self-test: 263 → 274 assertions, new battery replaying #15284's shape in both directions (all-registered, mixed with hand-authored content, and none). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7bf96cf commit a55efc6

2 files changed

Lines changed: 373 additions & 15 deletions

File tree

‎scripts/pm/check-governed-merges.mjs‎

Lines changed: 169 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -702,11 +702,12 @@ const SELF_TEST_BATTERIES = Object.freeze({
702702
'the #11705 generator-owned rows inside `skills/**`': 23,
703703
'#11705 end to end, against the REAL generator': 7,
704704
"the live battery's prerequisite, and the floor it was misread as": 17,
705+
'⭐ #15406: the sweep row names the register it does not recompute': 10,
705706
});
706707

707708
// DELETING an entry silences that battery's floor exactly as effectively as
708709
// zeroing it, so the roster's own size is pinned too.
709-
const SELF_TEST_BATTERY_FLOOR = 23;
710+
const SELF_TEST_BATTERY_FLOOR = 24;
710711

711712
// The key an assertion is filed under when no battery is open. It is not a
712713
// declared battery, so it reds by the same set difference rather than silently
@@ -1172,7 +1173,18 @@ export function renderExceptionLines(verdict) {
11721173

11731174
/** The words a seat reads before flipping ready. Pure, so --self-test pins them. */
11741175
export function renderTestVerdict(verdict) {
1175-
const head = `governed-surface predicate: ${verdict.hitPaths.length} of ${verdict.checked} path(s) hit the register (${verdict.surfacesChecked} surfaces, repo-agnostic).`;
1176+
// `hitPaths` is the POST-lift set, so on a diff whose only register hit was a
1177+
// certified regeneration this counted 0 — printed directly above the exception
1178+
// line naming that very path as a register hit (#15406, measured on PR #15284:
1179+
// "0 of 11 path(s) hit the register" over one lifted hit). The count keeps its
1180+
// meaning — what is STILL governed — and now says when the register lifted the
1181+
// difference. ⛔ Byte-identical when nothing was lifted: the clause appears only
1182+
// when there is a lift to name.
1183+
const liftedCount = (verdict.exceptions ?? []).filter((e) => e.pureRegeneration).length;
1184+
const head =
1185+
`governed-surface predicate: ${verdict.hitPaths.length} of ${verdict.checked} path(s) hit the register` +
1186+
(liftedCount > 0 ? ` after ${liftedCount} generated-artifact lift(s)` : '') +
1187+
` (${verdict.surfacesChecked} surfaces, repo-agnostic).`;
11761188
if (!verdict.governed) {
11771189
return (
11781190
`${head}\n` +
@@ -2114,6 +2126,49 @@ export function attributionCell(entry) {
21142126
return `merged_by NOT LOOKED UP — no attribution reading was recorded for this entry (not a channel failure)`;
21152127
}
21162128

2129+
/**
2130+
* The register reading a row carries, or `''` — report-only, and pure so
2131+
* `--self-test` pins the words.
2132+
*
2133+
* ⚠️ WHY A ROW SAYS THIS AT ALL (#15406). This sweep classifies with
2134+
* `governedPathsIn` and nothing else: it never consults the exception register,
2135+
* because provenance is a recompute against the tree a commit landed on and
2136+
* this sweep holds no such tree. That conservatism is right — the row is listed
2137+
* either way — but it left the row silent about a fact its reader needs.
2138+
* Measured on PR #15284: its one `skills/**` path was the register's
2139+
* `spec-react-blocks` row, the queue leg recomputed it on that tree, certified
2140+
* it byte-exact and LIFTED it, and the landing cleared exactly as the
2141+
* 2026-09-01 ruling provides for — while this report rendered a row identical
2142+
* in every visible way to a hand-authored governed merge.
2143+
*
2144+
* ⛔ This cell LIFTS NOTHING and excuses nothing, and it is not a second
2145+
* membership mechanism: it calls the register's own `generatedExceptionFor`,
2146+
* and it repeats that function's own doctrine rather than softening it — a
2147+
* candidate earns the QUESTION, never the answer. Certification, if any, is in
2148+
* that landing's own queue-guard log, and this cell says so and says where.
2149+
*/
2150+
export function registerCell(entry) {
2151+
const governed = (entry?.surfaces ?? []).flatMap((s) => s.files);
2152+
const rows = governed.map((path) => ({ path, row: generatedExceptionFor(path) })).filter((x) => x.row !== null);
2153+
if (rows.length === 0) return '';
2154+
const named = rows.slice(0, 4).map((x) => `${x.path} → ${x.row.id} (${x.row.ruling})`);
2155+
const all = rows.length === governed.length;
2156+
return (
2157+
`\n ℹ️ REGISTER: ${all ? 'every' : `${rows.length} of ${governed.length}`} governed path(s) on this row ` +
2158+
`${all ? 'is' : 'are'} a generated-artifact CANDIDATE:\n` +
2159+
named.map((n) => ` ${n}`).join('\n') +
2160+
(rows.length > named.length ? `\n … and ${rows.length - named.length} more` : '') +
2161+
'\n A candidate earns the QUESTION, never the answer: this sweep does NOT recompute provenance' +
2162+
'\n (it holds no tree to recompute against), so whether the queue leg certified it byte-exact and' +
2163+
'\n LIFTED it is recorded in the Governed Surface Queue Guard log of that landing. A hand edit to' +
2164+
'\n the same path is never lifted.' +
2165+
(all
2166+
? ''
2167+
: '\n The other governed path(s) on this row are on no register row at all, so this row is governed' +
2168+
'\n regardless of any recompute.')
2169+
);
2170+
}
2171+
21172172
/**
21182173
* The window, in the words the operator reads — pure, and the half of route A
21192174
* the #12633 ruling names explicitly: the back-off has to be SAID, or a
@@ -2205,7 +2260,7 @@ export function renderReport({ window, repos, scanned, entries, lookups, sweepCo
22052260
const who = attributionCell(e);
22062261
const prName = e.pr != null ? `PR #${e.pr}` : '⚠️ NO PR NUMBER IN SUBJECT — direct push to main? investigate';
22072262
const files = e.surfaces.flatMap((s) => s.files.slice(0, 6)).slice(0, 8);
2208-
return ` • ${e.repoSlug ? `${e.repoSlug} ` : ''}${prName} — ${e.subject}\n commit ${e.sha.slice(0, 9)} @ ${e.date}; ${who}\n surfaces: ${surfaces}\n${files.map((f) => ` - ${f}`).join('\n')}`;
2263+
return ` • ${e.repoSlug ? `${e.repoSlug} ` : ''}${prName} — ${e.subject}\n commit ${e.sha.slice(0, 9)} @ ${e.date}; ${who}\n surfaces: ${surfaces}\n${files.map((f) => ` - ${f}`).join('\n')}${registerCell(e)}`;
22092264
});
22102265

22112266
const notes = summariseAttributionFailures(entries).map((l) => ` ${l}`);
@@ -3856,6 +3911,116 @@ async function selfTest() {
38563911
}
38573912
}
38583913

3914+
// ── ⭐ #15406: the sweep row names the register it does not recompute ─────
3915+
//
3916+
// Replays objectstack PR #15284, the landing filed as "the mixed-diff
3917+
// diversion did not fire". The diversion had fired and found nothing to fork:
3918+
// the diff's one governed path was this register's `spec-react-blocks` row,
3919+
// the queue leg recomputed it on that tree, certified it byte-exact and
3920+
// LIFTED it. Two report-side readings made a compliant landing read as a
3921+
// mechanism failure, and both are pinned here:
3922+
//
3923+
// 1. the `--test` head counted the POST-lift set, printing "0 of 11 path(s)
3924+
// hit the register" immediately above the exception line naming the hit;
3925+
// 2. this sweep classifies with `governedPathsIn` alone — deliberately, it
3926+
// holds no tree to recompute against — so the row it renders for a
3927+
// certified regeneration is indistinguishable from one for a
3928+
// hand-authored governed merge.
3929+
//
3930+
// ⛔ Neither the predicate nor what gets LISTED changes: (2) is still listed,
3931+
// still governed, still the director's to read. Only the words change.
3932+
battery('⭐ #15406: the sweep row names the register it does not recompute');
3933+
const pr15284Files = [
3934+
'.changeset/list-view-grouping-server-side-contract.md',
3935+
'content/docs/references/ui/view.mdx',
3936+
'packages/spec/src/ui/view.zod.ts',
3937+
'skills/objectstack-ui/references/react-blocks.md',
3938+
];
3939+
const pr15284ReactBlocks = 'skills/objectstack-ui/references/react-blocks.md';
3940+
// The audit leg, unchanged and asserted to be unchanged: this sweep still
3941+
// classifies that commit as a governed merge. The register is NOT consulted
3942+
// here, and this case is what stops a later reader from "fixing" that.
3943+
const pr15284Entry = classifyCommit(
3944+
{ sha: 'f502898a49530a1c85e58f3c4d2b340c0e1cb909', date: '2026-09-04T13:08:11Z', subject: 'feat(spec): list-view grouping is server-side (#15284)' },
3945+
pr15284Files,
3946+
GOVERNED_REPOS[0],
3947+
);
3948+
assert(
3949+
'⭐ the-sweep-still-CLASSIFIES-a-certified-regeneration-as-a-governed-merge',
3950+
pr15284Entry !== null && pr15284Entry.pr === 15284 && pr15284Entry.surfaces.flatMap((s) => s.files).join() === pr15284ReactBlocks,
3951+
JSON.stringify(pr15284Entry),
3952+
);
3953+
const cell15284 = registerCell(pr15284Entry);
3954+
assert('the-row-names-the-register-row-and-its-ruling', cell15284.includes(`${pr15284ReactBlocks} → spec-react-blocks (#11705)`), cell15284);
3955+
assert('the-row-says-EVERY-governed-path-on-it-is-a-candidate', /REGISTER: every governed path\(s\) on this row is a generated-artifact CANDIDATE/.test(cell15284), cell15284);
3956+
assert(
3957+
'⭐ the-row-states-that-this-sweep-does-NOT-recompute-and-says-where-certification-is-recorded',
3958+
/does NOT recompute provenance/.test(cell15284) && /Governed Surface Queue Guard log/.test(cell15284),
3959+
cell15284,
3960+
);
3961+
assert(
3962+
'⭐ the-row-repeats-the-registers-doctrine-rather-than-softening-it-a-candidate-is-a-QUESTION',
3963+
/A candidate earns the QUESTION, never the answer/.test(cell15284) && /A hand edit to\n the same path is never lifted\./.test(cell15284),
3964+
cell15284,
3965+
);
3966+
// A row with no registered path renders EXACTLY as before — the cell is empty
3967+
// string, so every ordinary governed merge keeps its pre-#15406 rendering.
3968+
const handAuthoredEntry = classifyCommit(
3969+
{ sha: 'e'.repeat(40), date: '2026-09-04T00:00:00Z', subject: 'docs: rewrite a skill (#15285)' },
3970+
['skills/objectstack-ui/SKILL.md'],
3971+
GOVERNED_REPOS[0],
3972+
);
3973+
assert('a-row-with-no-registered-path-renders-byte-identically-the-cell-is-empty', registerCell(handAuthoredEntry) === '', JSON.stringify(registerCell(handAuthoredEntry)));
3974+
// Mixed: one registered path beside a hand-authored one. The row is governed
3975+
// regardless of any recompute, and must say so rather than reading as partly
3976+
// excused.
3977+
const mixedEntry = classifyCommit(
3978+
{ sha: 'b'.repeat(40), date: '2026-09-04T00:00:00Z', subject: 'chore: regenerate and edit (#15286)' },
3979+
[pr15284ReactBlocks, 'skills/objectstack-ui/SKILL.md'],
3980+
GOVERNED_REPOS[0],
3981+
);
3982+
const mixedCell = registerCell(mixedEntry);
3983+
assert(
3984+
'a-mixed-row-counts-the-candidates-and-says-it-is-governed-regardless',
3985+
/REGISTER: 1 of 2 governed path\(s\) on this row are a generated-artifact CANDIDATE/.test(mixedCell) &&
3986+
/governed\n regardless of any recompute/.test(mixedCell),
3987+
mixedCell,
3988+
);
3989+
// End to end in the report an operator actually reads: the row is still
3990+
// listed, still carries its attribution cell, and now carries the register
3991+
// reading beneath it.
3992+
const swept15284 = renderReport({
3993+
window: dateWindowFor('2026-09-04T00:00:00Z'),
3994+
repos: allAudited,
3995+
scanned: 40,
3996+
entries: [{ ...pr15284Entry, attribution: { mergedBy: 'os-justin', mergedAt: '2026-09-04T13:08:11Z', title: 'x' }, attributionChannel: 'rest' }],
3997+
lookups: 1,
3998+
});
3999+
assert(
4000+
'the-rendered-sweep-still-LISTS-the-row-and-now-carries-the-register-reading',
4001+
swept15284.includes('PR #15284') && swept15284.includes('merged_by os-justin') && swept15284.includes('spec-react-blocks (#11705)'),
4002+
swept15284,
4003+
);
4004+
assert('and-the-sweep-never-suppresses-such-a-row-it-still-counts-as-a-governed-merge', swept15284.includes('governed-merges sweep: 1 governed merge(s)'), swept15284);
4005+
// The `--test` head line, both directions. `hitPaths` is the post-lift set,
4006+
// so the count itself is right; what was missing is the reason it moved.
4007+
const liftedTest = applyGeneratedExceptions(
4008+
testVerdict(pr15284Files),
4009+
new Map([[pr15284ReactBlocks, { pureRegeneration: true, reason: 'byte-equal (fixture)' }]]),
4010+
);
4011+
const liftedHead = renderTestVerdict(liftedTest).split('\n')[0];
4012+
assert(
4013+
'⭐ the-test-head-no-longer-reports-a-post-lift-zero-as-if-nothing-had-hit-the-register',
4014+
liftedHead === 'governed-surface predicate: 0 of 4 path(s) hit the register after 1 generated-artifact lift(s) (5 surfaces, repo-agnostic).',
4015+
liftedHead,
4016+
);
4017+
const plainHead = renderTestVerdict(testVerdict(['packages/spec/src/ui/view.zod.ts'])).split('\n')[0];
4018+
assert(
4019+
'and-a-verdict-with-no-lift-keeps-its-head-line-byte-for-byte',
4020+
plainHead === 'governed-surface predicate: 0 of 1 path(s) hit the register (5 surfaces, repo-agnostic).',
4021+
plainHead,
4022+
);
4023+
38594024
// ── The floor: every declared battery RAN, and ran its cases (#13489) ────
38604025
//
38614026
// Evaluated after every battery has had its chance and BEFORE the verdict, so
@@ -3869,7 +4034,7 @@ async function selfTest() {
38694034
for (const failure of failures) console.error(` • ${failure}`);
38704035
process.exit(1);
38714036
}
3872-
console.log(`✓ check-governed-merges --self-test: ${checked} assertions (the unified governed predicate + near misses, subject→PR spellings, window parsing, the #12633 landing window — the QS-7 regression pin in both directions, the topological close beyond the budget, the unproven-boundary EDGE, the listed-or-INCOMPLETE invariant over every fixture, the escalating floors, per-repo --since-ref resolution and its named fallback, and the window words — the replay fixtures, the five-repo resolution incl. absent/wrong-origin/relocated checkouts, the attribution channel chain + its proxy-transport re-arm plan and its one named fallback line, the three-way attribution column (resolved · every-channel-failed · NOT LOOKED UP, and the note pointer that belongs to the middle one alone), the --test pre-arm predicate, the generated-artifact provenance exception — the register's invariants incl. the RETIRED #9866 row staying retired (no row lifts anything under .claude/**, and the audit workflow is plainly governed again), a row with no recompute failing closed, lift/reject/absent-provenance semantics, the untouched mixed-diff rule, named-rows-not-a-class, the #11084 generator co-edit fence in both directions incl. a row with no instrument tree, and its render words — the #11705 generator-owned rows inside skills/** (a genuine generated file passes, the same path hand-edited does not, a path no generator declares is hand-authored content, per-row fences, and the enumeration read from the real generator), the exit table, the report wording pins, and the #13307 remote-reachability leg — the pure freshness verdicts in every branch (unreachable · a remote naming no commit · an unreadable local tip · a mirror behind its remote · the two-unreadable-shas degenerate case that must never read as a match), the report words in both directions (an unreachable repo never renders the tick, a reachable one still says a MEASURED zero, and a row with no remote reading never claims one), and the REAL prober on local bare-repo fixtures over the file transport — a live remote, a deleted one, the --exit-code branch, and a mirror the remote moved past — the #13423 identity leg (an origin no slug parses from refuses, pure and end-to-end, with audited reachable only through a parsed matching slug), the #13424 per-repo window resolution (a sibling-only pin resolves in its own repo, the self-only control still errors, and the end-to-end sibling-pin sweep reports instead of exiting 1), the #13307 sweep-code provenance line in all three branches, and the #13836 attribution set — every refusal carries its precondition category on the row, in the footer, and in --json; the shallow-clone path in both directions; and the run-1-vs-run-2 flip reproduced on real fixtures with zero local writes — and the live battery's own PREREQUISITE, asked before a single case runs: an uninstalled checkout refuses with the repo-wide NOT-MEASURED code end to end instead of reporting a shrunken battery, while the floor still names the battery, by itself, for a case that genuinely stopped registering).\n ${liveNote}`);
4037+
console.log(`✓ check-governed-merges --self-test: ${checked} assertions (the unified governed predicate + near misses, subject→PR spellings, window parsing, the #12633 landing window — the QS-7 regression pin in both directions, the topological close beyond the budget, the unproven-boundary EDGE, the listed-or-INCOMPLETE invariant over every fixture, the escalating floors, per-repo --since-ref resolution and its named fallback, and the window words — the replay fixtures, the five-repo resolution incl. absent/wrong-origin/relocated checkouts, the attribution channel chain + its proxy-transport re-arm plan and its one named fallback line, the three-way attribution column (resolved · every-channel-failed · NOT LOOKED UP, and the note pointer that belongs to the middle one alone), the --test pre-arm predicate, the generated-artifact provenance exception — the register's invariants incl. the RETIRED #9866 row staying retired (no row lifts anything under .claude/**, and the audit workflow is plainly governed again), a row with no recompute failing closed, lift/reject/absent-provenance semantics, the untouched mixed-diff rule, named-rows-not-a-class, the #11084 generator co-edit fence in both directions incl. a row with no instrument tree, and its render words — the #11705 generator-owned rows inside skills/** (a genuine generated file passes, the same path hand-edited does not, a path no generator declares is hand-authored content, per-row fences, and the enumeration read from the real generator), the exit table, the report wording pins, and the #13307 remote-reachability leg — the pure freshness verdicts in every branch (unreachable · a remote naming no commit · an unreadable local tip · a mirror behind its remote · the two-unreadable-shas degenerate case that must never read as a match), the report words in both directions (an unreachable repo never renders the tick, a reachable one still says a MEASURED zero, and a row with no remote reading never claims one), and the REAL prober on local bare-repo fixtures over the file transport — a live remote, a deleted one, the --exit-code branch, and a mirror the remote moved past — the #13423 identity leg (an origin no slug parses from refuses, pure and end-to-end, with audited reachable only through a parsed matching slug), the #13424 per-repo window resolution (a sibling-only pin resolves in its own repo, the self-only control still errors, and the end-to-end sibling-pin sweep reports instead of exiting 1), the #13307 sweep-code provenance line in all three branches, and the #13836 attribution set — every refusal carries its precondition category on the row, in the footer, and in --json; the shallow-clone path in both directions; and the run-1-vs-run-2 flip reproduced on real fixtures with zero local writes — and the live battery's own PREREQUISITE, asked before a single case runs: an uninstalled checkout refuses with the repo-wide NOT-MEASURED code end to end instead of reporting a shrunken battery, while the floor still names the battery, by itself, for a case that genuinely stopped registering) — and the #15406 replay of PR #15284: the sweep still CLASSIFIES a certified regeneration as a governed merge and still lists it, its row now names the register row it does not recompute and where certification is recorded, and the --test head no longer reports a post-lift zero as if nothing had hit the register.\n ${liveNote}`);
38734038

38744039
return SELF_TEST_VERDICT;
38754040
}

0 commit comments

Comments
 (0)