Stop the pre-merge check from accepting a half-finished run (GST-2194) (#4258)

* Make pregate verify refuse a record from a partial run (GST-2194)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Update pregate and queue test fixtures to complete records

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Document that verify needs every check and that a staged rerun keeps the rest

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Skip the partial-record pregate tests on Windows, where the fixture lane is a remote obligation

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Give the crew queue tests complete pregate records

The train, train-ops and wave tests wrote one-check pregate records, which verify now refuses as a partial run. They now write every registered check, as test/gstack-ship-queue.test.ts already does (CI free-suite run 42718782737).

Review-Finding: lib/pregate/index.ts:251:functional-contract reader=core
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Explain the missing-checks pregate refusal in troubleshooting

Documents that verify refuses a record missing registered checks, and that a fix commit needs a full gstack-pregate run (Step 14.5 docs audit).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-10 01:33:31 +00:00
1 parent 00cb80cce8
commit 56c4e873b2
18 files changed
+250 -50

No files matched your search

+29 -2
View File
@@ -163,7 +163,7 @@ export function renderTable(file: PregateFile, opts: { lines?: boolean } = { lin
return [
`stage check status time detail`,
...rows, ...body, ...clocks, ...remote,
`PREGATE: ${file.verdict} tree=${file.tree.slice(0, 12)} head=${(file.head ?? '').slice(0, 12)} checks=${file.checks.length}`,
`PREGATE: ${file.verdict} tree=${file.tree.slice(0, 12)} head=${(file.head ?? '').slice(0, 12)} checks=${file.checks.length} of ${CHECK_IDS.length}`,
].join('\n') + '\n';
}
@@ -213,7 +213,9 @@ export interface Verification { ok: boolean; code?: 'PREGATE_STALE' | 'PREGATE_R
/**
* Publication check: the recorded tree must equal the tree now (results
* current after final generation and stamping), no check may be fail or
* incomplete, and every requires-remote obligation needs its receipt. The
* incomplete, every requires-remote obligation needs its receipt, and the
* record must hold every registered check (a `--only` or `--stage` run is
* not a full pass). The
* user-owned policy behind the run (or its absence, under the defaults) must
* still hold, since it lives outside the tree.
*/
@@ -245,6 +247,8 @@ export function verifyPregate(repoRoot: string, file: PregateFile, treeNow: Wtre
// A flaky exception was granted for one head, tree, run attempt and case list; recorded for another, or edited, it clears nothing.
const exception = exceptionProblem(file);
if (exception) return { ok: false, code: 'PREGATE_REMOTE_UNCLEARED', message: exception, current, uncleared: [] };
const missing = CHECK_IDS.filter(id => !file.checks.some(c => c.id === id));
if (missing.length) return { ok: false, code: 'PREGATE_INCOMPLETE', message: `pregate.json holds checks=${CHECK_IDS.length - missing.length} of ${CHECK_IDS.length} (missing: ${missing.join(', ')}); a --only or --stage run is not a full pass: run gstack-pregate --allow-repo-commands with neither at tree ${file.tree.slice(0, 12)}`, current, uncleared };
const substituted = (file.requires_remote ?? []).filter(r => r.substitution).map(r => `${r.lane}: ${describeSubstitution(r.substitution!)}`);
const excepted = exceptionSummary(file);
return { ok: true, message: `pregate ${file.verdict} is current for tree ${file.tree.slice(0, 12)}${substituted.length ? `; ${substituted.join('; ')}` : ''}${excepted ? `; ${excepted}` : ''}`, current, uncleared };
@@ -488,6 +492,29 @@ export function clearObligation(file: PregateFile, lane: string, receipt: string
return file;
}
/**
* A `--only` or `--stage preflight|tests` run keeps the checks it did not run from the record it replaces when that
* record has the same head, tree, base commit, policy and registry (GST-2194), so a staged rerun neither drops an earlier
* failure nor loses an earlier pass. Only `lanes` records requires-remote obligations, so the earlier obligations and
* exceptions come along exactly when its `lanes` result does. Any other record keeps nothing, and verify then refuses
* the partial record PREGATE_INCOMPLETE. Returns the PREGATE_MERGED line, or null when nothing was kept.
*/
export function mergeSameTreeChecks(prev: PregateFile | null, file: PregateFile): string | null {
if (!prev || prev.head !== file.head || prev.tree !== file.tree || prev.base !== file.base || prev.policy !== file.policy || prev.policy_digest !== file.policy_digest || prev.policy_user_path !== file.policy_user_path || prev.registry !== file.registry) return null;
const ran = new Set(file.checks.map(c => c.id));
const kept = prev.checks.filter(c => !ran.has(c.id) && CHECK_IDS.includes(c.id));
if (!kept.length) return null;
file.checks = [...file.checks, ...structuredClone(kept)].sort((a, b) => CHECK_IDS.indexOf(a.id) - CHECK_IDS.indexOf(b.id));
if (kept.some(c => c.id === 'lanes')) {
file.requires_remote = structuredClone(prev.requires_remote ?? []);
if (prev.exceptions?.length) file.exceptions = structuredClone(prev.exceptions); else delete file.exceptions;
}
if (kept.some(c => c.stage === 'preflight')) file.stage1_ms ??= prev.stage1_ms;
if (kept.some(c => c.stage === 'tests')) file.stage2_ms ??= prev.stage2_ms;
file.verdict = verdictOf(file);
return `PREGATE_MERGED: kept ${kept.map(c => `${c.id}=${c.status}`).join(',')} from the record at this tree ${file.tree.slice(0, 12)} and head ${(file.head ?? '').slice(0, 12)} (recorded ${prev.generated_at ?? 'earlier'}; docs/pregate.md#same-tree-checks)`;
}
/**
* A rerun at the same tree keeps the clears its predecessor recorded (GST-1627): `gstack-ship-release accept` reruns the
* pregate on the final tree after the lane cleared macos-named-regressions and windows-smoke from CI runs at that tree, and