← back to Homesonspec
Harden snapshot offload verification boundaries (TK-10809)
cece23b9a135802b0453e2aa1fc5eeef3c02f10b · 2026-08-30 09:38:19 -0700 · Steve
Files touched
M apps/workers/src/snapshot-path.test.tsM apps/workers/src/snapshot-path.tsM ops/lib/snapshot-resolve.mjsA ops/lib/snapshot-resolve.test.mjsM ops/offload-snapshots.mjsA ops/offload-snapshots.test.mjsM ops/verify-snapshots.mjsA verification/TK-10809-codex-e2e-proof.json
Diff
commit cece23b9a135802b0453e2aa1fc5eeef3c02f10b
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Aug 30 09:38:19 2026 -0700
Harden snapshot offload verification boundaries (TK-10809)
---
apps/workers/src/snapshot-path.test.ts | 5 ++
apps/workers/src/snapshot-path.ts | 15 +++++-
ops/lib/snapshot-resolve.mjs | 32 ++++++++++---
ops/lib/snapshot-resolve.test.mjs | 37 +++++++++++++++
ops/offload-snapshots.mjs | 57 ++++++++++++++++++-----
ops/offload-snapshots.test.mjs | 73 ++++++++++++++++++++++++++++++
ops/verify-snapshots.mjs | 70 +++++++++++++++++++++++-----
verification/TK-10809-codex-e2e-proof.json | 38 ++++++++++++++++
8 files changed, 295 insertions(+), 32 deletions(-)
diff --git a/apps/workers/src/snapshot-path.test.ts b/apps/workers/src/snapshot-path.test.ts
index 2879c2af..a9de2069 100644
--- a/apps/workers/src/snapshot-path.test.ts
+++ b/apps/workers/src/snapshot-path.test.ts
@@ -52,6 +52,11 @@ describe("resolveSnapshotPath", () => {
"/mnt/vol/snapshots/k/h.html",
);
});
+ it("rejects a relative path that escapes the configured snapshot root", () => {
+ expect(() => resolveSnapshotPath("../../outside.html", "/mnt/vol/snapshots")).toThrow(
+ /escapes snapshot root/,
+ );
+ });
});
describe("round-trip: write to disk, store relative, resolve, read back", () => {
diff --git a/apps/workers/src/snapshot-path.ts b/apps/workers/src/snapshot-path.ts
index 8e3f46fd..ae84f687 100644
--- a/apps/workers/src/snapshot-path.ts
+++ b/apps/workers/src/snapshot-path.ts
@@ -1,4 +1,4 @@
-import { isAbsolute, join } from "node:path";
+import { isAbsolute, join, relative, resolve } from "node:path";
// Single source of truth for the raw-snapshot store location. Prod offloads this
// to a dedicated volume by setting SNAPSHOT_DIR (see ops/offload-snapshots.mjs);
@@ -23,5 +23,16 @@ const relTail = (p: string) => p.replace(/^(?:\.\/)?var\/snapshots\//, "");
// resolving verbatim. New rows store a POSIX-relative path resolved against the
// active snapshot dir, so moving the volume + repointing SNAPSHOT_DIR is exact.
export function resolveSnapshotPath(storagePath: string, snapshotDir = SNAPSHOT_DIR) {
- return isAbsolute(storagePath) ? storagePath : join(snapshotDir, relTail(storagePath));
+ if (isAbsolute(storagePath)) return storagePath;
+ const root = resolve(snapshotDir);
+ const candidate = resolve(root, relTail(storagePath));
+ const rel = relative(root, candidate);
+ if (
+ rel === ".." ||
+ rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) ||
+ isAbsolute(rel)
+ ) {
+ throw new Error(`relative snapshot path escapes snapshot root: ${storagePath}`);
+ }
+ return candidate;
}
diff --git a/ops/lib/snapshot-resolve.mjs b/ops/lib/snapshot-resolve.mjs
index 26cb1fba..3b13fc28 100644
--- a/ops/lib/snapshot-resolve.mjs
+++ b/ops/lib/snapshot-resolve.mjs
@@ -1,7 +1,7 @@
// Shared snapshot-path resolver for the ops/*.mjs + scripts/*.mjs snapshot tooling
// (TK-10809). ONE source of truth so offload, verify, and backfill can't drift.
// apps/workers/src/snapshot-path.ts mirrors relTail() on the TS side (JS/TS boundary).
-import { isAbsolute, join, basename } from "node:path";
+import { isAbsolute, resolve, relative } from "node:path";
const stripTrailingSlash = (p) => p.replace(/\/$/, "");
@@ -9,9 +9,23 @@ const stripTrailingSlash = (p) => p.replace(/\/$/, "");
// local dw rows carry) so the tail resolves under the active snapshot dir directly.
export const relTail = (p) => p.replace(/^(?:\.\/)?var\/snapshots\//, "");
+const resolveContained = (dir, tail) => {
+ const root = resolve(dir);
+ const candidate = resolve(root, tail);
+ const rel = relative(root, candidate);
+ if (
+ rel === ".." ||
+ rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) ||
+ isAbsolute(rel)
+ ) {
+ throw new Error(`relative snapshot path escapes snapshot root: ${tail}`);
+ }
+ return candidate;
+};
+
// Resolve a stored storagePath against an arbitrary snapshot dir (verify path).
export const resolveAgainstDir = (storagePath, dir) =>
- isAbsolute(storagePath) ? storagePath : join(dir, relTail(storagePath));
+ isAbsolute(storagePath) ? storagePath : resolveContained(dir, relTail(storagePath));
// Resolve a stored storagePath against the DEST for the offload copy check.
// Returns { path, absNotUnderSrc } so the caller can tally the "wrong --src"
@@ -20,13 +34,17 @@ export const resolveAgainstDir = (storagePath, dir) =>
export const resolveAgainstDest = (storagePath, src, dest) => {
const destBase = stripTrailingSlash(dest);
if (!isAbsolute(storagePath)) {
- return { path: join(destBase, relTail(storagePath)), absNotUnderSrc: false };
+ return { path: resolveContained(destBase, relTail(storagePath)), absNotUnderSrc: false };
}
const srcBase = stripTrailingSlash(src);
if (storagePath.startsWith(srcBase + "/")) {
- return { path: join(destBase, storagePath.slice(srcBase.length + 1)), absNotUnderSrc: false };
+ return {
+ path: resolveContained(destBase, storagePath.slice(srcBase.length + 1)),
+ absNotUnderSrc: false,
+ };
}
- // absolute path not under SRC — cannot be resolved against DEST; fall back to
- // basename subtree but SIGNAL the case so it's counted separately, not as a miss.
- return { path: join(destBase, basename(storagePath)), absNotUnderSrc: true };
+ // An absolute path outside SRC has no defensible destination mapping. Return no
+ // candidate rather than guessing by basename and potentially hash-validating the
+ // wrong file. The caller reports this separately and treats it as unverified.
+ return { path: null, absNotUnderSrc: true };
};
diff --git a/ops/lib/snapshot-resolve.test.mjs b/ops/lib/snapshot-resolve.test.mjs
new file mode 100644
index 00000000..9031b7ab
--- /dev/null
+++ b/ops/lib/snapshot-resolve.test.mjs
@@ -0,0 +1,37 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { relTail, resolveAgainstDir, resolveAgainstDest } from "./snapshot-resolve.mjs";
+
+test("normalizes legacy relative prefixes under the active store", () => {
+ assert.equal(relTail("./var/snapshots/source/hash.html"), "source/hash.html");
+ assert.equal(
+ resolveAgainstDir("var/snapshots/source/hash.html", "/mnt/snapshots"),
+ "/mnt/snapshots/source/hash.html",
+ );
+});
+
+test("keeps legacy absolute paths unchanged for runtime reads", () => {
+ assert.equal(resolveAgainstDir("/legacy/source/hash.html", "/mnt/snapshots"), "/legacy/source/hash.html");
+});
+
+test("maps legacy absolute paths under src to the same tail under dest", () => {
+ assert.deepEqual(
+ resolveAgainstDest("/old/snapshots/source/hash.html", "/old/snapshots", "/new/snapshots"),
+ { path: "/new/snapshots/source/hash.html", absNotUnderSrc: false },
+ );
+});
+
+test("does not guess a destination for absolute paths outside src", () => {
+ assert.deepEqual(
+ resolveAgainstDest("/other/source/hash.html", "/old/snapshots", "/new/snapshots"),
+ { path: null, absNotUnderSrc: true },
+ );
+});
+
+test("rejects relative traversal outside the snapshot root", () => {
+ assert.throws(() => resolveAgainstDir("../../etc/passwd", "/mnt/snapshots"), /escapes snapshot root/);
+ assert.throws(
+ () => resolveAgainstDest("../outside", "/old/snapshots", "/new/snapshots"),
+ /escapes snapshot root/,
+ );
+});
diff --git a/ops/offload-snapshots.mjs b/ops/offload-snapshots.mjs
index baeac3f3..3da3a3cb 100644
--- a/ops/offload-snapshots.mjs
+++ b/ops/offload-snapshots.mjs
@@ -15,7 +15,7 @@
import { execFileSync } from "node:child_process";
import { readFileSync, statSync } from "node:fs";
import { createHash } from "node:crypto";
-import { isAbsolute } from "node:path";
+import { isAbsolute, relative, resolve } from "node:path";
import { resolveAgainstDest } from "./lib/snapshot-resolve.mjs";
const argv = process.argv.slice(2);
@@ -39,8 +39,17 @@ if (isAbsolute(DEST) === false || isAbsolute(SRC) === false) {
console.error("FATAL: --dest and --src must be absolute paths.");
process.exit(2);
}
-if (DEST === SRC) {
- console.error("FATAL: --dest must differ from --src (refusing to copy onto itself).");
+if (!Number.isInteger(SAMPLE) || SAMPLE <= 0) {
+ console.error("FATAL: --sample must be a positive integer.");
+ process.exit(2);
+}
+const srcRoot = resolve(SRC);
+const destRoot = resolve(DEST);
+const srcToDest = relative(srcRoot, destRoot);
+const destToSrc = relative(destRoot, srcRoot);
+const nested = (rel) => rel !== "" && rel !== ".." && !rel.startsWith("../") && !isAbsolute(rel);
+if (srcRoot === destRoot || nested(srcToDest) || nested(destToSrc)) {
+ console.error("FATAL: --src and --dest must be disjoint paths (refusing an overlapping copy).");
process.exit(2);
}
@@ -113,12 +122,27 @@ console.log(
let ok = 0;
const misses = [];
const mismatches = [];
+const invalid = [];
let absNotUnderSrc = 0;
for (const line of rows) {
const [storagePath, contentHash] = line.split("\t");
- if (!storagePath || !contentHash) continue;
- const { path: dest, absNotUnderSrc: notUnder } = resolveAgainstDest(storagePath, SRC, DEST);
+ if (!storagePath || !contentHash) {
+ invalid.push({ storagePath: storagePath || "(empty)", reason: "malformed DB row" });
+ continue;
+ }
+ let resolved;
+ try {
+ resolved = resolveAgainstDest(storagePath, SRC, DEST);
+ } catch (error) {
+ invalid.push({ storagePath, reason: error.message });
+ continue;
+ }
+ const { path: dest, absNotUnderSrc: notUnder } = resolved;
if (notUnder) absNotUnderSrc++;
+ if (!dest) {
+ invalid.push({ storagePath, reason: "absolute path is outside --src" });
+ continue;
+ }
try {
statSync(dest);
} catch {
@@ -135,16 +159,23 @@ const coverage = ((ok / total) * 100).toFixed(2);
console.log(`\n coverage: ${coverage}% (ok ${ok} / ${rows.length})`);
console.log(` missing : ${misses.length}`);
console.log(` mismatch: ${mismatches.length}`);
+console.log(` invalid : ${invalid.length}`);
console.log(
- ` abs-not-under-src: ${absNotUnderSrc} (these need --src to match the absolute paths in legacy rows — not necessarily a copy failure)`,
+ ` abs-not-under-src: ${absNotUnderSrc} (unverified: --src does not cover these legacy absolute paths)`,
);
for (const m of misses.slice(0, 20)) console.log(` MISS ${m.storagePath} -> ${m.dest}`);
if (misses.length > 20) console.log(` … and ${misses.length - 20} more misses`);
for (const m of mismatches.slice(0, 20))
console.log(` HASH ${m.storagePath}: expected ${m.expected.slice(0, 12)} got ${m.got.slice(0, 12)}`);
if (mismatches.length > 20) console.log(` … and ${mismatches.length - 20} more mismatches`);
+for (const m of invalid.slice(0, 20)) console.log(` INVALID ${m.storagePath}: ${m.reason}`);
-const clean = misses.length === 0 && mismatches.length === 0;
+const rowCoverageComplete = rows.length === totalRows && totalRows > 0;
+const clean =
+ misses.length === 0 &&
+ mismatches.length === 0 &&
+ invalid.length === 0 &&
+ (!FULL || rowCoverageComplete);
// Banner + exit-code logic (FIX 1). A SAMPLE run — even a clean one — is NOT proof
// of a complete copy, so it must never present as a plain "VERIFY PASS" or exit 0
@@ -164,12 +195,14 @@ if (clean && FULL) {
// Exit codes under --apply: FULL+clean = 0; any dirty = 1; SAMPLE+clean = 3
// (distinct so downstream can't treat sample-only as a proven-clean copy).
-// In dry-run/no --apply, keep exit 0 (the SAMPLE-ONLY warning above still prints).
+// A dirty verification is non-zero even in planning mode: callers must never infer
+// that a printed rsync plan also proved the destination. SAMPLE+clean under --apply
+// remains a distinct exit 3 because the copy ran but completeness is unproven.
+if (!clean) {
+ console.error("FAIL: destination is not fully verified; DO NOT proceed to repoint or delete.");
+ process.exit(1);
+}
if (APPLY) {
- if (!clean) {
- console.error("FAIL: verify coverage <100% under --apply; DO NOT proceed to repoint or delete.");
- process.exit(1);
- }
if (!FULL) {
console.error(
"SAMPLE-ONLY under --apply: exiting 3 (not proven-complete). Re-run with --full before any gated delete.",
diff --git a/ops/offload-snapshots.test.mjs b/ops/offload-snapshots.test.mjs
new file mode 100644
index 00000000..f2b6f303
--- /dev/null
+++ b/ops/offload-snapshots.test.mjs
@@ -0,0 +1,73 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, unlinkSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { createHash } from "node:crypto";
+import { spawnSync } from "node:child_process";
+
+const script = join(import.meta.dirname, "offload-snapshots.mjs");
+
+function fixture() {
+ const root = mkdtempSync(join(tmpdir(), "snapshot-offload-test-"));
+ const src = join(root, "src");
+ const dest = join(root, "dest");
+ const bin = join(root, "bin");
+ mkdirSync(join(src, "builder"), { recursive: true });
+ mkdirSync(join(dest, "builder"), { recursive: true });
+ mkdirSync(bin);
+ const body = Buffer.from("preserved raw evidence\n");
+ const hash = createHash("sha256").update(body).digest("hex");
+ writeFileSync(join(src, "builder", `${hash}.html`), body);
+ writeFileSync(join(dest, "builder", `${hash}.html`), body);
+ const psql = join(bin, "psql");
+ writeFileSync(
+ psql,
+ `#!/bin/sh\ncase "$*" in\n *'count(*)'*) printf '1\\n' ;;\n *) printf 'builder/${hash}.html\\t${hash}\\n' ;;\nesac\n`,
+ );
+ chmodSync(psql, 0o755);
+ return { root, src, dest, bin, hash };
+}
+
+function run(args, bin) {
+ return spawnSync(process.execPath, [script, ...args], {
+ encoding: "utf8",
+ env: { ...process.env, PATH: `${bin}:${process.env.PATH}` },
+ });
+}
+
+test("full read-only verification proves every DB row by hash", () => {
+ const { src, dest, bin } = fixture();
+ const result = run(["--src", src, "--dest", dest, "--full"], bin);
+ assert.equal(result.status, 0, result.stderr || result.stdout);
+ assert.match(result.stdout, /VERIFY PASS \(FULL — 1 rows\)/);
+ assert.match(result.stdout, /COPY ONLY, no --delete/);
+});
+
+test("real copy-only rsync followed by full hash verification succeeds", () => {
+ const { src, dest, bin, hash } = fixture();
+ unlinkSync(join(dest, "builder", `${hash}.html`));
+ const result = run(["--src", src, "--dest", dest, "--apply", "--full"], bin);
+ assert.equal(result.status, 0, result.stderr || result.stdout);
+ assert.match(result.stdout, /rsync complete \(copy only\)/);
+ assert.match(result.stdout, /VERIFY PASS \(FULL — 1 rows\)/);
+});
+
+test("sample-only apply cannot masquerade as a complete verification", () => {
+ const { root, src, dest, bin } = fixture();
+ const rsync = join(bin, "rsync");
+ writeFileSync(rsync, "#!/bin/sh\nexit 0\n");
+ chmodSync(rsync, 0o755);
+ const result = run(["--src", src, "--dest", dest, "--apply", "--sample", "1"], bin);
+ assert.equal(result.status, 3, result.stderr || result.stdout);
+ assert.match(result.stdout, /SAMPLE ONLY/);
+ assert.match(result.stderr, /not proven-complete/);
+ assert.ok(root);
+});
+
+test("overlapping source and destination fail before copy or DB access", () => {
+ const { src, bin } = fixture();
+ const result = run(["--src", src, "--dest", join(src, "nested")], bin);
+ assert.equal(result.status, 2);
+ assert.match(result.stderr, /must be disjoint/);
+});
diff --git a/ops/verify-snapshots.mjs b/ops/verify-snapshots.mjs
index fc3be0ac..be929a3a 100644
--- a/ops/verify-snapshots.mjs
+++ b/ops/verify-snapshots.mjs
@@ -20,6 +20,10 @@ const opt = (name, def) => {
const FULL = flag("full");
const SAMPLE = Number(opt("sample", "500"));
+if (!Number.isInteger(SAMPLE) || SAMPLE <= 0) {
+ console.error("FATAL: --sample must be a positive integer.");
+ process.exit(2);
+}
const SNAPSHOT_DIR = process.env.SNAPSHOT_DIR ?? join(import.meta.dirname, "../var/snapshots");
const resolveSnapshot = (p) => resolveAgainstDir(p, SNAPSHOT_DIR);
@@ -33,47 +37,91 @@ const writeLatest = (obj) => {
writeFileSync(join(dir, "latest.json"), JSON.stringify(obj, null, 2));
};
-let rows;
+let rows, totalRows;
try {
+ totalRows = Number(psql(`select count(*) from "RawSnapshot"`)) || 0;
const orderClause = FULL ? "" : `order by random() limit ${SAMPLE}`;
rows = psql(`select "storagePath" || E'\\t' || "contentHash" from "RawSnapshot" ${orderClause}`)
.split("\n")
.filter(Boolean);
} catch (e) {
- const rec = { verdict: "FAIL", status: "FAIL", checked: 0, ok: 0, missing: 0, mismatch: 0, error: e.message.split("\n")[0] };
+ const rec = {
+ verdict: "FAIL",
+ status: "FAIL",
+ checked: 0,
+ ok: 0,
+ missing: 0,
+ mismatch: 0,
+ error: e.message.split("\n")[0],
+ };
writeLatest(rec);
console.error(`FAIL: could not read RawSnapshot (${rec.error})`);
process.exit(1);
}
-let ok = 0, missing = 0, mismatch = 0;
-const misses = [], mismatches = [];
+let ok = 0,
+ missing = 0,
+ mismatch = 0,
+ invalid = 0;
+const misses = [],
+ mismatches = [],
+ invalids = [];
for (const line of rows) {
const [storagePath, contentHash] = line.split("\t");
- if (!storagePath || !contentHash) continue;
- const path = resolveSnapshot(storagePath);
+ if (!storagePath || !contentHash) {
+ invalid++;
+ invalids.push(storagePath || "(empty)");
+ continue;
+ }
+ let path;
+ try {
+ path = resolveSnapshot(storagePath);
+ } catch {
+ invalid++;
+ invalids.push(storagePath);
+ continue;
+ }
try {
statSync(path);
} catch {
- missing++; misses.push(storagePath); continue;
+ missing++;
+ misses.push(storagePath);
+ continue;
}
const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
if (digest === contentHash) ok++;
- else { mismatch++; mismatches.push(storagePath); }
+ else {
+ mismatch++;
+ mismatches.push(storagePath);
+ }
}
const checked = rows.length;
+const expectedChecked = FULL ? totalRows : Math.min(SAMPLE, totalRows);
// PASS = all sampled files present + hash-match. WARN = only missing files (a move
// mid-flight or an un-offloaded row). FAIL = any hash mismatch (corruption).
let verdict;
-if (mismatch > 0) verdict = "FAIL";
+if (mismatch > 0 || invalid > 0 || totalRows === 0 || checked !== expectedChecked) verdict = "FAIL";
else if (missing > 0) verdict = "WARN";
else verdict = "PASS";
-const rec = { verdict, status: verdict, checked, ok, missing, mismatch };
+const rec = {
+ verdict,
+ status: verdict,
+ full: FULL,
+ totalRows,
+ checked,
+ ok,
+ missing,
+ mismatch,
+ invalid,
+};
writeLatest(rec);
-console.log(`snapshot integrity: ${verdict} checked=${checked} ok=${ok} missing=${missing} mismatch=${mismatch}`);
+console.log(
+ `snapshot integrity: ${verdict} checked=${checked}/${totalRows} ok=${ok} missing=${missing} mismatch=${mismatch} invalid=${invalid}`,
+);
for (const p of misses.slice(0, 10)) console.log(` MISS ${p}`);
for (const p of mismatches.slice(0, 10)) console.log(` HASH ${p}`);
+for (const p of invalids.slice(0, 10)) console.log(` INVALID ${p}`);
process.exit(verdict === "FAIL" ? 1 : 0);
diff --git a/verification/TK-10809-codex-e2e-proof.json b/verification/TK-10809-codex-e2e-proof.json
new file mode 100644
index 00000000..da77a899
--- /dev/null
+++ b/verification/TK-10809-codex-e2e-proof.json
@@ -0,0 +1,38 @@
+{
+ "ticket": "TK-10809",
+ "model": "codex",
+ "intent": "Prove that snapshot evidence can be copied to a disjoint store without deletion and that every selected DB row resolves and hash-matches at the destination.",
+ "riskTier": "R1",
+ "environment": "local macOS workspace; temporary snapshot stores and a deterministic fake read-only psql boundary; real rsync for the copy journey",
+ "baseline": {
+ "commit": "ca3c67b35c1eadf4b99cb1bdff831f86cd4ad41d",
+ "productionTouched": false,
+ "evidencePruned": false
+ },
+ "timestamp": "2026-08-30T16:37:04Z",
+ "commands": [
+ "node --test ops/lib/snapshot-resolve.test.mjs ops/offload-snapshots.test.mjs",
+ "apps/workers/node_modules/.bin/vitest run apps/workers/src/snapshot-path.test.ts",
+ "apps/workers/node_modules/.bin/tsc --noEmit -p apps/workers/tsconfig.json",
+ "node --check ops/offload-snapshots.mjs",
+ "node --check ops/verify-snapshots.mjs",
+ "node --check scripts/backfill-drh-geo-from-cache.mjs",
+ "git diff --check"
+ ],
+ "assertions": [
+ { "boundary": "path resolver", "verdict": "PASS", "evidence": "Relative and var/snapshots-prefixed rows resolve under the active root; traversal is rejected; legacy absolute rows remain readable." },
+ { "boundary": "copy", "verdict": "PASS", "evidence": "Real rsync -a copied a temporary raw-evidence file with no --delete and retained the source." },
+ { "boundary": "database-to-file", "verdict": "PASS", "evidence": "Full mode resolved 1/1 deterministic RawSnapshot row against the destination and matched SHA-256 contentHash." },
+ { "boundary": "incomplete verification", "verdict": "PASS", "evidence": "Apply plus sample-only verification exits 3 and explicitly states that completeness is not proven." },
+ { "boundary": "overlap guard", "verdict": "PASS", "evidence": "Destination nested under source exits 2 before DB or copy access." },
+ { "boundary": "worker compatibility", "verdict": "PASS", "evidence": "10 focused snapshot-path tests pass and worker TypeScript compilation is clean." }
+ ],
+ "negativeChecks": [
+ "Relative ../ traversal cannot escape SNAPSHOT_DIR.",
+ "Absolute legacy rows outside the declared source are not guessed by basename and remain unverified.",
+ "Invalid sample sizes fail closed.",
+ "Empty, malformed, missing, mismatched, or incomplete full verification cannot print a clean full-pass verdict."
+ ],
+ "cleanup": "All copy fixtures were created under the OS temporary directory. No production state, database rows, source snapshots, services, or approval-gated configuration were changed.",
+ "verdict": "PASS"
+}
← ca3c67b3 Add Mungo and Chafin CPG collectors
·
back to Homesonspec
·
Record TK-10809 post-commit proof ef253225 →