diff --git a/packages/extension/scripts/build_app_bundle.mjs b/packages/extension/scripts/build_app_bundle.mjs
index f631b097..53fc9d84 100644
--- a/packages/extension/scripts/build_app_bundle.mjs
+++ b/packages/extension/scripts/build_app_bundle.mjs
@@ -11,6 +11,19 @@
// node scripts/build_app_bundle.mjs --dist
# stage an already-built dist
// node scripts/build_app_bundle.mjs --work # materialize/reuse this tree
// AMICODE_APP_BUNDLE_WORK= # --work via env
+// AMICODE_DEPLOY_OVERRIDE= # #992: proceed despite a
+// stale/dirty pre-flight — the reason MUST be non-empty and is
+// stamped into dist-app/deploy.json (honest hotfixes, never
+// silent ones)
+//
+// #992 DEPLOY GUARD: before any build/stage, fetch origin and refuse (exit 1,
+// named reason + remedy) when HEAD ≠ origin/main or the tree is dirty, and
+// when the tree is missing a #964 known-fixed hunk. Every deploy stamps
+// dist-app/deploy.json {commit, branch, dirty, override_reason, built_at,
+// deployed_by} so the served dist always traces to a recorded commit.
+// EXCEPTION: under CI (the vsix-gate's packaging lane, a merge-ref build that
+// is not a deploy) the stale/dirty checks are advisory and recorded in the
+// manifest; the known-fixes check stays enforcing everywhere.
//
// FAILS LOUDLY, never a silent skip: a packaging step that no-ops is the
// "silently no-op'd fetch" trap the vsix-gate exists to catch. The RUNTIME half
@@ -23,9 +36,16 @@
// app-shelf-boot-proof CI lane runs the env-gated probe, which skips with the
// reason printed until a dist is built there.
import { spawnSync } from "node:child_process";
-import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
+import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
+import { hostname, userInfo } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
+import {
+ buildDeployManifest,
+ checkKnownFixes,
+ evaluatePreflight,
+ headRelation,
+} from "./deploy_guard.mjs";
const EXT_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const REPO_ROOT = join(EXT_ROOT, "..", "..");
@@ -48,7 +68,93 @@ const run = (cmd, cmdArgs, cwd, note) => {
if (r.status !== 0) fail(`${note} failed (exit ${r.status})`, 2);
};
-const stageDist = (distDir) => {
+const gitOut = (cmdArgs, note) => {
+ const r = spawnSync("git", cmdArgs, { cwd: REPO_ROOT, encoding: "utf8" });
+ if (r.status !== 0) fail(`${note} failed (git exit ${r.status}): ${r.stderr?.trim()}`, 2);
+ return r.stdout.trim();
+};
+
+// ── #992 pre-flight: recorded main is canonical for the served dist ─────────
+// Fetch origin; refuse when HEAD ≠ origin/main or the tree is dirty. An
+// override (AMICODE_DEPLOY_OVERRIDE) MUST carry a non-empty reason and is
+// recorded in the deploy manifest. Also the #964 known-fixes check at deploy
+// time: a tree missing a recorded fix refuses with the named-remedy shape.
+//
+// CI EXCEPTION (honest, never silent): the vsix-gate's packaging lane runs
+// build:app from the PR MERGE REF — legitimately ahead of origin/main and
+// shallow-cloned (ancestry probes can't decide). That is a packaging build,
+// not a deploy: under CI the stale/dirty checks run ADVISORY (printed, and
+// recorded as the manifest's override_reason) while the #964 known-fixes
+// check stays ENFORCING — a merge ref containing main carries main's fixes,
+// so a regression there still refuses. Locally, CI is not set: the guard is
+// always a hard refusal, overridable only via a recorded AMICODE_DEPLOY_OVERRIDE.
+const preflight = () => {
+ const ciPackaging = process.env.CI === "true" || process.env.CI === "1";
+ console.log("[build:app] pre-flight (#992): fetch origin, compare HEAD to origin/main, check the tree");
+ if (gitOut(["rev-parse", "--is-shallow-repository"], "shallow check") === "true") {
+ console.log("[build:app] shallow clone — unshallowing so the origin/main ancestry probes are honest");
+ run("git", ["fetch", "--unshallow", "origin"], REPO_ROOT, "git fetch --unshallow origin");
+ } else {
+ run("git", ["fetch", "origin"], REPO_ROOT, "git fetch origin");
+ }
+ const headSha = gitOut(["rev-parse", "HEAD"], "rev-parse HEAD");
+ const originSha = gitOut(["rev-parse", "origin/main"], "rev-parse origin/main");
+ const headIsAncestor =
+ spawnSync("git", ["merge-base", "--is-ancestor", "HEAD", "origin/main"], { cwd: REPO_ROOT }).status === 0;
+ const originIsAncestor =
+ spawnSync("git", ["merge-base", "--is-ancestor", "origin/main", "HEAD"], { cwd: REPO_ROOT }).status === 0;
+ const dirtyEntries = gitOut(["status", "--porcelain"], "git status").split("\n").filter(Boolean);
+ const decision = evaluatePreflight({
+ headSha,
+ originMainSha: originSha,
+ relation: headRelation(headSha, originSha, headIsAncestor, originIsAncestor),
+ dirtyEntries,
+ overrideReason: process.env.AMICODE_DEPLOY_OVERRIDE,
+ });
+ for (const line of decision.recorded ?? []) console.log(`[build:app] ${line}`);
+ if (!decision.ok) {
+ if (ciPackaging) {
+ const advisoryReason = `ci-packaging (merge ref ${headSha.slice(0, 12)}, branch ${gitOut(["rev-parse", "--abbrev-ref", "HEAD"], "branch name")}): ${decision.reasons.join(" | ")}`;
+ console.log("[build:app] CI packaging build — stale/dirty guard is ADVISORY here (not a deploy); recorded in the manifest:");
+ for (const r of decision.reasons) console.log(`[build:app] ADVISORY: ${r}`);
+ return { headSha, dirty: dirtyEntries.length > 0, overrideReason: advisoryReason };
+ }
+ console.error("[build:app] pre-flight FAILED — refusing to build/stage a deploy from this tree:");
+ for (const r of decision.reasons) console.error(`[build:app] ${r}`);
+ process.exit(1);
+ }
+ // The #964 known-fixes check at deploy time (the guard test
+ // packages/extension/test/overlay_known_fixes_964.test.ts is the source of
+ // record; deploy_guard.mjs mirrors its fixture list).
+ const overlayApp = join(REPO_ROOT, "packages", "app-bundle", "overlay", "packages", "app", "src");
+ if (existsSync(overlayApp)) {
+ const regressed = checkKnownFixes(overlayApp);
+ if (regressed.length > 0) {
+ console.error("[build:app] pre-flight FAILED — the tree is missing known-fixed hunks (#964):");
+ for (const r of regressed) console.error(`[build:app] ${r}`);
+ process.exit(1);
+ }
+ console.log("[build:app] known-fixes check (#964 hunks): all present in the overlay");
+ } else {
+ console.log("[build:app] known-fixes check skipped: no overlay tree at packages/app-bundle/overlay (non-app-bundle build context)");
+ }
+ return { headSha, dirty: dirtyEntries.length > 0, overrideReason: decision.overrideReason };
+};
+
+const stampDeployManifest = (target, { headSha, dirty, overrideReason }) => {
+ const manifest = buildDeployManifest({
+ commit: headSha,
+ branch: gitOut(["rev-parse", "--abbrev-ref", "HEAD"], "branch name"),
+ dirty,
+ overrideReason,
+ builtAt: new Date().toISOString(),
+ deployedBy: `${userInfo().username}@${hostname()}`,
+ });
+ writeFileSync(join(target, "deploy.json"), JSON.stringify(manifest, null, 2) + "\n");
+ console.log(`[build:app] stamped deploy.json → commit ${manifest.commit.slice(0, 12)}${manifest.override_reason ? ` (override: ${manifest.override_reason})` : ""}`);
+};
+
+const stageDist = (distDir, manifestInputs) => {
if (!existsSync(join(distDir, "index.html")))
fail(`no dist to stage: ${distDir} has no index.html`);
const target = join(EXT_ROOT, "dist", "app");
@@ -56,6 +162,7 @@ const stageDist = (distDir) => {
mkdirSync(join(target, ".."), { recursive: true });
cpSync(distDir, target, { recursive: true });
if (!existsSync(join(target, "index.html"))) fail(`staging ${distDir} → ${target} lost the index document`);
+ stampDeployManifest(target, manifestInputs);
const files = readdirSync(target);
console.log(`[build:app] staged ${files.length} top-level entries → packages/extension/dist/app`);
console.log("[build:app] DONE — the amicode service's shelf serves this at its origin");
@@ -64,11 +171,13 @@ const stageDist = (distDir) => {
// ── stage-only mode: an already-built dist (the telaio probe's recipe) ───────
const prebuilt = flag("dist");
if (prebuilt) {
- stageDist(prebuilt);
+ const manifestInputs = preflight();
+ stageDist(prebuilt, manifestInputs);
process.exit(0);
}
// ── the full recipe ──────────────────────────────────────────────────────────
+const manifestInputs = preflight();
const work = flag("work") ?? process.env.AMICODE_APP_BUNDLE_WORK ?? join(BUNDLE_PKG, ".materialized");
if (!existsSync(join(work, "package.json"))) {
@@ -97,4 +206,4 @@ if (!existsSync(join(built, "index.html"))) {
(candidates.length > 0 ? `Found index.html in: ${candidates.join(", ")}` : "No index.html anywhere under packages/app — the build did not emit the app document."),
);
}
-stageDist(built);
+stageDist(built, manifestInputs);
diff --git a/packages/extension/scripts/deploy_guard.mjs b/packages/extension/scripts/deploy_guard.mjs
new file mode 100644
index 00000000..88eedee8
--- /dev/null
+++ b/packages/extension/scripts/deploy_guard.mjs
@@ -0,0 +1,171 @@
+// deploy_guard.mjs — amicode#992: the deploy pre-flight + the deploy manifest,
+// as pure functions so both build_app_bundle.mjs and the vitest suite
+// (test/deploy_guard.test.ts) drive the SAME logic. The doctrine: recorded
+// main is canonical; anything deploying stale or unrecorded state must fail
+// loudly (#992, same class as #964 — recorded state is canonical,
+// reference-20260907-021500-agentic-substrate-doctrine).
+import { readFileSync, readdirSync } from "node:fs";
+import { join } from "node:path";
+
+// ── Pre-flight evaluation (pure) ────────────────────────────────────────────
+// Inputs are gathered by the caller (git, fs); the decision lives here so the
+// tests can drive every branch without shelling out.
+//
+// headSha — the local HEAD's full sha
+// originMainSha — origin/main's full sha (after a fetch)
+// relation — "at" | "BEHIND" | "DIVERGED" (caller computes via
+// `git merge-base --is-ancestor`; headRelation below)
+// dirtyEntries — `git status --porcelain` lines ([] when clean)
+// overrideReason — $AMICODE_DEPLOY_OVERRIDE (undefined when unset)
+//
+// Returns { ok: true, overrideReason: string|null, recorded: string[] } on
+// proceed, or { ok: false, reasons: string[] } with one NAMED reason + remedy
+// per failure. An override MUST carry a non-empty reason: an empty or missing
+// reason with the flag set refuses (no silent overrides) — and a VALID
+// override waives the stale/dirty refusal (an honest hotfix) but is RECORDED
+// in the returned decision so the manifest stamps it. The #964 known-fixes
+// refusal is NEVER overridable — a tree missing a recorded fix does not build
+// a deploy, hotfix or not.
+export function evaluatePreflight({ headSha, originMainSha, relation, dirtyEntries, overrideReason }) {
+ const reasons = [];
+ const hasOverrideFlag = overrideReason !== undefined;
+ const validOverride = hasOverrideFlag && String(overrideReason).trim() !== "";
+
+ if (!validOverride && headSha !== originMainSha) {
+ const rel = relation && relation !== "at" ? relation : "≠";
+ reasons.push(
+ `REFUSED: HEAD ${headSha.slice(0, 12)} is ${rel} origin/main (${originMainSha.slice(0, 12)}) — ` +
+ `a deploy from a non-recorded-main tree can serve unrecorded state over a recorded fix (the 2026-09-10 deploy race, #992). ` +
+ `REMEDY: pull/rebase onto origin/main and land any local work via a PR, then redeploy ` +
+ `(or set AMICODE_DEPLOY_OVERRIDE= to proceed with the hotfix recorded).`,
+ );
+ }
+ if (!validOverride && dirtyEntries.length > 0) {
+ reasons.push(
+ `REFUSED: the working tree is dirty (${dirtyEntries.length} entr${dirtyEntries.length === 1 ? "y" : "ies"}: ` +
+ `${dirtyEntries.slice(0, 5).map((e) => e.trim()).join("; ")}${dirtyEntries.length > 5 ? "; …" : ""}) — ` +
+ `a dirty tree's build output does not trace to any recorded commit. ` +
+ `REMEDY: commit via a PR, or stash before deploying ` +
+ `(or set AMICODE_DEPLOY_OVERRIDE= to proceed with the hotfix recorded).`,
+ );
+ }
+ if (hasOverrideFlag && !validOverride) {
+ reasons.push(
+ `REFUSED: AMICODE_DEPLOY_OVERRIDE is set but its reason is empty — overrides without a stated reason ` +
+ `are silent hotfixes, and silent hotfixes are exactly what #992 exists to prevent. ` +
+ `REMEDY: set AMICODE_DEPLOY_OVERRIDE to a non-empty reason (it will be recorded in deploy.json).`,
+ );
+ }
+ if (reasons.length > 0) return { ok: false, reasons };
+
+ const reason = validOverride ? String(overrideReason).trim() : null;
+ const recorded = [];
+ if (validOverride) {
+ if (headSha !== originMainSha)
+ recorded.push(
+ `OVERRIDE (stale tree: HEAD ${headSha.slice(0, 12)} ${relation ?? "≠"} origin/main): ${reason}`,
+ );
+ if (dirtyEntries.length > 0)
+ recorded.push(`OVERRIDE (dirty tree: ${dirtyEntries.length} entries): ${reason}`);
+ recorded.push(
+ `OVERRIDE RECORDED: AMICODE_DEPLOY_OVERRIDE="${reason}" — stamped into dist-app/deploy.json`,
+ );
+ }
+ return { ok: true, overrideReason: reason, recorded };
+}
+
+// A thin helper the script uses after `git merge-base --is-ancestor` probes —
+// pure: it just picks the relation label.
+// headIsAncestor — merge-base --is-ancestor HEAD origin/main
+// originIsAncestor — merge-base --is-ancestor origin/main HEAD
+export function headRelation(headSha, originMainSha, headIsAncestor, originIsAncestor) {
+ if (headSha === originMainSha) return "at";
+ if (headIsAncestor) return "BEHIND";
+ if (originIsAncestor) return "AHEAD of";
+ return "DIVERGED from";
+}
+
+// ── The known-fixes check (#964's hunks, checked at deploy time) ────────────
+// SOURCE OF RECORD for these fixtures is packages/extension/test/
+// overlay_known_fixes_964.test.ts — the deploy-side copy below is mirrored
+// from it; when the guard's list grows, mirror it here (and the mirrored
+// fork branch local/amicode carries the same hunks).
+const KNOWN_FIXED_HUNKS = [
+ {
+ fix: "#929 (59b447e7) — the v2 composer's design placeholder takes the translate callback",
+ file: "components/prompt-input-v2.tsx",
+ signature: /promptDesignPlaceholder\(\s*mode\(\),\s*placeholder\(\),/,
+ },
+ {
+ fix: "#832 (faac5bdf) — the session-cache diff_version reconciliation shape",
+ file: "context/global-sync/session-cache.ts",
+ signature: /diff_version: Record/,
+ },
+ {
+ fix: "#832 (faac5bdf) — the session-cache diff_version guarded delete",
+ file: "context/global-sync/session-cache.ts",
+ signature: /delete store\.diff_version\[sessionID\]/,
+ },
+ {
+ fix: "#832 (872a5218) — session.exportTrace restored in the non-English app locales",
+ file: "i18n/",
+ signature: /"session\.exportTrace"/,
+ },
+];
+
+const LOCALE_SKIP = new Set(["desktop-native.ts", "parity.test.ts"]);
+
+// overlayAppDir — packages/app-bundle/overlay/packages/app/src (what the
+// materialize step folds INTO the built tree, so a missing fix here means a
+// missing fix in the dist). Returns regressions as named-remedy strings.
+export function checkKnownFixes(overlayAppDir) {
+ const regressed = [];
+ for (const kf of KNOWN_FIXED_HUNKS) {
+ if (kf.file === "i18n/") {
+ const locales = readdirSync(join(overlayAppDir, "i18n")).filter(
+ (f) => f.endsWith(".ts") && !LOCALE_SKIP.has(f),
+ );
+ if (locales.length === 0) {
+ regressed.push(
+ `KNOWN-FIX UNCHECKABLE in i18n/: no locale dicts found under ${overlayAppDir}/i18n — ` +
+ `the overlay tree is not a materializable app source. REMEDY: pass the real overlay root ` +
+ `(packages/app-bundle/overlay/packages/app/src). See harmoniqs/amicode#964.`,
+ );
+ continue;
+ }
+ for (const f of locales) {
+ const source = readFileSync(join(overlayAppDir, "i18n", f), "utf8");
+ if (!kf.signature.test(source))
+ regressed.push(knownFixRefusal(`i18n/${f}`, kf.fix));
+ }
+ continue;
+ }
+ const source = readFileSync(join(overlayAppDir, kf.file), "utf8");
+ if (!kf.signature.test(source)) regressed.push(knownFixRefusal(kf.file, kf.fix));
+ }
+ return regressed;
+}
+
+function knownFixRefusal(path, fix) {
+ return (
+ `REFUSED: KNOWN-FIX MISSING in ${path}: ${fix}. A build from this tree would serve the dist WITHOUT a ` +
+ `recorded fix (the #964 class). REMEDY: restore the fix in the overlay ` +
+ `(packages/app-bundle/overlay/packages/app/src — the guard test ` +
+ `packages/extension/test/overlay_known_fixes_964.test.ts is the source of record for the hunk list; ` +
+ `the fork branch local/amicode carries the same hunks so the next sync brings the fix, not the regression), ` +
+ `land it via PR, then redeploy. See harmoniqs/amicode#964 and #992.`
+ );
+}
+
+// ── The deploy manifest (#992 slice 2) ──────────────────────────────────────
+// Pure builder: the script supplies observed values; the shape is contract.
+export function buildDeployManifest({ commit, branch, dirty, overrideReason, builtAt, deployedBy }) {
+ return {
+ commit, // full sha of the tree the dist was built from
+ branch,
+ dirty, // whether the tree had uncommitted changes at build time
+ override_reason: overrideReason ?? null,
+ built_at: builtAt, // ISO timestamp
+ deployed_by: deployedBy, // user@host
+ };
+}
diff --git a/packages/extension/test/deploy_guard.test.ts b/packages/extension/test/deploy_guard.test.ts
new file mode 100644
index 00000000..e4365bbf
--- /dev/null
+++ b/packages/extension/test/deploy_guard.test.ts
@@ -0,0 +1,277 @@
+import { describe, expect, test } from "vitest"
+import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
+import { tmpdir } from "node:os"
+import { join } from "node:path"
+import {
+ buildDeployManifest,
+ checkKnownFixes,
+ evaluatePreflight,
+ headRelation,
+} from "../scripts/deploy_guard.mjs"
+
+// amicode#992 — the deploy guard.
+//
+// The 2026-09-10 deploy race: a local-tree deploy (build_app_bundle.mjs from a
+// tree predating #988) overwrote the recorded-main dist-app swap and the
+// tool-count-label crash returned — an unrecorded local build served over a
+// recorded fix. The guard's contract, tested here as pure functions (the
+// script gathers git's observations; the DECISION lives in deploy_guard.mjs):
+//
+// 1. refuse when HEAD ≠ origin/main (behind or diverged) — named reason + remedy
+// 2. refuse when the tree is dirty — named reason + remedy
+// 3. refuse an override with an empty/missing reason (no silent overrides)
+// 4. a VALID override proceeds but is RECORDED (stamped into deploy.json)
+// 5. a clean, at-origin tree proceeds with no override
+// 6. the manifest shape: {commit, branch, dirty, override_reason, built_at, deployed_by}
+// 7. the #964 known-fixes check: missing hunk → named-remedy refusal; full
+// overlay → clean
+
+const HEAD = "aaaabbbbccccddddeeeeffff0000111122223333"
+const ORIGIN = "9999888877776666555544443333222211110000"
+
+const proceed = (d: ReturnType) => {
+ expect(d.ok).toBe(true)
+ return d as { ok: true; overrideReason: string | null; recorded: string[] }
+}
+
+describe("#992 pre-flight: HEAD vs origin/main", () => {
+ test("refuses a tree BEHIND origin/main with a named reason and remedy", () => {
+ const d = evaluatePreflight({
+ headSha: HEAD,
+ originMainSha: ORIGIN,
+ relation: "BEHIND",
+ dirtyEntries: [],
+ overrideReason: undefined,
+ })
+ expect(d.ok).toBe(false)
+ if (d.ok) return
+ expect(d.reasons[0]).toContain("BEHIND")
+ expect(d.reasons[0]).toContain("pull/rebase")
+ })
+
+ test("refuses a DIVERGED tree with a named reason and remedy", () => {
+ const d = evaluatePreflight({
+ headSha: HEAD,
+ originMainSha: ORIGIN,
+ relation: "DIVERGED from",
+ dirtyEntries: [],
+ overrideReason: undefined,
+ })
+ expect(d.ok).toBe(false)
+ if (d.ok) return
+ expect(d.reasons[0]).toContain("DIVERGED")
+ expect(d.reasons[0]).toContain("pull/rebase")
+ })
+
+ test("proceeds when HEAD is at origin/main and the tree is clean", () => {
+ const d = proceed(
+ evaluatePreflight({
+ headSha: HEAD,
+ originMainSha: HEAD,
+ relation: "at",
+ dirtyEntries: [],
+ overrideReason: undefined,
+ }),
+ )
+ expect(d.overrideReason).toBeNull()
+ expect(d.recorded).toEqual([])
+ })
+})
+
+describe("#992 pre-flight: dirty tree", () => {
+ test("refuses a dirty tree with a named reason and remedy", () => {
+ const d = evaluatePreflight({
+ headSha: HEAD,
+ originMainSha: HEAD,
+ relation: "at",
+ dirtyEntries: [" M packages/app/src/foo.ts", "?? scratch.md"],
+ overrideReason: undefined,
+ })
+ expect(d.ok).toBe(false)
+ if (d.ok) return
+ expect(d.reasons[0]).toContain("dirty")
+ expect(d.reasons[0]).toContain("stash")
+ expect(d.reasons[0]).toContain("commit via a PR")
+ expect(d.reasons[0]).toContain("foo.ts")
+ })
+})
+
+describe("#992 pre-flight: the override gate", () => {
+ test("refuses an override whose reason is EMPTY (flag set, no silent overrides)", () => {
+ const d = evaluatePreflight({
+ headSha: HEAD,
+ originMainSha: ORIGIN,
+ relation: "BEHIND",
+ dirtyEntries: [],
+ overrideReason: "",
+ })
+ expect(d.ok).toBe(false)
+ if (d.ok) return
+ expect(d.reasons.some((r) => r.includes("AMICODE_DEPLOY_OVERRIDE") && r.includes("empty"))).toBe(true)
+ })
+
+ test("refuses an override whose reason is WHITESPACE only", () => {
+ const d = evaluatePreflight({
+ headSha: HEAD,
+ originMainSha: ORIGIN,
+ relation: "BEHIND",
+ dirtyEntries: [],
+ overrideReason: " ",
+ })
+ expect(d.ok).toBe(false)
+ })
+
+ test("a valid override PROCEEDS but is recorded", () => {
+ const d = proceed(
+ evaluatePreflight({
+ headSha: HEAD,
+ originMainSha: ORIGIN,
+ relation: "BEHIND",
+ dirtyEntries: [],
+ overrideReason: "hotfix: rollback dist to last known good while #1000 is debugged",
+ }),
+ )
+ expect(d.overrideReason).toBe("hotfix: rollback dist to last known good while #1000 is debugged")
+ expect(d.recorded.at(-1)).toContain("OVERRIDE RECORDED")
+ expect(d.recorded.at(-1)).toContain("deploy.json")
+ expect(d.recorded[0]).toContain("stale tree")
+ })
+
+ test("a valid override on a clean at-origin tree also proceeds, recorded", () => {
+ const d = proceed(
+ evaluatePreflight({
+ headSha: HEAD,
+ originMainSha: HEAD,
+ relation: "at",
+ dirtyEntries: [],
+ overrideReason: "not needed but stated",
+ }),
+ )
+ expect(d.overrideReason).toBe("not needed but stated")
+ })
+})
+
+describe("#992 the deploy manifest", () => {
+ test("stamps the full contract shape", () => {
+ const m = buildDeployManifest({
+ commit: HEAD,
+ branch: "main",
+ dirty: false,
+ overrideReason: null,
+ builtAt: "2026-09-10T15:06:00.000Z",
+ deployedBy: "aaron@erlich",
+ })
+ expect(m).toEqual({
+ commit: HEAD,
+ branch: "main",
+ dirty: false,
+ override_reason: null,
+ built_at: "2026-09-10T15:06:00.000Z",
+ deployed_by: "aaron@erlich",
+ })
+ })
+
+ test("an undefined override_reason serializes as null, not undefined", () => {
+ const m = buildDeployManifest({
+ commit: HEAD,
+ branch: "main",
+ dirty: true,
+ overrideReason: undefined,
+ builtAt: "2026-09-10T15:06:00.000Z",
+ deployedBy: "aaron@erlich",
+ })
+ expect(m.override_reason).toBeNull()
+ expect(JSON.parse(JSON.stringify(m))).toHaveProperty("override_reason", null)
+ })
+})
+
+describe("#992 the #964 known-fixes check at deploy time", () => {
+ const writeOverlay = (files: Record) => {
+ const dir = mkdtempSync(join(tmpdir(), "deploy-guard-overlay-"))
+ for (const [rel, content] of Object.entries(files)) {
+ const abs = join(dir, rel)
+ mkdirSync(join(abs, ".."), { recursive: true })
+ writeFileSync(abs, content)
+ }
+ return dir
+ }
+
+ const HEALTHY = {
+ "components/prompt-input-v2.tsx": "export const x = promptDesignPlaceholder(\n mode(),\n placeholder(),\n)",
+ "context/global-sync/session-cache.ts":
+ "const diff_version: Record = {}\ndelete store.diff_version[sessionID]",
+ "i18n/en.ts": 'export const dict = { "session.exportTrace": "Export trace" }',
+ "i18n/de.ts": 'export const dict = { "session.exportTrace": "Trace exportieren" }',
+ }
+
+ test("a full overlay with every known hunk passes clean", () => {
+ const dir = writeOverlay(HEALTHY)
+ try {
+ expect(checkKnownFixes(dir)).toEqual([])
+ } finally {
+ rmSync(dir, { recursive: true, force: true })
+ }
+ })
+
+ test("a missing #929 3-arg translate call refuses with the named remedy", () => {
+ const dir = writeOverlay({ ...HEALTHY, "components/prompt-input-v2.tsx": "promptDesignPlaceholder(mode())" })
+ try {
+ const regressed = checkKnownFixes(dir)
+ expect(regressed).toHaveLength(1)
+ expect(regressed[0]).toContain("#929")
+ expect(regressed[0]).toContain("prompt-input-v2.tsx")
+ expect(regressed[0]).toContain("REFUSED")
+ expect(regressed[0]).toContain("#964")
+ } finally {
+ rmSync(dir, { recursive: true, force: true })
+ }
+ })
+
+ test("a missing #832 exportTrace locale entry refuses per locale", () => {
+ const dir = writeOverlay({
+ ...HEALTHY,
+ "i18n/de.ts": 'export const dict = { "session.other": "x" }',
+ "i18n/ar.ts": 'export const dict = { "session.exportTrace": "…" }',
+ })
+ try {
+ const regressed = checkKnownFixes(dir)
+ expect(regressed).toHaveLength(1)
+ expect(regressed[0]).toContain("i18n/de.ts")
+ expect(regressed[0]).toContain("#832")
+ } finally {
+ rmSync(dir, { recursive: true, force: true })
+ }
+ })
+
+ test("the mirrored fixture list matches the #964 guard's source of record", () => {
+ // deploy_guard.mjs mirrors KNOWN_FIXED_HUNKS from
+ // test/overlay_known_fixes_964.test.ts (the source of record). This
+ // cross-checks the two lists can't drift silently: same file set.
+ const guardSource = readFileSync(
+ join(__dirname, "overlay_known_fixes_964.test.ts"),
+ "utf8",
+ )
+ const guardFiles = [...guardSource.matchAll(/file: "([^"]+)"/g)].map((m) => m[1])
+ const guardSig = [...guardSource.matchAll(/signature: (\/.+\/[a-z]*)/g)].map((m) => m[1])
+ const mirrorSource = readFileSync(join(__dirname, "..", "scripts", "deploy_guard.mjs"), "utf8")
+ const mirrorFiles = [...mirrorSource.matchAll(/file: "([^"]+)"/g)].map((m) => m[1])
+ const mirrorSig = [...mirrorSource.matchAll(/signature: (\/.+\/[a-z]*)/g)].map((m) => m[1])
+ expect(mirrorFiles).toEqual(guardFiles)
+ expect(mirrorSig).toEqual(guardSig)
+ })
+})
+
+describe("#992 headRelation (the git probe → label mapping)", () => {
+ test("equal shas → at", () => {
+ expect(headRelation(HEAD, HEAD, true, true)).toBe("at")
+ })
+ test("HEAD behind origin/main → BEHIND", () => {
+ expect(headRelation(HEAD, ORIGIN, true, false)).toBe("BEHIND")
+ })
+ test("HEAD ahead of origin/main → AHEAD of", () => {
+ expect(headRelation(HEAD, ORIGIN, false, true)).toBe("AHEAD of")
+ })
+ test("diverged → DIVERGED from", () => {
+ expect(headRelation(HEAD, ORIGIN, false, false)).toBe("DIVERGED from")
+ })
+})