v1.94.19.0 stamp: merge main 44471cff1f into #3932 (land)

This commit is contained in:
garrytan committed 2026-11-03 14:21:58 +00:00
commit 0e18bfa87f
88 files changed
+4006 -301

No files matched your search

+2 -1
View File
@@ -16,6 +16,7 @@ import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { BANNED_TERMS_PATH, addedLines, exampleBannedTermsJson, loadBannedTerms, renderBannedTerms, scanBannedTerms, validateBannedTerms, wholeFiles } from '../lib/banned-terms';
import { baseArg } from '../lib/gate-diff/git';
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
import { resultLine } from '../lib/result-codes';
import { resolveBaseBranch } from '../lib/ship-policy';
@@ -75,7 +76,7 @@ function main(argv: string[]): number {
let files;
if (filesFlag) files = wholeFiles(repoRoot, filesFlag.split(',').map(s => s.trim()).filter(Boolean));
else {
const diffBase = flag(rest, '--diff-base') ?? (() => { const mb = spawnSync('git', ['merge-base', base, 'HEAD'], { cwd: repoRoot, encoding: 'utf8', timeout: 30_000 }); return mb.status === 0 ? mb.stdout.trim() : undefined; })();
const diffBase = flag(rest, '--diff-base') ?? (() => { const mb = spawnSync('git', ['merge-base', baseArg('gstack-banned-terms', base, repoRoot), 'HEAD'], { cwd: repoRoot, encoding: 'utf8', timeout: 30_000 }); return mb.status === 0 ? mb.stdout.trim() : undefined; })();
if (!diffBase) { process.stderr.write(`gstack-banned-terms: no merge base with ${base}; pass --diff-base <sha> or --files <csv>\n`); return EXIT.usage; }
const diff = spawnSync('git', ['diff', '--unified=0', '--no-color', diffBase], { cwd: repoRoot, encoding: 'utf8', timeout: 120_000, maxBuffer: 256 * 1024 * 1024 });
if (diff.status !== 0) { process.stderr.write(`gstack-banned-terms: git diff ${diffBase} failed\n`); return EXIT.fail; }
+18 -11
View File
@@ -3,6 +3,7 @@
* gstack-ci-carry — CI as the one full test run (GSTA-258; docs/pregate.md#ci-carry).
*
* gstack-ci-carry find [--base <branch>] [--profile <name>]... [--command <exact lane command> --allow-repo-commands] [--json]
* [--policy-from head --simulate]
* gstack-ci-carry wait [--base <branch>] [--profile <name>] [--timeout <s>] [--interval <s>]
*
* find: the newest verified run of each profile (default: every profile the base branch's
@@ -14,10 +15,9 @@
* carried, 3 still running or no run. Logic: lib/ci-carry.ts; the run checks are lib/run-evidence.ts.
*/
import { spawnSync } from 'node:child_process';
import { findCarry, waitForRun } from '../lib/ci-carry';
import { isConfigUnavailable, loadConfigFromBase } from '../lib/pregate/config';
import { findCarry, laneMapping, mappingCoverage, waitForRun } from '../lib/ci-carry';
import { isConfigUnavailable, loadConfigFromBase, loadConfigFromHead } from '../lib/pregate/config';
import { buildRunCheck } from '../lib/pregate/run-check';
import { runInventory } from '../lib/run-evidence';
import { wtreeFingerprint } from '../lib/script-invocation';
import { resolveBaseBranch } from '../lib/ship-policy';
@@ -25,11 +25,13 @@ const HELP = `gstack-ci-carry — use a verified CI run at HEAD's exact tree ins
find [--base <branch>] [--profile <name>]... [--command <exact lane command> --allow-repo-commands] [--json]
CI_CARRY: accepted <receipt> (exit 0) or CI_CARRY: none (<reason>) (exit 1)
--policy-from head --simulate: the same check under the branch's own map and profiles, every line starting
SIMULATION:; a dry run that proves a new mapping before it lands (find never writes evidence; nothing clears)
wait [--base <branch>] [--profile <name>] [--timeout <s>] [--interval <s>]
poll the newest run at HEAD (default 1800 s, every 20 s): CI_WAIT: green|red|running|none, then find on green;
exit 0 carried, 1 red or not carried, 3 still running or no run
Profiles and ship_lanes_from come from origin/<base>'s .gstack/pregate.json only. Only a clean, committed tree
Profiles and ship_lanes_from come from origin/<base>'s .gstack/pregate.json only (a simulation reads the branch's). Only a clean, committed tree
carries; fork runs, stale runs, a newer red or running run on HEAD and a tree.json that misses an inventory
file never do. Exit 2 is usage.`;
@@ -37,30 +39,35 @@ const argv = process.argv.slice(2);
const cmd = argv[0];
const all = (n: string) => argv.flatMap((a, i) => (a === n && argv[i + 1] !== undefined ? [argv[i + 1]!] : []));
const flag = (n: string) => all(n).at(-1);
const out = (s: string) => process.stdout.write(`${s}\n`);
const simulate = argv.includes('--simulate');
const out = (s: string) => process.stdout.write(`${simulate ? 'SIMULATION: ' : ''}${s}\n`);
const usage = (m: string) => { process.stderr.write(`gstack-ci-carry: ${m}\n`); process.exit(2); };
if (!cmd || argv.includes('--help') || argv.includes('-h')) { out(HELP); process.exit(cmd ? 0 : 2); }
if (cmd !== 'find' && cmd !== 'wait') usage(`unknown command ${cmd}; run gstack-ci-carry --help`);
const policyFrom = flag('--policy-from') ?? 'base';
if (policyFrom !== 'base' && policyFrom !== 'head') usage('--policy-from needs base or head');
if (simulate !== (policyFrom === 'head') || (simulate && cmd !== 'find')) usage('find --policy-from head --simulate go together: a branch\'s own map is only ever simulated, never carried');
const git = (args: string[]) => { const r = spawnSync('git', args, { encoding: 'utf8', timeout: 30_000 }); return r.status === 0 ? r.stdout.trim() : ''; };
const root = git(['rev-parse', '--show-toplevel']);
if (!root) usage('not inside a git repository');
const base = resolveBaseBranch(root, flag('--base'));
const loaded = loadConfigFromBase(root, base);
if (isConfigUnavailable(loaded) || loaded.errors.length) { out(`CI_CARRY: none (profiles_unavailable: origin/${base}'s .gstack/pregate.json is unreadable or invalid)`); process.exit(1); }
const loaded = simulate ? loadConfigFromHead(root) : loadConfigFromBase(root, base);
if (simulate) out(`policy ${isConfigUnavailable(loaded) ? 'unavailable' : loaded.label}; a dry run: nothing is written and nothing clears (a carry reads origin/${base}'s map only)`);
if (isConfigUnavailable(loaded) || loaded.errors.length) { out(`CI_CARRY: none (profiles_unavailable: ${simulate ? 'the branch\'s' : `origin/${base}'s`} .gstack/pregate.json is unreadable or invalid)`); process.exit(1); }
const command = flag('--command');
const mapping = command !== undefined ? loaded.config.ship_lanes_from[command] : undefined;
if (command !== undefined && !mapping) { out(`CI_CARRY: none (not_mapped: ${JSON.stringify(command)} is not a key of origin/${base}'s ship_lanes_from)`); process.exit(1); }
const mapping = command !== undefined ? laneMapping(loaded.config.ship_lanes_from, command) : undefined;
if (command !== undefined && !mapping) { out(`CI_CARRY: none (not_mapped: ${JSON.stringify(command)} is neither a key nor a declared alias in origin/${base}'s ship_lanes_from)`); process.exit(1); }
if (mapping && !argv.includes('--allow-repo-commands')) usage('--command runs the mapping\'s inventory, a pin-file command: pass --allow-repo-commands');
const profiles = all('--profile').length ? all('--profile') : mapping ? mapping.profiles : [...new Set(Object.values(loaded.config.ship_lanes_from).flatMap(m => m.profiles))];
if (!profiles.length) { out(`CI_CARRY: none (no_profile: origin/${base} maps no lane in ship_lanes_from; pass --profile <name>)`); process.exit(1); }
const check = buildRunCheck(root, null, argv.includes('--allow-repo-commands'), base);
const check = buildRunCheck(root, null, argv.includes('--allow-repo-commands'), base, simulate ? 'head' : 'base');
if ('error' in check) { out(`CI_CARRY: none (profiles_unavailable: ${check.error})`); process.exit(1); }
const head = git(['rev-parse', 'HEAD']);
const tree = git(['rev-parse', 'HEAD^{tree}']);
function find(): number {
const wtree = () => { const fp = wtreeFingerprint({ cwd: root }); return 'tree' in fp ? fp.tree : null; };
const v = findCarry(check as Exclude<typeof check, { error: string }>, { profiles, head, tree, wtree, ...(mapping ? { inventory: () => runInventory(mapping.inventory, root) } : {}) });
const v = findCarry(check as Exclude<typeof check, { error: string }>, { profiles, head, tree, wtree, ...(mapping ? mappingCoverage(mapping, root) : {}) });
if (argv.includes('--json')) out(JSON.stringify(v));
else out(v.ok ? `CI_CARRY: accepted ${v.receipt}` : `CI_CARRY: none (${v.reason}: ${v.detail})`);
return v.ok ? 0 : 1;
+2 -1
View File
@@ -97,6 +97,7 @@ import { writeReceipt, writeOutcome } from '../lib/egress-receipt';
import { readConfigKey, resolveStateRoot } from '../lib/state-root';
import { DESIGN_SLOP_CATALOG, entryForImpeccableId } from '../lib/design-catalog';
import { isFrontendPath } from '../lib/frontend-scope';
import { baseArg } from '../lib/gate-diff/git';
// ── Environment ──────────────────────────────────────────────────────────────
@@ -821,7 +822,7 @@ function resolveTargets(args: ScanArgs, p: Probe): { targets: string[]; refusedB
for (const rel of r.stdout.toString('utf-8').split('\0')) if (rel) files.add(rel);
return true;
};
if (!runZ(['diff', '-z', '--name-only', '--diff-filter=ACMR', `${base}...HEAD`])) {
if (!runZ(['diff', '-z', '--name-only', '--diff-filter=ACMR', `${baseArg('gstack-design-detect', base, top)}...HEAD`])) {
// A base that does not resolve must not read as "no frontend changes".
refuse(base, 'git diff against this base failed (unknown ref or unfetched base)');
return { targets: out, refusedBase: true };
+22 -1
View File
@@ -128,6 +128,26 @@ _session_data() {
return "$r"
}
# An auth path holds a token, split the way lib/review-risk.ts tokens() splits
# paths (camelCase, then every non-alphanumeric run), that contains auth
# (GST-1065). It fails safe: every such token raises the class except the
# authorship words below, so author-spec.md, AUTHORS, authority.ts and
# coauthors.ts do not, while auth.ts, /auth/, nextauthmiddleware.ts,
# noauthrequired.ts, unauthenticated.ts and HTTPAuthorization.ts do.
_auth_path() {
local s="$1" t r=1
while [[ "$s" =~ ^(.*[a-z0-9])([A-Z].*)$ ]]; do s="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"; done
shopt -s nocasematch
for t in ${s//[^a-zA-Z0-9]/ }; do
case "$t" in
author|authors|authored|authoring|authorship|authority|authorities|coauthor|coauthors) ;;
*auth*) r=0; break ;;
esac
done
shopt -u nocasematch
return "$r"
}
_print_flags() {
echo "SCOPE_FRONTEND=$FRONTEND"
echo "SCOPE_BACKEND=$BACKEND"
@@ -293,9 +313,10 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
# named for sessions (recovered-sessions.jsonl, GST-911) holds rows, not
# session handling, so it is not auth; session config still is.
case "$f" in
*auth*|*jwt*|*oauth*|*permission*|*role*) m_auth=true ;;
*jwt*|*permission*|*role*) m_auth=true ;;
*session*) _session_data "$f" || m_auth=true ;;
esac
_auth_path "$f" && m_auth=true
# Backend: code that isn't a frontend component/view file. Includes ESM/CJS
# (.mjs/.cjs) and explicit-module TS (.mts/.cts) — #1810: these matched no
+2 -1
View File
@@ -38,6 +38,7 @@ import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { baseContainsHead } from '../lib/default-base';
import { baseArg } from '../lib/gate-diff/git';
import { resultLine } from '../lib/result-codes';
import { parseReceiptDocs, type ReceiptDocs } from '../lib/ship-receipt-docs';
@@ -138,7 +139,7 @@ function main(argv: string[]): void {
if (out === top || out.startsWith(top + path.sep)) throw new Error('--out must be outside the repository');
const mode = one(args, '--mode');
if (mode !== 'edit' && mode !== 'read-only') throw new Error('--mode must be edit or read-only');
const base = git(['rev-parse', '--verify', one(args, '--base') + '^{commit}']).trim();
const base = git(['rev-parse', '--verify', baseArg('gstack-docs-candidate', one(args, '--base'), process.cwd()) + '^{commit}']).trim();
const self = baseContainsHead(top, base);
if (self) throw new Error(resultLine('snapshot', 'BASE_CONTAINS_HEAD', self));
const exceptionFile = flag(args, '--exception')[0];
+1
View File
@@ -258,6 +258,7 @@ gstack_result_anchor() {
DX_CAPTURE_REDACTION_FAILED) printf '%s' 'dx-capture-redaction-failed' ;;
DX_CAPTURE_INTERNAL) printf '%s' 'dx-capture-internal' ;;
POLICY_GSTACK_TOO_OLD) printf '%s' 'policy-gstack-too-old' ;;
PREGATE_GSTACK_TOO_OLD) printf '%s' 'pregate-gstack-too-old' ;;
QUEUE_NOT_READY) printf '%s' 'queue-not-ready' ;;
QUEUE_PROOF_MISSING) printf '%s' 'queue-proof-missing' ;;
QUEUE_STAMP_NOT_STAMP_ONLY) printf '%s' 'queue-stamp-not-stamp-only' ;;
+89 -24
View File
@@ -5,7 +5,8 @@
*
* gstack-evidence run [--reuse [--max-age <hours>]] --label <L> -- <cmd...>
* gstack-evidence record-ci --label <L> --base <base> -- <cmd>
* gstack-evidence carried [--json]
* gstack-evidence passed -- <cmd>
* gstack-evidence carried [--base <branch>] [--json]
* gstack-evidence check [--label <L> [--expect-cmd <exact string>]]... | --all
* [--max-age <hours>] [--allow-paths <csv>]
* gstack-evidence bundle --out <file> [--label <L>]... [--require <csv>] [--selection L=<text>]...
@@ -65,8 +66,11 @@
* `record-ci` writes the explicit CI-sourced row for a lane CI carried: it
* finds and verifies the run itself (lib/ci-carry.ts) for a command the base's
* ship_lanes_from maps, then records source "ci", the runs and profiles, the
* run's finish time as ts, bound to HEAD's clean tree; `carried` lists those
* rows at the current tree for the ship receipt.
* run's finish time as ts, bound to HEAD's clean tree, and the policy that
* mapped it (`origin/<base>@<sha>:…`); `carried` lists those rows at the
* current tree for the ship receipt, only when that policy is origin/<the
* receipt's base> (GST-1190: a row a run against another base wrote never
* counts).
*
* MACHINE-LOCAL by design: neither the ledger nor the logs are brain-synced.
* A synced record citing an unsynced log would grade FRESH on a machine where
@@ -90,10 +94,10 @@ import { resultLine } from "../lib/result-codes";
import { isUnavailable, loadPolicyFromBase, resolveBaseBranch } from "../lib/ship-policy";
import { versionAtRef } from "../lib/restamp";
import { bashCommandInvocation, runScriptSync, wtreeFingerprint } from "../lib/script-invocation";
import { findCarry } from "../lib/ci-carry";
import { findCarry, laneMapping, mappingCoverage, perFileCommand } from "../lib/ci-carry";
import { runChanged } from "../lib/run-evidence";
import { isConfigUnavailable, loadConfigFromBase } from "../lib/pregate/config";
import { buildRunCheck } from "../lib/pregate/run-check";
import { runInventory } from "../lib/run-evidence";
const BIN_DIR = dirname(Bun.fileURLToPath(import.meta.url));
const LOG_MAX_BYTES = 2 * 1024 * 1024;
@@ -122,10 +126,17 @@ interface EvidenceRecord {
source?: "ci";
runs?: string[];
profiles?: string[];
/** The CI jobs a jobs-mapped lane carried from (record-ci, GST-1189); tests_ran is then "n/a". */
jobs?: string[];
/** The base policy that mapped a carried lane (record-ci), as `origin/<base>@<sha>:.gstack/pregate.json`. */
policy?: string;
/** The runner's own test count (executed for bun/jest/vitest/pytest/rspec; ran with skips included for gstack's shard runner and the remote gate), 0 for ZERO-RUN, or "unknown" when no supported summary was seen. */
tests_ran?: number | "unknown";
tests_ran?: number | "unknown" | "n/a";
/** "files" when a carried narrow lane's tests_ran counts its inventory files the run covered (record-ci), not tests. */
tests_unit?: "files";
/** Why a carried lane was not owed at all: every job it names was path-skipped by an unchanged gate (record-ci, GST-1190). */
not_owed?: string;
skipped_here?: { label: string; files: string[]; tests_ran: number | "unknown" };
}
/** Bytes of streamed output kept for the tests_ran parse (the 2MB log would truncate the summary). */
@@ -598,6 +609,36 @@ function identityDigest(): string {
return sha256(JSON.stringify({ bun: version("bun"), node: version("node"), os: process.platform, arch: process.arch, image: process.env.GSTACK_RUNNER_IMAGE || null }));
}
/** A local row that passed with a counted run within the max age, under this machine's identity. */
function countedPass(r: EvidenceRecord, maxAgeHours: number): boolean {
if (r.exit !== 0 || typeof r.tests_ran !== "number" || r.tests_ran <= 0) return false;
const age = Date.now() - Date.parse(r.ts);
if (!(age >= 0 && age <= maxAgeHours * 3600_000)) return false;
return r.identity === identityDigest();
}
/**
* `passed -- <cmd>` (GST-1186, read-only): did this exact command pass here at the current working tree under any label? The
* newest local row for the command hash and tree decides, so a newer failure vetoes; it must be a counted pass (countedPass).
* CI rows never count: a CI pass counts only through record-ci's live check against the base. Prints PASSED: label=<L>
* source=local ts=<ts> and exits 0, else NOT_PASSED: <reason> and exits 1. gstack-pregate's guards read it.
*/
function cmdPassed(argv: string[]): number {
const sep = argv.indexOf("--");
const command = sep < 0 ? "" : argv.slice(sep + 1).join(" ");
if (!command || sep !== 0) { console.error("usage: gstack-evidence passed -- <command>"); return 2; }
const wtree = currentWtree();
if (!wtree) { console.log("NOT_PASSED: no working-tree fingerprint"); return 1; }
let records: EvidenceRecord[] = [];
try { records = readJsonl<EvidenceRecord>(ledgerPath().file); } catch { records = []; }
const cmdSha = sha256(command);
const newest = records.findLast((r) => r.cmd_sha256 === cmdSha && r.wtree === wtree && r.source === undefined);
if (!newest) { console.log(`NOT_PASSED: no local ledger row for this command at tree ${wtree.slice(0, 12)}`); return 1; }
if (!countedPass(newest, 24)) { console.log(`NOT_PASSED: the newest row (label=${newest.label} ts=${newest.ts}) is not a counted pass within 24 h under this runtime`); return 1; }
console.log(`PASSED: label=${newest.label} source=local ts=${newest.reused_from ?? newest.ts}`);
return 0;
}
/**
* `run --reuse`: the newest local row for this label, command and working tree decides. A counted pass under the same
* identity within the max age is copied (original ts kept) and printed as a run would be; anything else runs the command.
@@ -612,10 +653,7 @@ function reused(file: string, label: string, cmdSha: string, maxAgeHours: number
return false;
}
const newest = records.findLast((r) => r.label === label && r.cmd_sha256 === cmdSha && r.wtree === wtree && r.source === undefined);
if (!newest || newest.exit !== 0 || typeof newest.tests_ran !== "number" || newest.tests_ran <= 0) return false;
const age = Date.now() - Date.parse(newest.ts);
if (!(age >= 0 && age <= maxAgeHours * 3600_000)) return false;
if (newest.identity !== identityDigest()) return false;
if (!newest || !countedPass(newest, maxAgeHours)) return false;
try {
appendJsonl(file, { ...newest, reused_from: newest.reused_from ?? newest.ts }, { mode: 0o600 });
} catch (e: any) {
@@ -629,9 +667,11 @@ function reused(file: string, label: string, cmdSha: string, maxAgeHours: number
/**
* `record-ci`: carry a lane from CI. Its command must be a key of the base branch's ship_lanes_from; the run is found and
* verified here (lib/ci-carry.ts) at HEAD's clean tree, so no caller supplies run ids, a test count or a tree. Never runs the lane.
* verified here (lib/ci-carry.ts) at HEAD's clean tree, so no caller supplies run ids, a test count or a tree. Never runs the lane;
* inventory files every run skipped entirely run here through the entry's per_file runner (label <L>.skipped) first, and the CI
* row is written only when that run passes at the same tree.
*/
function cmdRecordCi(argv: string[]): number {
async function cmdRecordCi(argv: string[]): Promise<number> {
const sep = argv.indexOf("--");
const opts = sep < 0 ? argv : argv.slice(0, sep);
const get = (name: string) => { const i = opts.indexOf(name); return i >= 0 ? opts[i + 1] : undefined; };
@@ -651,17 +691,33 @@ function cmdRecordCi(argv: string[]): number {
const loaded = loadConfigFromBase(cwd, base);
if (isConfigUnavailable(loaded)) return none("config_unavailable", loaded.detail);
if (loaded.errors.length) return none("config_invalid", loaded.errors.map((x) => `${x.path}: ${x.message}`).join("; "));
const from = loaded.config.ship_lanes_from;
if (!Object.prototype.hasOwnProperty.call(from, command)) return none("not_mapped", `the command is not a key of origin/${base}'s ship_lanes_from`);
const m = from[command]!;
const m = laneMapping(loaded.config.ship_lanes_from, command);
if (!m) return none("not_mapped", `the command is neither a key nor a declared alias in origin/${base}'s ship_lanes_from`);
const check = buildRunCheck(cwd, null, true, base);
if ("error" in check) return none("profiles_unavailable", check.error);
const head = git(["rev-parse", "HEAD"]);
const tree = git(["rev-parse", "HEAD^{tree}"]);
if (!head || !tree) return none("no_head", "HEAD is unreadable");
const wtree = () => { const fp = wtreeFingerprint({ cwd, timeoutMs: 30000 }); return "tree" in fp ? fp.tree : null; };
const v = findCarry(check, { profiles: m.profiles, head, tree, wtree, inventory: () => runInventory(m.inventory, cwd) });
const v = findCarry(check, { profiles: m.profiles, head, tree, wtree, ...mappingCoverage(m, cwd) });
if (!v.ok) return none(v.reason, v.detail);
let here: EvidenceRecord["skipped_here"];
if (v.remainder?.length) {
const hereLabel = `${label}.skipped`;
console.error(`gstack-evidence record-ci: ${v.remainder.length} inventory file(s) skipped every test in CI; running them here (label=${hereLabel})`);
const hereCommand = perFileCommand(m.per_file!, v.remainder);
const exit = await cmdRun(["--label", hereLabel, "--", hereCommand]);
if (exit !== 0) return none("skipped_failed", `the per_file run of ${v.remainder.length} skipped file(s) exited ${exit} (label=${hereLabel})`);
let rows: EvidenceRecord[] = [];
try { rows = readJsonl<EvidenceRecord>(ledgerPath().file); } catch { rows = []; }
here = { label: hereLabel, files: v.remainder, tests_ran: rows.findLast((r) => r.label === hereLabel && r.source === undefined && r.cmd_sha256 === sha256(hereCommand) && r.wtree === tree)?.tests_ran ?? "unknown" };
if (typeof here.tests_ran !== "number" || here.tests_ran <= 0) return none("skipped_zero_run", `the per_file run of ${v.remainder.length} skipped file(s) ran ${here.tests_ran} tests here (label=${hereLabel}); a lane passes on its runner count, never exit 0 alone`);
for (const e of v.runs) {
const changed = runChanged(check.deps, e.run_id, e.run_attempt);
if (changed) return none("run_changed", changed);
}
if (wtree() !== tree) return none("tree_changed", `the working tree changed while the per_file run of label=${hereLabel} ran`);
}
let paths: ReturnType<typeof ledgerPath>;
try {
paths = ledgerPath();
@@ -673,15 +729,22 @@ function cmdRecordCi(argv: string[]): number {
const runs = v.runs.map((r) => `${r.run_id}/${r.run_attempt}`);
const rec: EvidenceRecord = {
ts: new Date(v.runs.map((r) => r.finished_at).sort()[0]!).toISOString(), label, command, cmd_sha256: sha256(command), exit: 0, duration_s: 0,
commit: head, tree, dirty: false, wtree: tree, tests_ran: v.tests, source: "ci", runs, profiles: m.profiles, policy: loaded.label,
commit: head, tree, dirty: false, wtree: tree, tests_ran: v.tests, source: "ci", runs, profiles: m.profiles, policy: loaded.label, ...(m.jobs ? { jobs: m.jobs } : {}), ...(v.unit ? { tests_unit: v.unit } : {}), ...(v.notOwed ? { not_owed: v.notOwed } : {}), ...(here ? { skipped_here: here } : {}),
};
appendJsonl(paths.file, rec, { mode: 0o600 });
console.error(`gstack-evidence: recorded label=${label} exit=0 tests_ran=${v.tests} log=- source=ci runs=${runs.join(",")} policy=${loaded.label}`);
console.error(`gstack-evidence: recorded label=${label} exit=0 tests_ran=${v.tests}${v.unit ? ` unit=${v.unit}` : ""} log=- source=ci runs=${runs.join(",")} policy=${loaded.label}${here ? ` skipped_here=${here.files.length} tests_ran_here=${here.tests_ran}` : ""}`);
if (v.notOwed) console.error(`gstack-evidence: not owed: ${v.notOwed}`);
return 0;
}
/** `carried`: each label's newest row when it is a CI-sourced row at the current working tree, as the receipt's ci_carried entries. */
/**
* `carried`: each label's newest row when it is a CI-sourced row at the current working tree whose mapping policy is
* origin/<base> (the receipt's base; default the resolved base branch), as the receipt's ci_carried entries.
*/
function cmdCarried(argv: string[]): number {
const at = argv.indexOf("--base");
if (at >= 0 && !argv[at + 1]) { console.error("usage: gstack-evidence carried [--base <branch>] [--json]"); return 2; }
const policyRef = `origin/${(at >= 0 ? argv[at + 1]! : resolveBaseBranch(git(["rev-parse", "--show-toplevel"]) ?? process.cwd())).replace(/^origin\//, "")}@`;
let records: EvidenceRecord[] = [];
try {
records = readJsonl<EvidenceRecord>(ledgerPath().file);
@@ -691,7 +754,7 @@ function cmdCarried(argv: string[]): number {
const wtree = currentWtree();
const latest = new Map<string, EvidenceRecord>();
for (const r of records) latest.set(r.label, r);
const carried = [...latest.values()].filter((r) => r.source === "ci" && !!wtree && r.wtree === wtree && Array.isArray(r.runs))
const carried = [...latest.values()].filter((r) => r.source === "ci" && !!wtree && r.wtree === wtree && Array.isArray(r.runs) && typeof r.policy === "string" && r.policy.startsWith(policyRef))
.flatMap((r) => r.runs!.map((ref) => { const [run, attempt] = ref.split("/").map(Number); return { lane: r.label, run, attempt, tree: r.wtree! }; }));
if (argv.includes("--json")) console.log(JSON.stringify(carried));
else for (const c of carried) console.log(`CI_CARRIED: ${c.lane} run=${c.run}/${c.attempt} tree=${c.tree.slice(0, 12)}`);
@@ -756,10 +819,10 @@ function cmdCheck(argv: string[]): number {
allFresh = false;
continue;
}
const testsRan = typeof latest.tests_ran === "number" ? latest.tests_ran : "unknown";
const testsRan = typeof latest.tests_ran === "number" || latest.tests_ran === "n/a" ? latest.tests_ran : "unknown";
const changedDuringRun = Array.isArray(latest.wtree_changed) ? latest.wtree_changed.filter((p): p is string => typeof p === "string" && p !== "" && !/[\u0000-\u001f\u007f]/.test(p)).slice(0, 5) : [];
const ci = latest.source === "ci" && Array.isArray(latest.runs) ? ` source=ci runs=${latest.runs.filter((s) => /^\d+\/\d+$/.test(s)).join(",")}` : "";
const detail = `label=${label} exit=${latest.exit} tests_ran=${testsRan} ts=${latest.ts}${latest.log_path ? ` log=${latest.log_path}` : ""}${ci}`;
const detail = `label=${label} exit=${latest.exit} tests_ran=${testsRan}${latest.tests_unit === "files" ? " unit=files" : ""} ts=${latest.ts}${latest.log_path ? ` log=${latest.log_path}` : ""}${ci}`;
let verdict: "FRESH" | "STALE" | "ZERO-RUN" = "FRESH";
let reason = "";
if (latest.exit !== 0) {
@@ -911,7 +974,9 @@ try {
if (sub === "run") {
process.exit(await cmdRun(rest));
} else if (sub === "record-ci") {
process.exit(cmdRecordCi(rest));
process.exit(await cmdRecordCi(rest));
} else if (sub === "passed") {
process.exit(cmdPassed(rest));
} else if (sub === "carried") {
process.exit(cmdCarried(rest));
} else if (sub === "check") {
@@ -923,7 +988,7 @@ try {
} else if (sub === "verify") {
process.exit(cmdVerify(rest));
} else {
console.error("usage: gstack-evidence run|record-ci|carried|check|bundle|ancestor|verify ...");
console.error("usage: gstack-evidence run|record-ci|passed|carried|check|bundle|ancestor|verify ...");
process.exit(2);
}
} catch (e: any) {
+3 -3
View File
@@ -32,7 +32,7 @@ import * as path from 'node:path';
import * as os from 'node:os';
import { scanCandidate, parseGateIntegrityBlock, specDirectoryHoldsSpecs, type FileDiff, type CommitInfo } from '../lib/gate-diff/index';
import {
commitInfo, commitsInRange, defaultBase, diffCommit, diffWorkingTree, firstParent, headSha, mergeBase, noBaseFix, readClaudeMd, repoRoot,
baseNote, commitInfo, commitsInRange, defaultBase, diffCommit, diffWorkingTree, firstParent, headSha, mergeBase, noBaseFix, readClaudeMd, repoRoot,
resolveBase, resolvesToCommit, treePaths, untrackedAsAdded,
} from '../lib/gate-diff/git';
import { baseContainsHead, noDefaultBaseLine } from '../lib/default-base';
@@ -162,8 +162,8 @@ function main(argv: string[]): number {
process.stdout.write(`GATE_ERROR=no_base ref=${baseRef} fix=${noBaseFix(baseRef, root)}\n`);
return 2;
}
if (base.local === 'fallback') console.error(`gstack-gate-diff: ${base.unrelatedRemote ? `${base.unrelatedRemote} shares no history with HEAD` : `no remote-tracking branch for ${baseRef}`}; base is the local ${baseRef}, which may lag its remote`);
else if (base.local) console.error(`gstack-gate-diff: ${{ unrelated: `local ${baseRef} shares no history with HEAD`, missing: `no local branch ${baseRef}`, behind: `local ${baseRef} is behind ${base.ref}`, ahead: `local ${baseRef} is ahead of ${base.ref}`, diverged: `local ${baseRef} has diverged from ${base.ref}` }[base.local]}; base is ${base.ref}`);
const note = baseNote(baseRef, base);
if (note) console.error(`gstack-gate-diff: ${note}`);
const mb = mergeBase(base.sha, head, root);
if (!mb) {
process.stdout.write(`GATE_ERROR=no_base ref=${baseRef} fix=${noBaseFix(baseRef, root)}\n`);
+26 -6
View File
@@ -7,6 +7,7 @@
* [--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--run <id>] [--from-run <id|url>] [--no-ci-carry] [--explain] [--json]
* gstack-pregate verify [--out <pregate.json>] [--json] publication check: current tree, no fail/incomplete, obligations cleared
* gstack-pregate clear --remote <workflow.yml/job> --run-url <url> | --run <id|url> [--run …] [--allow-repo-commands] | --bundle-lane <label> | --receipt <file> [--out <pregate.json>]
* [--flaky-exception "<test file>::<case>" … --pr <n> [--grant <approval id>]] a run with failures, under the owner's grant
* gstack-pregate lanes [--explain] the lane list and platform tiers read from .github/workflows
* gstack-pregate validate | init [--explain] [--force] the working-tree .gstack/pregate.json
* gstack-pregate sealed [--range <a..b>] [--remote <name>] [--base <branch>] [--no-skip] [--json]
@@ -36,6 +37,7 @@ import { CHECKS, CHECK_IDS, PREGATE_OUT_REL, PREGATE_TIER2, clearDecision, clear
import { describeLane, discoverLanes } from '../lib/pregate/workflows';
import { REMOTE_GATE_LANE } from '../lib/pregate/checks/lanes';
import { describeSubstitution } from '../lib/pregate/remote-receipt';
import { approvalGrants, describeExceptions, type FlakyInput } from '../lib/pregate/flaky-exception';
import { ensureTmpExcluded } from '../lib/restamp';
import { RESULT_CODES, resultCodeSuffix, resultLine } from '../lib/result-codes';
import { DEFAULT_STALL_TIMEOUT_S } from '../lib/pregate/context';
@@ -58,7 +60,10 @@ const HELP = `gstack-pregate — cheap checks before anything paid or remote (do
verifies GitHub Actions runs against the base branch's run_profiles instead (docs/pregate.md#run-evidence):
one run of the profile named after the lane, or for pregate.json/remote_gate one run per
remote_gate_runs profile covering the gate's inventory (needs --allow-repo-commands); where a lane has a
profile, --run-url is verified the same way
profile, --run-url is verified the same way. --flaky-exception "<test file>::<case>" (at most 3) --pr <n>
--grant <approval id> takes such runs with failures when every failure is named from the run's shard
results and logs, outside the PR's diff and import closure, and an owner's gstack-approvals row binds
them; without --grant it prints the row to record (pregate only; docs/pregate.md#flaky-exception)
lanes print every CI lane with its platform tier (from .github/workflows/*.yml)
validate | init the working-tree ${PREGATE_CONFIG_PATH}
sealed [--range <a..b>] [--remote <name>] [--base <branch>] [--no-skip]
@@ -117,7 +122,7 @@ function flag(args: string[], name: string): string | undefined { const i = args
const VERB_FLAGS: Record<string, [string[], string[]]> = {
run: [['--stage', '--only', '--base', '--policy-from', '--out', '--timeout', '--stall-timeout', '--spend-cap', '--run', '--from-run'], ['--allow-repo-commands', '--explain', '--json', '--remote', '--no-ci-carry']],
verify: [['--out'], ['--json']],
clear: [['--remote', '--run-url', '--run', '--bundle-lane', '--receipt', '--out'], ['--allow-repo-commands']],
clear: [['--remote', '--run-url', '--run', '--bundle-lane', '--receipt', '--out', '--flaky-exception', '--pr', '--grant'], ['--allow-repo-commands']],
lanes: [[], ['--explain', '--json']],
validate: [[], ['--json']],
init: [[], ['--explain', '--force']],
@@ -223,7 +228,12 @@ function cmdVerify(root: string, rest: string[], json: boolean): number {
if (!read.file) { for (const e of read.errors) process.stderr.write(resultLine('gstack-pregate verify', e.code, `${e.message} at ${e.path}`) + '\n'); return EXIT.fail; }
const v = verifyPregate(root, read.file);
if (json) { process.stdout.write(JSON.stringify({ ...v, verdict: read.file.verdict, tree: read.file.tree, head: read.file.head, out }) + '\n'); return v.ok ? EXIT.ok : EXIT.fail; }
if (v.ok) { process.stdout.write(`PREGATE_VERIFY: ok ${v.message}\n`); return EXIT.ok; }
if (v.ok) {
const excepted = describeExceptions(read.file);
if (excepted) process.stdout.write(`PREGATE: ${excepted}\n`);
process.stdout.write(`PREGATE_VERIFY: ok ${v.message}\n`);
return EXIT.ok;
}
process.stderr.write(resultLine('gstack-pregate verify', v.code!, v.message) + '\n');
for (const r of v.uncleared) process.stdout.write(`requires-remote: ${r.lane} (${r.platform}) ${r.tests.length} test(s) uncleared\n`);
process.stdout.write(`PREGATE_VERIFY: refused ${v.code}\n`);
@@ -233,6 +243,14 @@ function cmdVerify(root: string, rest: string[], json: boolean): number {
function cmdClear(root: string, rest: string[]): number {
const lane = flag(rest, '--remote');
if (!lane) { process.stderr.write(`gstack-pregate clear: --remote <workflow.yml/job> with --run-url <url>, --bundle-lane <label> or (for ${REMOTE_GATE_LANE}) --receipt <file> is required\n`); return EXIT.usage; }
const cases = rest.flatMap((a, i) => a === '--flaky-exception' ? [rest[i + 1]!] : []);
let flaky: FlakyInput | undefined;
if (cases.length) {
if (!rest.includes('--run') && !rest.includes('--run-url')) { process.stderr.write('gstack-pregate clear: --flaky-exception needs --run <id|url>: an exception reads GitHub Actions runs\n'); return EXIT.usage; }
const pr = Number(flag(rest, '--pr'));
if (!Number.isInteger(pr) || pr <= 0) { process.stderr.write('gstack-pregate clear: --flaky-exception needs --pr <n>, the pull request the grant binds\n'); return EXIT.usage; }
flaky = { cases, pr, grant: flag(rest, '--grant'), grantRow: approvalGrants(root) };
} else if (flag(rest, '--pr') !== undefined || flag(rest, '--grant') !== undefined) { process.stderr.write('gstack-pregate clear: --pr and --grant apply only with --flaky-exception\n'); return EXIT.usage; }
const out = outPath(root, rest);
const read = fs.existsSync(out) ? readPregateFile(out) : { file: null, errors: [] };
if (!read.file) { process.stderr.write(resultLine('gstack-pregate clear', 'PREGATE_STALE', `${out} is missing or invalid; run gstack-pregate first`) + '\n'); return EXIT.fail; }
@@ -242,7 +260,7 @@ function cmdClear(root: string, rest: string[]): number {
const checked = runs.length || runUrl || bundleLane ? buildRunCheck(root, lane, rest.includes('--allow-repo-commands')) : undefined;
if (checked && 'error' in checked && (runs.length || checked.profiled)) { process.stderr.write(`gstack-pregate clear: ${checked.error}${checked.profiled ? `; ${lane} may have a run profile on the base branch, so it clears only with a verified --run` : ''}\n`); return EXIT.fail; }
const evidence = checked && !('error' in checked) ? checked : undefined;
const decision = clearDecision(read.file, lane, { runUrl, runs, bundleLane, receiptPath: flag(rest, '--receipt'), evidence }, root);
const decision = clearDecision(read.file, lane, { runUrl, runs, bundleLane, receiptPath: flag(rest, '--receipt'), evidence, flaky }, root);
if ('refused' in decision) {
if (decision.refused === 'usage') { process.stderr.write(`gstack-pregate clear: ${decision.message}\n`); return EXIT.usage; }
process.stderr.write(`gstack-pregate clear: receipt refused — ${decision.message}; fix: ${decision.fix ?? RESULT_CODES.PREGATE_RECEIPT_INVALID.fix} ${resultCodeSuffix('PREGATE_RECEIPT_INVALID')}\n`);
@@ -250,9 +268,11 @@ function cmdClear(root: string, rest: string[]): number {
}
const current = readPregateFile(out).file;
if (!current || current.tree !== read.file.tree || current.generated_at !== read.file.generated_at) { process.stderr.write(resultLine('gstack-pregate clear', 'PREGATE_STALE', `${out} changed while ${lane} was being verified; rerun the clear against the new file`) + '\n'); return EXIT.fail; }
const file = clearObligation(current, lane, decision.receipt, `gstack-pregate clear ${new Date().toISOString()}`, decision.substitution);
const file = clearObligation(current, lane, decision.receipt, `gstack-pregate clear ${new Date().toISOString()}`, decision.substitution, decision.exception);
writePregateFile(root, out, file);
if (/ runs=\S+ profiles=/.test(decision.receipt)) process.stdout.write(`REUSE: accepted ${lane} ${decision.receipt}\n`);
const e = decision.exception;
if (e) process.stdout.write(`FLAKY_EXCEPTION: accepted ${lane} cases=${e.cases.join(' | ')} run=${e.runs.join(',')} grant=${e.grant} by=${e.by} (pregate only; every required CI check still needs its own green run at this head)\n`);
else if (/ runs=\S+ profiles=/.test(decision.receipt)) process.stdout.write(`REUSE: accepted ${lane} ${decision.receipt}\n`);
if (decision.substitution) process.stdout.write(`PREGATE_RECEIPT_SUBSTITUTED: ${describeSubstitution(decision.substitution)}\n`);
process.stdout.write(`PREGATE_CLEARED: ${lane} receipt=${decision.receipt} verdict=${file.verdict}${decision.attested ? ` (attested, not validated: ${lane} has no machine-checkable receipt)` : ''}\n`);
if (checked && 'error' in checked) process.stderr.write(`gstack-pregate clear: run profiles unavailable (${checked.error}); ${lane} was recorded without run verification\n`);
+2 -1
View File
@@ -18,6 +18,7 @@
*/
import * as fs from 'node:fs';
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
import { baseArg } from '../lib/gate-diff/git';
import { lintFiles, renderProseLint } from '../lib/prose-lint';
import { resultLine } from '../lib/result-codes';
@@ -54,7 +55,7 @@ function main(argv: string[]): number {
if ([base, sections, flags.has('--unreleased') || undefined].filter(Boolean).length > 1) { process.stderr.write('gstack-prose-lint: --unreleased, --base and --sections are different selections; pass one\n'); return EXIT.usage; }
for (const f of files) if (!fs.existsSync(f) || fs.statSync(f).isDirectory()) { process.stderr.write(`gstack-prose-lint: cannot read ${f}\n`); return EXIT.usage; }
let result;
try { result = lintFiles(files, { unreleased: flags.has('--unreleased'), base, sections }); }
try { result = lintFiles(files, { unreleased: flags.has('--unreleased'), base: base && baseArg('gstack-prose-lint', base, process.cwd()), sections }); }
catch (e: any) { process.stderr.write(`gstack-prose-lint: ${e.message}\n`); return EXIT.usage; }
if (sections && flags.has('--gate') && result.empty.length) {
process.stderr.write(`gstack-prose-lint: no "## <section>" heading from --sections in ${result.empty.join(', ')}; nothing would be gated\n`);
+7 -4
View File
@@ -28,6 +28,7 @@ import { locate, readRecord } from '../lib/approval-record';
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
import { resultLine } from '../lib/result-codes';
import { runScriptSync } from '../lib/script-invocation';
import { baseNote } from '../lib/gate-diff/git';
import { collectCommit, collectLive } from '../lib/review-collect';
import { excludeLines, reviewExclude, type Exclusion } from '../lib/review-exclude';
import { isUnavailable } from '../lib/ship-policy';
@@ -126,8 +127,11 @@ function main(): number {
for (const e of collected.errors) process.stderr.write(`gstack-review-plan: git could not list the diff (${e.input}: ${e.detail}); fix: ${e.repair}; no plan is safer than a plan for an empty diff\n`);
return EXIT.fail;
}
const note = collected.base && baseNote(base, collected.base);
if (note) process.stderr.write(`gstack-review-plan: ${note}\n`);
const ref = collected.base?.ref ?? base;
const head = git(['rev-parse', 'HEAD'])?.trim() ?? 'unknown';
const self = collected.mergeBase === head ? baseContainsHead(root, git(['rev-parse', '--verify', '-q', `origin/${base}^{commit}`]) ? `origin/${base}` : base) : null;
const self = collected.mergeBase === head ? baseContainsHead(root, ref) : null;
if (self) { process.stderr.write(`${resultLine('gstack-review-plan', 'BASE_CONTAINS_HEAD', self)}; no plan is safer than a plan for a diff compared with itself\n`); return EXIT.fail; }
const binDir = path.dirname(Bun.fileURLToPath(import.meta.url));
const scopeOf = (args: string[]): Record<string, string> => {
@@ -145,7 +149,6 @@ function main(): number {
const gateTagsOf = (args: string[]): number | null => { const g = runGateDiff(args); return tagCount(g.stdout, g.status); };
const diffBase = collected.mergeBase ?? base;
const scope = scopeOf([diffBase]);
const ref = git(['rev-parse', '--verify', '-q', `origin/${base}^{commit}`]) !== null ? `origin/${base}` : base;
const baseSha = collected.registry.sha ?? git(['rev-parse', '--verify', '-q', `${ref}^{commit}`])?.trim() ?? '';
const policy = baseSha ? loadPolicyAt(root, baseSha, ref) : null;
const declares = !!policy && !isUnavailable(policy) && policy.policy.review_exclude_paths.length > 0;
@@ -193,11 +196,11 @@ function main(): number {
const gateScan = { tagged: gateTagged, head };
if (has('--json')) {
const { runtimeLines, stepDepth, depthReasons, ...rest } = plan;
process.stdout.write(JSON.stringify({ ...rest, targeted: { ...targeted, brief, targeted_patch_id: patch }, step_depth: stepDepth, runtime_lines: runtimeLines, depth_reasons: depthReasons, depth_floor: floor ?? null, gate_scan: gateScan, fix_range: fixRange ?? null, registry: { sha: registry.sha, outputs: registry.outputs }, per_path: perPath, diff_lines: diffLines, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly, consumed: collected.consumed, drawings: collected.drawings,
process.stdout.write(JSON.stringify({ ...rest, targeted: { ...targeted, brief, targeted_patch_id: patch }, step_depth: stepDepth, runtime_lines: runtimeLines, depth_reasons: depthReasons, depth_floor: floor ?? null, gate_scan: gateScan, fix_range: fixRange ?? null, registry: { sha: registry.sha, outputs: registry.outputs }, per_path: perPath, diff_lines: diffLines, base: collected.base, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly, consumed: collected.consumed, drawings: collected.drawings,
...(exclusion.state === 'head-only' ? { review_exclude: exclusion } : active ? { review_exclude: { state: 'active', ...active.record, specs: active.specs, keep: active.keep, kept_files: active.kept, content: active.content, references: active.references } } : {}) }) + '\n');
return EXIT.ok;
}
const out = [`REVIEW_CLASS: ${plan.class} (${plan.reasons.slice(0, 3).join('; ') || 'no signals'})`, reviewDepthLine(plan), `GATE_SCAN: ${gateTagged === null ? 'unavailable' : `tagged=${gateTagged}`} head=${head}`,
const out = [`REVIEW_CLASS: ${plan.class} (${plan.reasons.slice(0, 3).join('; ') || 'no signals'})`, reviewDepthLine(plan), `GATE_SCAN: ${gateTagged === null ? 'unavailable' : `tagged=${gateTagged}`} head=${head}`, `REVIEW_BASE: ${ref} ${baseSha.slice(0, 12)}`,
`TARGETED: classes=${classList(targeted.classes)} rules=${targeted.rules} brief=${brief} targeted_patch_id=${patch}${targeted.reasons.length ? ` reasons=${targeted.reasons.join('; ')}` : ''}`];
out.push(...excludeLines(exclusion, base));
if (fixRange) out.push(`FIX_RANGE: ${fixRange.runtime_lines === null ? `unread (${fixRange.error})` : `runtime_lines=${fixRange.runtime_lines}`} base=${fixRange.base}`);
+60 -30
View File
@@ -40,11 +40,12 @@ import { versionAtRef } from '../lib/restamp';
import { branchSides, stampOnlyLine, type BranchSide } from '../lib/stamp-only';
import { runScriptSync } from '../lib/script-invocation';
import { readPregateFile, verifyPregate } from '../lib/pregate/index';
import { exceptionConsumerProblem } from '../lib/pregate/flaky-exception';
import { isUnavailable, loadPolicyFromBase, matchesAny, policyUnavailableLine, resolveBaseBranch, runningGstackVersion, stampFilesInTree, validatePolicy, type ShipPolicy } from '../lib/ship-policy';
import { approvalForReceipt } from '../lib/approval-citations';
import {
CARRY_METADATA_GLOBS, CARRY_NAMED_PATHS, CARRY_TEST_GLOBS,
bindReceipt, carryReceipt, classifyCarryPath, computeCensus, extractReceipt, missingIssueLines, orderReceipt, parseReviewLoop, placeReceiptFirst, planCarry, planTreeCarry,
bindReceipt, carryReceipt, classifyCarryPath, computeCensus, extractReceipt, landedStackParent, missingIssueLines, orderReceipt, parseReviewLoop, placeReceiptFirst, planCarry, planTreeCarry,
receiptIssuesFrom, receiptSummaryLine, renderCensus, renderReceiptBlock, validateReceipt, type CensusPr,
} from '../lib/ship-receipt';
import { checkDocsCaps, docsCapLine, docsExceptionLine, parseReceiptDocs } from '../lib/ship-receipt-docs';
@@ -98,7 +99,9 @@ write flags: --pr --head --base --version --gated-tree --predecessor --gate <jso
unreadable input records full).
--review-exclude <digest|none> (the digest= on gstack-review-dispatch verify's REVIEW_EXCLUDE_RECORD: line, which the reviewers' briefs
name; none without that line): must equal the digest HEAD has under the base policy's review_exclude_paths, else
RECEIPT_INVALID field=review_exclude and nothing is written (rerun the review; write alone cannot clear it); recorded as review_exclude. Carry and the queue compare that digest.
RECEIPT_INVALID field=review_exclude and nothing is written; recorded as review_exclude. Carry and the queue compare that digest.
The check binds the digest to the base policy, not to a review: write does not read the review ledger, so the expected=
digest a refusal prints passes too. After a refusal, rerun the review and pass the digest its verify prints.
--review-targeted <json|@file> ({classes, rules, brief_sha, status, findings, raised_by, head}: gstack-review-dispatch verify
--targeted-out); recorded as review_targeted bound to the receipt's product_patch_id, and refused (RECEIPT_INVALID) when the
commit the reader read (head) is not an ancestor or a change since it carries a targeted class (gate scan included). write and carry
@@ -172,9 +175,9 @@ function reviewedProduct(repoRoot: string, base: string, head: string, pregateFi
return typeof id === 'string' ? { head_tree: tree, product_patch_id: id || 'none' } : { head_tree: tree };
}
/** The lanes CI carried at the checked-out tree, from this machine's evidence ledger (gstack-evidence carried); undefined when none. */
function ciCarried(): ShipReceipt['ci_carried'] {
const r = runScriptSync(path.join(path.dirname(Bun.fileURLToPath(import.meta.url)), 'gstack-evidence'), ['carried', '--json'], { encoding: 'utf8', timeout: 60_000 });
/** The lanes CI carried at the checked-out tree under origin/<base>'s mapping, from this machine's evidence ledger (gstack-evidence carried); undefined when none. */
function ciCarried(base: string): ShipReceipt['ci_carried'] {
const r = runScriptSync(path.join(path.dirname(Bun.fileURLToPath(import.meta.url)), 'gstack-evidence'), ['carried', '--base', base, '--json'], { encoding: 'utf8', timeout: 60_000 });
try { const list = JSON.parse(r.stdout); return Array.isArray(list) && list.length ? list : undefined; } catch { return undefined; }
}
@@ -235,7 +238,7 @@ function cmdWrite(rest: string[], json: boolean): number {
const excludeNow = review.exclude.state === 'active' ? review.exclude.record : null;
const excludeGiven = flag(rest, '--review-exclude') ?? 'none';
if ((excludeNow?.digest ?? 'none') !== excludeGiven) {
process.stderr.write(`gstack-ship-receipt write: field=review_exclude expected=${excludeNow?.digest ?? 'none'} recorded=${excludeGiven} source=${excludeNow?.source ?? `origin/${base} (no review_exclude_paths)`}: the review --review-exclude names read another selection than ${base}'s review_exclude_paths gives ${head.slice(0, 12)}; fix: rerun /ship's review on ${head.slice(0, 12)} (gstack-review-dispatch pass --base ${base}), run gstack-review-dispatch verify --ledger <ledger> and write with the --review-exclude <d> its REVIEW_EXCLUDE_RECORD: line prints (none when it prints no such line); nothing written (RECEIPT_INVALID)\n`);
process.stderr.write(`gstack-ship-receipt write: field=review_exclude expected=${excludeNow?.digest ?? 'none'} recorded=${excludeGiven} source=${excludeNow?.source ?? `origin/${base} (no review_exclude_paths)`}: the --review-exclude argument names another selection than ${base}'s review_exclude_paths gives ${head.slice(0, 12)} (write compares the argument, not the review ledger); fix: rerun /ship's review on ${head.slice(0, 12)} (gstack-review-dispatch pass --base ${base}), run gstack-review-dispatch verify --ledger <ledger> and write with the --review-exclude <d> its REVIEW_EXCLUDE_RECORD: line prints (none when it prints no such line); nothing written (RECEIPT_INVALID)\n`);
return EXIT.fail;
}
const product = reviewedProduct(repoRoot, base, head, pregateFile);
@@ -268,7 +271,7 @@ function cmdWrite(rest: string[], json: boolean): number {
version: flag(rest, '--version') ?? receiptVersion(repoRoot, head, base), base,
gated_tree: flag(rest, '--gated-tree') ?? tree?.gated_tree, predecessor: flag(rest, '--predecessor') ?? tree?.predecessor?.recorded_head ?? undefined,
tree: tree?.tree, gate_reuse: tree ? `${tree.gate_reuse} (${tree.gate_reuse_reason})` : undefined,
gate: jsonArg(flag(rest, '--gate')), ci: jsonArg(flag(rest, '--ci')), ci_carried: ciCarried(),
gate: jsonArg(flag(rest, '--gate')), ci: jsonArg(flag(rest, '--ci')), ci_carried: ciCarried(base),
spend_usd: flag(rest, '--spend-usd') !== undefined ? (Number.isFinite(Number(flag(rest, '--spend-usd'))) ? Number(flag(rest, '--spend-usd')) : flag(rest, '--spend-usd')) : undefined,
session_kind: sessionKind, artifacts_consumed: flag(rest, '--artifacts-consumed'), run: flag(rest, '--run'), thread: flag(rest, '--thread'),
policy: flag(rest, '--policy') ?? tree?.policy, queue_mode: flag(rest, '--queue-mode'), preregistration: flag(rest, '--preregistration'), history: flag(rest, '--history'),
@@ -472,15 +475,18 @@ function gitVersionBelow(repoRoot: string, minimum: string): string | null {
return major > minMajor || (major === minMajor && minor >= minMinor) ? null : m[0];
}
type PregateField = { pregate: string; requires_remote?: string[] };
type PregateField = { pregate: string; requires_remote?: string[]; flaky_exception?: ShipReceipt['flaky_exception'] };
/** A pregate result file → the receipt's pregate field, verified current for this checkout's tree (shared by write and carry). */
function pregateFieldFor(repoRoot: string, file: string, head: string): { ok: true; field: PregateField } | { ok: false; errors: Array<{ code: string; message: string }> } {
const read = readPregateFile(file);
if (!read.file) return { ok: false, errors: read.errors.map(e => ({ code: e.code, message: `${e.message} at ${e.path}` })) };
const v = verifyPregate(repoRoot, read.file);
if (!v.ok) return { ok: false, errors: [{ code: v.code!, message: v.message ?? 'pregate is not current' }] };
const foreign = exceptionConsumerProblem(read.file, head);
if (foreign) return { ok: false, errors: [{ code: 'PREGATE_REMOTE_UNCLEARED', message: foreign }] };
const remote = read.file.requires_remote ?? [];
return { ok: true, field: { pregate: `${read.file.verdict ?? 'pass'} @${read.file.tree.slice(0, 12)} (${read.file.checks.length} checks, current for ${head.slice(0, 12)})`, requires_remote: remote.length ? remote.map(r => `${r.lane}: ${r.receipt}`) : undefined } };
const exceptions = read.file.exceptions ?? [];
return { ok: true, field: { pregate: `${read.file.verdict ?? 'pass'} @${read.file.tree.slice(0, 12)} (${read.file.checks.length} checks, current for ${head.slice(0, 12)})`, requires_remote: remote.length ? remote.map(r => `${r.lane}: ${r.receipt}`) : undefined, flaky_exception: exceptions.length ? exceptions.map(e => ({ lane: e.lane, runs: e.runs, cases: e.cases, grant: e.grant, by: e.by })) : undefined } };
}
/**
@@ -501,13 +507,13 @@ function verifiedOutputs(repoRoot: string, base: string, pregateFile: string | u
return regen.proven.filter(p => !/[*?[\]\\]/.test(p) && matchesAny(owned, p));
}
interface ProductRules { extra: { metadata: string[]; outputs: readonly string[]; fragments?: string }; metadata: string[]; templates: readonly string[] }
interface ProductRules { extra: { metadata: string[]; outputs: readonly string[]; fragments?: string }; metadata: string[]; templates: readonly string[]; placeholders: readonly string[] }
function productRules(repoRoot: string, base: string, outputs: readonly string[]): ProductRules {
const load = loadPolicyFromBase(repoRoot, base);
const policy: ShipPolicy | null = isUnavailable(load) ? null : load.policy;
const fragments = policy?.changelog_fragments?.dir.replace(/^(\.\/)+/, '').replace(/\/+$/, '');
const extra = { metadata: [...(policy?.stamp_files ?? []), ...(policy?.stamp_paths ?? []), ...(policy?.release_outputs ?? []), ...(policy?.changelog ? [policy.changelog] : [])], outputs, fragments };
return { extra, metadata: [...CARRY_METADATA_GLOBS, ...extra.metadata, ...(fragments ? [`${fragments}/*.md`] : []), ...outputs], templates: policy?.stamp_patterns ?? [] };
return { extra, metadata: [...CARRY_METADATA_GLOBS, ...extra.metadata, ...(fragments ? [`${fragments}/*.md`] : []), ...outputs], templates: policy?.stamp_patterns ?? [], placeholders: policy?.stamp_placeholders ?? [] };
}
/**
@@ -534,7 +540,7 @@ function productSpec(repoRoot: string, rules: ProductRules, from: string, to: st
const versions = new Map<string, string | null>();
const version = (ref: string) => { if (!versions.has(ref)) versions.set(ref, versionAt(ref)); return versions.get(ref)!; };
const stamped = (p: string) => matchesAny(rules.metadata, p) && !matchesAny(rules.extra.outputs, p);
const classify = (p: string, a: string, b: string) => classifyCarryPath(p, rules.extra, stamped(p) ? { before: blobAt(a, p), after: blobAt(b, p), oldVersion: version(a), newVersion: version(b), templates: rules.templates, side: a === from && b === to ? branch?.own(p) : undefined } : undefined);
const classify = (p: string, a: string, b: string) => classifyCarryPath(p, rules.extra, stamped(p) ? { before: blobAt(a, p), after: blobAt(b, p), oldVersion: version(a), newVersion: version(b), templates: rules.templates, placeholders: rules.placeholders, side: a === from && b === to ? branch?.own(p) : undefined } : undefined);
const changed = changedRaw.split('\n').filter(Boolean).map(p => ({ path: p, class: classify(p, from, to) }));
const excludeGlobs = (globs: readonly string[]) => globs.map(g => `:(exclude,glob)${g}`);
const demoted = !branch ? changed.filter(c => c.class === 'product' && matchesAny(rules.metadata, c.path)).map(c => c.path)
@@ -550,7 +556,7 @@ function productSpec(repoRoot: string, rules: ProductRules, from: string, to: st
const was = before.split('\n');
const now = after.split('\n');
const lines = was.map((_, i) => i).filter(i => was[i] !== now[i]);
return was.length === now.length && lines.length > 0 && lines.every(i => side.at.has(i + 1) && stampOnlyLine(was[i]!, now[i]!, oldVersion, newVersion, rules.templates, side, i + 1));
return was.length === now.length && lines.length > 0 && lines.every(i => side.at.has(i + 1) && stampOnlyLine(was[i]!, now[i]!, oldVersion, newVersion, rules.templates, side, i + 1, rules.placeholders));
});
return { changed, product: ['.', ...excludeGlobs(CARRY_TEST_GLOBS), ...excludeGlobs(metadataGlobs)], tests: [...CARRY_TEST_GLOBS.map(g => `:(glob)${g}`), ...excludeGlobs(metadataGlobs)], demoted, ownRows };
}
@@ -656,18 +662,29 @@ function cmdCarry(rest: string[], json: boolean): number {
} else {
const fromBase = git(['merge-base', baseRef, from], repoRoot);
if (!fromBase) return refused('RECEIPT_CARRY_REFUSED', `cannot resolve the merge base of ${baseRef} (fetch it first)`);
const listed = [touchedPaths(repoRoot, fromBase, from), touchedPaths(repoRoot, headBase, head)];
if (!listed[0] || !listed[1]) return refused('RECEIPT_CARRY_REFUSED', `git could not list the paths the branch touched against ${base}; run /ship on ${head.slice(0, 12)}`);
const touched = new Set([...listed[0], ...listed[1]]);
const spec = productSpec(repoRoot, rules, from, head, { touched, sides: [[fromBase, from], [headBase, head]], own: branchSides(repoRoot, baseRef, from, '') });
if (!spec) return refused('RECEIPT_CARRY_REFUSED', `git diff ${from.slice(0, 12)} ${head.slice(0, 12)} failed`);
// The reviewed range runs from the fork point, or from a stack parent inside it that has since landed on the base: a
// receipt written against the base while stacked reviewed the parent's hunks too, and the squash moved them into the
// base (GST-1199). A parent counts only when merging it into the current base changes no product path.
const measure = (rangeBase: string) => {
const listed = [touchedPaths(repoRoot, rangeBase, from), touchedPaths(repoRoot, headBase, head)];
if (!listed[0] || !listed[1]) return `git could not list the paths the branch touched against ${base}; run /ship on ${head.slice(0, 12)}`;
const touched = new Set([...listed[0], ...listed[1]]);
const spec = productSpec(repoRoot, rules, from, head, { touched, sides: [[rangeBase, from], [headBase, head]], own: branchSides(repoRoot, baseRef, from, '') });
if (!spec) return `git diff ${from.slice(0, 12)} ${head.slice(0, 12)} failed`;
const ids = [patchId(repoRoot, rangeBase, from, spec.product), patchId(repoRoot, headBase, head, spec.product)];
for (const r of ids) if (typeof r !== 'string') return `git could not compute the product patch-id at one of the heads: ${r.error}`;
return { rangeBase, touched, spec, productFrom: ids[0] as string, productHead: ids[1] as string };
};
let measured = measure(fromBase);
if (typeof measured === 'string') return refused('RECEIPT_CARRY_REFUSED', measured);
const stackParent = !retarget && measured.productFrom !== measured.productHead ? landedStackParent(repoRoot, { rangeBase: fromBase, reviewed: from, currentBase: headBase }, measured.spec.product) : null;
if (stackParent) measured = measure(stackParent);
if (typeof measured === 'string') return refused('RECEIPT_CARRY_REFUSED', measured);
const { rangeBase, touched, spec, productFrom, productHead } = measured;
const { changed } = spec;
const productFrom = patchId(repoRoot, fromBase, from, spec.product);
const productHead = patchId(repoRoot, headBase, head, spec.product);
const asId = (r: string | { error: string }) => (typeof r === 'string' ? r : '');
const testFrom = asId(patchId(repoRoot, fromBase, from, spec.tests));
const testFrom = asId(patchId(repoRoot, rangeBase, from, spec.tests));
const testHead = asId(patchId(repoRoot, headBase, head, spec.tests));
for (const r of [productFrom, productHead]) if (typeof r !== 'string') return refused('RECEIPT_CARRY_REFUSED', `git could not compute the product patch-id at one of the heads: ${r.error}`);
// A stacked child whose parent landed on the base (#3570): the receipt reviewed the child's own range against the
// parent branch, and the PR now targets the base. That reviewed range (the recorded product patch-id, else the one
// against origin/<receipt base> when it still resolves) must be HEAD's whole product diff against the base, computed
@@ -691,8 +708,8 @@ function cmdCarry(rest: string[], json: boolean): number {
// which can differ between the two base diffs. A touched path with the same blob at both heads and at both bases has
// the same patch, so only paths that moved on one side are compared (GST-448: 6000 touched paths). A path whose only difference is base context next to unchanged hunks
// (lib/ship-patch.ts) needs a scoped review instead of the loop.
const sides = { reviewedBase: fromBase, reviewed: from, currentBase: headBase, current: head };
const moved = [touchedPaths(repoRoot, from, head), touchedPaths(repoRoot, fromBase, headBase)];
const sides = { reviewedBase: rangeBase, reviewed: from, currentBase: headBase, current: head };
const moved = [touchedPaths(repoRoot, from, head), touchedPaths(repoRoot, rangeBase, headBase)];
if (moved.some(l => !l)) return refused('RECEIPT_CARRY_REFUSED', `git could not list the paths the branch touched against ${base}; run /ship on ${head.slice(0, 12)}`);
const [movedHead, movedBase] = moved as Set<string>[];
// A stamp file the branch changed as product (its own row) whose reviewed-to-current change is a stamp on its own lines
@@ -701,6 +718,16 @@ function cmdCarry(rest: string[], json: boolean): number {
if (changedProduct.length >= CARRY_NAMED_PATHS) break;
const cls = changed.find(c => c.path === p)?.class ?? classifyCarryPath(p, rules.extra);
if (cls === 'metadata' && spec.ownRows.includes(p) && !movedBase.has(p)) stampOnly.push(p);
if (cls === 'metadata' && spec.demoted.includes(p) && movedBase.has(p) && /(^|\/)package\.json$/.test(p)) {
const masked = Object.values(sides).map(ref => {
const r = spawnSync('git', ['show', `${ref}:${p}`], { cwd: repoRoot, encoding: 'utf8', timeout: 30_000, maxBuffer: 16 * 1024 * 1024 });
try { const { version: _, ...rest } = JSON.parse(r.status === 0 ? r.stdout : 'null') ?? {}; return JSON.stringify(rest); } catch { return null; }
});
const [rb, rv, cb, cv] = masked;
if (masked.some(m => m === null) || rv !== cv || rb !== cb) changedProduct.push(p);
else stampOnly.push(p);
continue;
}
if (cls !== 'product') continue;
const verdict = comparePath(repoRoot, sides, p);
if (verdict === 'changed') changedProduct.push(p);
@@ -710,14 +737,14 @@ function cmdCarry(rest: string[], json: boolean): number {
if (!changedProduct.length && !left.length) changedProduct.push('(a product path outside the branch\'s touched files; compare the two base diffs)');
else if (!changedProduct.length) {
const rest = [...spec.product, ...left.map(p => `:(exclude,literal)${p}`)];
const restFrom = patchId(repoRoot, fromBase, from, rest);
const restFrom = patchId(repoRoot, rangeBase, from, rest);
const restHead = patchId(repoRoot, headBase, head, rest);
if (typeof restFrom !== 'string' || typeof restHead !== 'string' || restFrom !== restHead) changedProduct.push(`(a product path outside ${left.join(', ')}; compare the two base diffs without them)`);
}
}
const stampedRows = changedProduct.length ? [] : stampOnly;
let scopedReview: string | undefined;
const packetBody = !changedProduct.length && contextPaths.length ? contextPacket(repoRoot, { reviewedBase: fromBase, reviewed: from, currentBase: headBase, current: head }, contextPaths) : null;
const packetBody = !changedProduct.length && contextPaths.length ? contextPacket(repoRoot, { reviewedBase: rangeBase, reviewed: from, currentBase: headBase, current: head }, contextPaths) : null;
if (packetBody?.over.length) changedProduct.push(...packetBody.over.map(p => `${p} (too large for the scoped review: over ${PACKET_SECTION_CAP} lines in a section)`));
if (packetBody && !changedProduct.length) {
const record = flag(rest, '--scoped-review');
@@ -743,9 +770,9 @@ function cmdCarry(rest: string[], json: boolean): number {
}
const mergeCommits = (git(['rev-list', '--merges', '--count', `${from}..${head}`], repoRoot) ?? '0').trim();
// Evidence for the record, never the decision: a slow range-diff is reported unavailable rather than waited for.
const rangeRaw = spawnSync('git', ['range-diff', '--no-color', `${fromBase}..${from}`, `${headBase}..${head}`], { cwd: repoRoot, encoding: 'utf8', timeout: 15_000, maxBuffer: 64 * 1024 * 1024 });
const rangeRaw = spawnSync('git', ['range-diff', '--no-color', `${rangeBase}..${from}`, `${headBase}..${head}`], { cwd: repoRoot, encoding: 'utf8', timeout: 15_000, maxBuffer: 64 * 1024 * 1024 });
const rangeDiff = rangeRaw.status === 0 ? (rangeRaw.stdout.trim().split('\n').slice(0, 20).join('\n') || '(empty: identical ranges)') : `unavailable: ${(rangeRaw.stderr ?? '').trim().split('\n')[0] || (rangeRaw.signal ? `range-diff killed by ${rangeRaw.signal} after 15 s` : 'range-diff failed')}`;
plan = planCarry({ from, head, changed, productPatchId: { from: reviewed as string, head: current as string }, testPatchId: { from: testFrom, head: testHead }, changedProduct, mergeCommits: Number(mergeCommits) || 0, rangeDiff, ...(retarget ? { retargetedFrom: reviewedBase } : {}), ...(scopedReview ? { scopedReview } : {}), ...(stampedRows.length ? { stampedRows } : {}) });
plan = planCarry({ from, head, changed, productPatchId: { from: reviewed as string, head: current as string }, testPatchId: { from: testFrom, head: testHead }, changedProduct, mergeCommits: Number(mergeCommits) || 0, rangeDiff, ...(retarget ? { retargetedFrom: reviewedBase } : {}), ...(stackParent ? { stackParent } : {}), ...(scopedReview ? { scopedReview } : {}), ...(stampedRows.length ? { stampedRows } : {}) });
}
if (!plan.ok) return refused(plan.code, plan.message);
// Eligibility is not publication: a receipt that recorded a pregate is rebound only with a pregate current for HEAD,
@@ -773,12 +800,15 @@ function cmdCarry(rest: string[], json: boolean): number {
const now = review.depth;
if (review.exclude.state === 'refused') return refused('RECEIPT_CARRY_REFUSED', `${review.exclude.lines.join('; ')}; run /ship on ${head.slice(0, 12)} once the base policy reads`);
const digest = review.exclude.state === 'active' ? review.exclude.record.digest : 'none';
if (digest !== (receipt.review_exclude?.digest ?? 'none')) return refused('RECEIPT_CARRY_REFUSED', `review_exclude changed: recorded=${receipt.review_exclude?.digest ?? 'none'} now=${digest} source=${review.exclude.state === 'active' ? review.exclude.record.source : `${base} (no review_exclude_paths)`}; the reviewers read another selection than HEAD ${head.slice(0, 12)} has under the base policy, so carry does not apply; run /ship on ${head.slice(0, 12)}`);
if (digest !== (receipt.review_exclude?.digest ?? 'none')) return refused('RECEIPT_CARRY_REFUSED', `review_exclude changed: recorded=${receipt.review_exclude?.digest ?? 'none'} now=${digest} source=${review.exclude.state === 'active' ? review.exclude.record.source : `${base} (no review_exclude_paths)`}; the receipt's digest is the --review-exclude argument write was given, and it names another selection than HEAD ${head.slice(0, 12)} has under the base policy (carry compares that digest, not the review ledger), so carry does not apply; run /ship on ${head.slice(0, 12)}`);
if (depthBelow(receipt.review_depth.depth, now.depth)) return refused('RECEIPT_CARRY_REFUSED', `HEAD ${head.slice(0, 12)} needs ${now.depth} review depth (${now.reasons.join(', ')}), above the receipt's ${receipt.review_depth.depth}; run /ship on ${head.slice(0, 12)}`);
depthField = { review_depth: { ...now, depth: deeper(receipt.review_depth.depth, now.depth) }, review_exclude: receiptExclude(review.exclude) };
}
// The rebound product_patch_id is HEAD's diff against the carry's base, so a retargeted receipt names that base from now on.
const rebound = { ...receipt, ...(retarget ? { base: base.replace(/^origin\//, '') } : {}), ...(pregateField ?? {}), ...depthField };
// A receipt that records a version names HEAD's, read as write reads it: a restamp after a base merge moves it (GST-1030).
// A head with no version source keeps the recorded one (write --version named it).
const version = receipt.version === undefined ? undefined : (receiptVersion(repoRoot, head, base) ?? receipt.version);
const rebound = { ...receipt, ...(version !== undefined ? { version } : {}), ...(retarget ? { base: base.replace(/^origin\//, '') } : {}), ...(pregateField ?? {}), ...depthField };
const carried = carryReceipt(rebound, head, plan.carry, { tree: headTree, fromTree, productPatchId: headPatch });
const errors = validateReceipt(carried);
if (errors.length) return refused('RECEIPT_CARRY_REFUSED', `the carried receipt does not validate: ${errors[0]!.message} at ${errors[0]!.path}`);