From 3da65ccebf650917391992281a67e2510d81908d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 01:15:23 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(pm):=20the=20queue=20guard=20=E2=80=94?= =?UTF-8?q?=20a=20CLEAR=20reached=20through=20a=20generated-artifact=20lif?= =?UTF-8?q?t=20no=20longer=20reports=20itself=20as=20a=20clear=20that=20ma?= =?UTF-8?q?tched=20nothing=20(#15406)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/pm/check-governed-queue-guard.mjs | 215 ++++++++++++++++++++-- 1 file changed, 204 insertions(+), 11 deletions(-) diff --git a/scripts/pm/check-governed-queue-guard.mjs b/scripts/pm/check-governed-queue-guard.mjs index ae5f041ea8..240297fb1f 100644 --- a/scripts/pm/check-governed-queue-guard.mjs +++ b/scripts/pm/check-governed-queue-guard.mjs @@ -302,11 +302,12 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'the PR-head reader: throws, and the caller no longer refuses on it': 3, 'the WIRING pin: the workflow still spells this context name': 8, '⭐ #14063: the environment the exemption needs, pinned to the YAML': 7, + '⭐ #15406: a CLEAR reached through a lift is not a clear that saw nothing': 10, }); // 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 = 17; +const SELF_TEST_BATTERY_FLOOR = 18; // 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 @@ -559,12 +560,12 @@ export function unreadableApproval(reason) { * The verdict, as data. Pure — every branch of the decision is here, and the * renderer and the exit code both read it rather than re-deriving it. */ -export function guardVerdict({ event, governed = [], unattributed = [], approvals = new Map(), apiCalls = 0, headNotes = [] }) { +export function guardVerdict({ event, governed = [], unattributed = [], approvals = new Map(), apiCalls = 0, headNotes = [], lifted = [] }) { const entries = governed.map((entry) => ({ ...entry, approval: approvals.get(entry.pr) ?? unreadableApproval('no review reading was recorded for this pull request'), })); - const base = { event, entries, unattributed, apiCalls, headNotes, contextName: CHECK_CONTEXT_NAME }; + const base = { event, 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 }; @@ -611,11 +612,36 @@ export function renderGuardVerdict(verdict) { ); if (verdict.conclusion === 'clear') { + // ⭐ TWO DIFFERENT CLEARS, and #15406 is what conflating them costs. The + // wording below used to be unconditional, so a run in which the register + // LIFTED a governed path printed "the diff touches no governed surface" + // immediately under its own LIFTED line — and "the path test runs first and + // returns", when the path test had matched and the register's recompute had + // run. A landing read back from that log looks like a guard that never saw + // the file, which is exactly the reading the post-merge audit was filed on. + // A verdict may not deny its own evidence: the zero-cost clear keeps its + // wording BYTE-FOR-BYTE (both legs, so the pull_request leg's byte-identity + // constraint is untouched), and a clear reached THROUGH a lift says so. + const lifted = verdict.lifted ?? []; + 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),`, + ' never from a restated list. ⛔ ZERO review lookups were made: the path test runs first and returns,', + ' so a GitHub API outage can never block a diff that touches nothing governed.', + ); + return lines.join('\n'); + } lines.push( - ' ✅ CLEAR — the diff touches no governed surface, so this guard has nothing to judge.', + ' ✅ CLEAR — this diff DID touch a governed surface, and every hit was LIFTED by the generated-artifact', + ' register; nothing hand-authored is left for this guard to judge. ⚠️ This is NOT the "no governed', + ' path in the diff" clear: the path test MATCHED, the register recomputed provenance on this tree,', + ' 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),`, - ' 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.', + ' 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.', ); return lines.join('\n'); } @@ -773,10 +799,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, fetchPullHead }) { +export async function runGuard({ event, rows, fetchReviews, fetchPullHead, lifted = [] }) { const { governed, unattributed } = decomposeGovernedWork(rows); if (governed.length === 0 && unattributed.length === 0) { - return guardVerdict({ event, governed, unattributed, apiCalls: 0 }); + return guardVerdict({ event, governed, unattributed, apiCalls: 0, lifted }); } const approvals = new Map(); const headNotes = []; @@ -812,7 +838,7 @@ export async function runGuard({ event, rows, fetchReviews, fetchPullHead }) { approvals.set(entry.pr, unreadableApproval(String(error?.message ?? error).split('\n')[0])); } } - return guardVerdict({ event, governed, unattributed, approvals, apiCalls, headNotes }); + return guardVerdict({ event, governed, unattributed, approvals, apiCalls, headNotes, lifted }); } // ── git (diff decomposition; zero API) ────────────────────────────────────── @@ -914,6 +940,34 @@ export async function liftGeneratedExceptions(root, baseSha, rows, notes, recomp return out; } +/** + * Which registered paths `liftGeneratedExceptions` actually LIFTED, derived + * from the row lists on either side of it. Pure, so the rendering that depends + * on it is pinned offline. + * + * Read from the rows rather than from the notes: the notes are prose for a + * human, and a verdict that parsed them back would be deriving a decision from + * a rendering. Membership is still the register's own `generatedExceptionFor` — + * a path that vanished for any other reason is not reported as a lift. + * + * ⚠️ Deliberately conservative across rows: a path kept by ANY row is not + * listed, because the #11084 fence is per-row and one row's co-edit can keep a + * path governed that another row's recompute certified. Under-reporting a lift + * only ever costs a line of log; over-reporting one would put "we lifted it" + * next to a verdict that did not. + */ +export function liftedPathsBetween(before, after) { + const kept = new Set((Array.isArray(after) ? after : []).flatMap((r) => r?.paths ?? [])); + const out = []; + for (const row of Array.isArray(before) ? before : []) { + for (const p of row?.paths ?? []) { + if (kept.has(p) || generatedExceptionFor(p) === null || out.includes(p)) continue; + out.push(p); + } + } + return out; +} + /** * The workflow's toolchain wiring, as data — the #14063 half of the wiring pin. * @@ -1048,10 +1102,13 @@ async function main() { const notes = []; let rows; + let lifted = []; try { 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); + 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; @@ -1066,7 +1123,7 @@ async function main() { const fetchReviews = makeReviewReader(reader); const fetchPullHead = makePullHeadReader(reader); - const verdict = await runGuard({ event: context.event, rows, fetchReviews, fetchPullHead }); + const verdict = await runGuard({ event: context.event, rows, fetchReviews, fetchPullHead, lifted }); const report = [`${context.label} — ${rows.length} commit(s) in range`, ...notes, renderGuardVerdict(verdict)].join('\n'); console.log(report); @@ -1708,6 +1765,141 @@ export async function selfTest() { } assert('a-recompute-that-THROWS-never-lifts-it-propagates-into-CANNOT-RUN', liftThrew !== null && /EACCES/.test(liftThrew), String(liftThrew)); + // ── ⭐ #15406: a CLEAR reached through a lift is not a clear that saw nothing ─ + // + // Replays the real diff shape of objectstack#15284 — the landing that was + // filed as "the mixed-diff diversion did not fire". It had NOT failed: the + // diff's single governed path was the register's own `spec-react-blocks` + // row, the recompute certified it byte-exact, and the queue leg cleared with + // zero approvals exactly as the 2026-09-01 ruling provides for. What failed + // was the LOG. Its verdict line read, directly under its own LIFTED line: + // + // ✅ 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, and the + // register's recompute ran. Read back from the merge queue log, that landing + // is indistinguishable from a guard that never saw the file — which is the + // reading the post-merge audit row was filed on. ⛔ The predicate is NOT what + // these cases pin: the verdict, the exit code and the API cost are asserted + // to be the SAME as before, and only the words change. + battery('⭐ #15406: a CLEAR reached through a lift is not a clear that saw nothing'); + // #15284's real file list, in its merged order. + const pr15284Paths = [ + '.changeset/list-view-grouping-server-side-contract.md', + 'content/docs/references/api/protocol.mdx', + 'content/docs/references/data/object.mdx', + 'content/docs/references/ui/view.mdx', + 'packages/spec/api-surface/ui.json', + 'packages/spec/export-origins/ui.json', + 'packages/spec/src/ui/index.ts', + 'packages/spec/src/ui/view-grouping-query.test.ts', + 'packages/spec/src/ui/view-grouping-query.ts', + 'packages/spec/src/ui/view.zod.ts', + 'skills/objectstack-ui/references/react-blocks.md', + ]; + const pr15284ReactBlocks = 'skills/objectstack-ui/references/react-blocks.md'; + const pr15284Row = (paths = pr15284Paths) => ({ + sha: 'f502898a49530a1c85e58f3c4d2b340c0e1cb909', + subject: 'feat(spec): list-view grouping is server-side (#15284)', + pr: 15284, + paths, + }); + // main()'s own wiring, reproduced: lift, derive what was lifted, then judge. + // ⛔ Not the `endToEnd` helper above — the defect lived in the step BETWEEN + // 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 lifted = liftedPathsBetween(rows, after); + const verdict = await runGuard({ + event: 'merge_group', + rows: after, + fetchPullHead: io.head ?? (() => HEAD), + fetchReviews: io.reviews ?? (() => []), + lifted, + }); + return { verdict, notes, lifted, text: renderGuardVerdict(verdict) }; + }; + const noApi = () => { + throw new Error('the API must not be reached — the only governed path was lifted'); + }; + // The classification leg first: the file IS on the register, and the diff DID + // hit a governed surface before anything was lifted. Both are facts the old + // verdict line denied. + assert( + 'the-one-skills-path-in-15284-is-a-register-CANDIDATE-not-hand-authored-content', + generatedExceptionFor(pr15284ReactBlocks)?.id === 'spec-react-blocks', + JSON.stringify(generatedExceptionFor(pr15284ReactBlocks)), + ); + 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)), + ); + const certified15284 = await asMainDoes( + verified('byte-equal to the react-blocks generator recomputed on this tree (fixture)'), + [pr15284Row()], + { head: noApi, reviews: noApi }, + ); + // The verdict itself is UNCHANGED — the 2026-09-01 ruling working, with an + // approver set that does not contain the merging account and no review of any + // kind on the pull request. + assert( + 'a-certified-regeneration-still-CLEARS-with-zero-reviews-and-zero-api-calls', + certified15284.verdict.conclusion === 'clear' && certified15284.verdict.exitCode === EXIT_CLEAR && certified15284.verdict.apiCalls === 0, + JSON.stringify({ c: certified15284.verdict.conclusion, e: certified15284.verdict.exitCode, api: certified15284.verdict.apiCalls }), + ); + assert( + 'liftedPathsBetween-names-exactly-the-path-the-register-lifted', + certified15284.lifted.join() === pr15284ReactBlocks, + JSON.stringify(certified15284.lifted), + ); + assert( + '⭐ the-CLEAR-line-does-NOT-claim-the-diff-touched-no-governed-surface', + !certified15284.text.includes('the diff touches no governed surface'), + certified15284.text, + ); + assert( + '⭐ the-CLEAR-line-does-NOT-claim-the-path-test-returned-before-matching', + !certified15284.text.includes('the path test runs first and returns'), + certified15284.text, + ); + assert( + 'the-CLEAR-line-names-the-lifted-path-and-says-the-recompute-ran', + certified15284.text.includes(pr15284ReactBlocks) && + /LIFTED/.test(certified15284.text) && + /recomputed provenance on this tree/.test(certified15284.text), + certified15284.text, + ); + assert( + 'and-it-still-reports-the-zero-lookup-cost-a-reader-checks-for', + /ZERO review lookups/.test(certified15284.text), + certified15284.text, + ); + // The other direction, on the SAME path: a hand edit to that file is not + // lifted, so the identical file list refuses. The shape is not an exemption + // for `references/react-blocks.md`; it is an exemption for a recompute. + const handEdited15284 = await asMainDoes(refused('the generator does not certify this tree (fixture)'), [pr15284Row()]); + assert( + 'a-hand-edit-to-the-SAME-path-still-REFUSES-the-same-file-list', + handEdited15284.verdict.exitCode === EXIT_REFUSED_UNAPPROVED && handEdited15284.lifted.length === 0, + JSON.stringify({ e: handEdited15284.verdict.exitCode, lifted: handEdited15284.lifted }), + ); + // 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 })); + 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.') && + nothing.includes(' never from a restated list. ⛔ ZERO review lookups were made: the path test runs first and returns,') && + nothing.includes(' so a GitHub API outage can never block a diff that touches nothing governed.'), + nothing, + ); + } + // ── the PR-head reader: throws, and the caller no longer refuses on it ─── // // The READER's own contract is unchanged — a non-2xx or an unparseable body @@ -1855,7 +2047,8 @@ export async function selfTest() { 'regeneration (clears with zero approvals and zero API calls; still refuses on an uncertified recompute, on ' + 'drift, on a hand-authored sibling, and on a recompute that throws), and the workflow wiring pin including the ' + 'dependency install the recompute needs, its register-agnostic filter-free form, and its continue-on-error ' + - 'degradation).', + 'degradation), and the #15406 replay of PR #15284 — a clear reached THROUGH a lift no longer reports itself as a ' + + 'clear that matched nothing, while the zero-cost clear keeps its wording byte-for-byte on both legs.', ); selfTestReachedVerdict = true; From 9832369e98a0398d7159f7dfa3c1b1924664b81e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 01:19:41 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(pm):=20the=20post-merge=20audit=20?= =?UTF-8?q?=E2=80=94=20a=20governed=20row=20names=20the=20register=20row?= =?UTF-8?q?=20it=20does=20not=20recompute,=20and=20--test=20stops=20report?= =?UTF-8?q?ing=20a=20post-lift=20zero=20as=20a=20clean=20read=20(#15406)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/pm/check-governed-merges.mjs | 173 ++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 4 deletions(-) diff --git a/scripts/pm/check-governed-merges.mjs b/scripts/pm/check-governed-merges.mjs index 6a2dc979c3..081df968d6 100644 --- a/scripts/pm/check-governed-merges.mjs +++ b/scripts/pm/check-governed-merges.mjs @@ -702,11 +702,12 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'the #11705 generator-owned rows inside `skills/**`': 23, '#11705 end to end, against the REAL generator': 7, "the live battery's prerequisite, and the floor it was misread as": 17, + '⭐ #15406: the sweep row names the register it does not recompute': 10, }); // 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 = 23; +const SELF_TEST_BATTERY_FLOOR = 24; // 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 @@ -1172,7 +1173,18 @@ export function renderExceptionLines(verdict) { /** The words a seat reads before flipping ready. Pure, so --self-test pins them. */ export function renderTestVerdict(verdict) { - const head = `governed-surface predicate: ${verdict.hitPaths.length} of ${verdict.checked} path(s) hit the register (${verdict.surfacesChecked} surfaces, repo-agnostic).`; + // `hitPaths` is the POST-lift set, so on a diff whose only register hit was a + // certified regeneration this counted 0 — printed directly above the exception + // line naming that very path as a register hit (#15406, measured on PR #15284: + // "0 of 11 path(s) hit the register" over one lifted hit). The count keeps its + // meaning — what is STILL governed — and now says when the register lifted the + // difference. ⛔ Byte-identical when nothing was lifted: the clause appears only + // when there is a lift to name. + const liftedCount = (verdict.exceptions ?? []).filter((e) => e.pureRegeneration).length; + 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).`; if (!verdict.governed) { return ( `${head}\n` + @@ -2114,6 +2126,49 @@ export function attributionCell(entry) { return `merged_by NOT LOOKED UP — no attribution reading was recorded for this entry (not a channel failure)`; } +/** + * The register reading a row carries, or `''` — report-only, and pure so + * `--self-test` pins the words. + * + * ⚠️ WHY A ROW SAYS THIS AT ALL (#15406). This sweep classifies with + * `governedPathsIn` and nothing else: it never consults the exception register, + * because provenance is a recompute against the tree a commit landed on and + * this sweep holds no such tree. That conservatism is right — the row is listed + * either way — but it left the row silent about a fact its reader needs. + * Measured on PR #15284: its one `skills/**` path was the register's + * `spec-react-blocks` row, the queue leg recomputed it on that tree, certified + * it byte-exact and LIFTED it, and the landing cleared exactly as the + * 2026-09-01 ruling provides for — while this report rendered a row identical + * in every visible way to a hand-authored governed merge. + * + * ⛔ This cell LIFTS NOTHING and excuses nothing, and it is not a second + * membership mechanism: it calls the register's own `generatedExceptionFor`, + * and it repeats that function's own doctrine rather than softening it — a + * candidate earns the QUESTION, never the answer. Certification, if any, is in + * that landing's own queue-guard log, and this cell says so and says where. + */ +export function registerCell(entry) { + const governed = (entry?.surfaces ?? []).flatMap((s) => s.files); + const rows = governed.map((path) => ({ path, row: generatedExceptionFor(path) })).filter((x) => x.row !== null); + if (rows.length === 0) return ''; + const named = rows.slice(0, 4).map((x) => `${x.path} → ${x.row.id} (${x.row.ruling})`); + const all = rows.length === governed.length; + return ( + `\n ℹ️ REGISTER: ${all ? 'every' : `${rows.length} of ${governed.length}`} governed path(s) on this row ` + + `${all ? 'is' : 'are'} a generated-artifact CANDIDATE:\n` + + named.map((n) => ` ${n}`).join('\n') + + (rows.length > named.length ? `\n … and ${rows.length - named.length} more` : '') + + '\n A candidate earns the QUESTION, never the answer: this sweep does NOT recompute provenance' + + '\n (it holds no tree to recompute against), so whether the queue leg certified it byte-exact and' + + '\n LIFTED it is recorded in the Governed Surface Queue Guard log of that landing. A hand edit to' + + '\n the same path is never lifted.' + + (all + ? '' + : '\n The other governed path(s) on this row are on no register row at all, so this row is governed' + + '\n regardless of any recompute.') + ); +} + /** * The window, in the words the operator reads — pure, and the half of route A * 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 const who = attributionCell(e); const prName = e.pr != null ? `PR #${e.pr}` : '⚠️ NO PR NUMBER IN SUBJECT — direct push to main? investigate'; const files = e.surfaces.flatMap((s) => s.files.slice(0, 6)).slice(0, 8); - 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')}`; + 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)}`; }); const notes = summariseAttributionFailures(entries).map((l) => ` ${l}`); @@ -3856,6 +3911,116 @@ async function selfTest() { } } + // ── ⭐ #15406: the sweep row names the register it does not recompute ───── + // + // Replays objectstack PR #15284, the landing filed as "the mixed-diff + // diversion did not fire". The diversion had fired and found nothing to fork: + // the diff's one governed path was this register's `spec-react-blocks` row, + // the queue leg recomputed it on that tree, certified it byte-exact and + // LIFTED it. Two report-side readings made a compliant landing read as a + // mechanism failure, and both are pinned here: + // + // 1. the `--test` head counted the POST-lift set, printing "0 of 11 path(s) + // hit the register" immediately above the exception line naming the hit; + // 2. this sweep classifies with `governedPathsIn` alone — deliberately, it + // holds no tree to recompute against — so the row it renders for a + // certified regeneration is indistinguishable from one for a + // hand-authored governed merge. + // + // ⛔ Neither the predicate nor what gets LISTED changes: (2) is still listed, + // still governed, still the director's to read. Only the words change. + battery('⭐ #15406: the sweep row names the register it does not recompute'); + const pr15284Files = [ + '.changeset/list-view-grouping-server-side-contract.md', + 'content/docs/references/ui/view.mdx', + 'packages/spec/src/ui/view.zod.ts', + 'skills/objectstack-ui/references/react-blocks.md', + ]; + const pr15284ReactBlocks = 'skills/objectstack-ui/references/react-blocks.md'; + // The audit leg, unchanged and asserted to be unchanged: this sweep still + // classifies that commit as a governed merge. The register is NOT consulted + // here, and this case is what stops a later reader from "fixing" that. + const pr15284Entry = classifyCommit( + { sha: 'f502898a49530a1c85e58f3c4d2b340c0e1cb909', date: '2026-09-04T13:08:11Z', subject: 'feat(spec): list-view grouping is server-side (#15284)' }, + pr15284Files, + GOVERNED_REPOS[0], + ); + assert( + '⭐ the-sweep-still-CLASSIFIES-a-certified-regeneration-as-a-governed-merge', + pr15284Entry !== null && pr15284Entry.pr === 15284 && pr15284Entry.surfaces.flatMap((s) => s.files).join() === pr15284ReactBlocks, + JSON.stringify(pr15284Entry), + ); + const cell15284 = registerCell(pr15284Entry); + assert('the-row-names-the-register-row-and-its-ruling', cell15284.includes(`${pr15284ReactBlocks} → spec-react-blocks (#11705)`), cell15284); + 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); + assert( + '⭐ the-row-states-that-this-sweep-does-NOT-recompute-and-says-where-certification-is-recorded', + /does NOT recompute provenance/.test(cell15284) && /Governed Surface Queue Guard log/.test(cell15284), + cell15284, + ); + assert( + '⭐ the-row-repeats-the-registers-doctrine-rather-than-softening-it-a-candidate-is-a-QUESTION', + /A candidate earns the QUESTION, never the answer/.test(cell15284) && /A hand edit to\n the same path is never lifted\./.test(cell15284), + cell15284, + ); + // A row with no registered path renders EXACTLY as before — the cell is empty + // string, so every ordinary governed merge keeps its pre-#15406 rendering. + const handAuthoredEntry = classifyCommit( + { sha: 'e'.repeat(40), date: '2026-09-04T00:00:00Z', subject: 'docs: rewrite a skill (#15285)' }, + ['skills/objectstack-ui/SKILL.md'], + GOVERNED_REPOS[0], + ); + assert('a-row-with-no-registered-path-renders-byte-identically-the-cell-is-empty', registerCell(handAuthoredEntry) === '', JSON.stringify(registerCell(handAuthoredEntry))); + // Mixed: one registered path beside a hand-authored one. The row is governed + // regardless of any recompute, and must say so rather than reading as partly + // excused. + const mixedEntry = classifyCommit( + { sha: 'b'.repeat(40), date: '2026-09-04T00:00:00Z', subject: 'chore: regenerate and edit (#15286)' }, + [pr15284ReactBlocks, 'skills/objectstack-ui/SKILL.md'], + GOVERNED_REPOS[0], + ); + const mixedCell = registerCell(mixedEntry); + assert( + 'a-mixed-row-counts-the-candidates-and-says-it-is-governed-regardless', + /REGISTER: 1 of 2 governed path\(s\) on this row are a generated-artifact CANDIDATE/.test(mixedCell) && + /governed\n regardless of any recompute/.test(mixedCell), + mixedCell, + ); + // End to end in the report an operator actually reads: the row is still + // listed, still carries its attribution cell, and now carries the register + // reading beneath it. + const swept15284 = renderReport({ + window: dateWindowFor('2026-09-04T00:00:00Z'), + repos: allAudited, + scanned: 40, + entries: [{ ...pr15284Entry, attribution: { mergedBy: 'os-justin', mergedAt: '2026-09-04T13:08:11Z', title: 'x' }, attributionChannel: 'rest' }], + lookups: 1, + }); + assert( + 'the-rendered-sweep-still-LISTS-the-row-and-now-carries-the-register-reading', + swept15284.includes('PR #15284') && swept15284.includes('merged_by os-justin') && swept15284.includes('spec-react-blocks (#11705)'), + swept15284, + ); + 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); + // 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), + 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) (5 surfaces, repo-agnostic).', + liftedHead, + ); + const plainHead = renderTestVerdict(testVerdict(['packages/spec/src/ui/view.zod.ts'])).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 (5 surfaces, repo-agnostic).', + plainHead, + ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ──── // // Evaluated after every battery has had its chance and BEFORE the verdict, so @@ -3869,7 +4034,7 @@ async function selfTest() { for (const failure of failures) console.error(` • ${failure}`); process.exit(1); } - 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}`); + 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}`); return SELF_TEST_VERDICT; }