← back to Homesonspec
TK-10809: offload VERIFY — no false PASS on a sample, tally wrong --src
aff87907c517dd6bae3cf800bf8238678e00567e · 2026-08-30 09:33:50 -0700 · Steve
FIX1: show sampled/total + coverage% of the WHOLE store; a clean SAMPLE prints
'VERIFY PASS (SAMPLE ONLY …)' + a loud not-proof warning and, under --apply, exits
3 (distinct) so automation can't treat sample-only as a proven-clean copy. FULL+clean=0,
any dirty=1. FIX3: count abs-not-under-src rows separately (wrong --src, not a copy
failure) so the operator can tell it from an incomplete copy. Uses the shared resolver.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M ops/offload-snapshots.mjs
Diff
commit aff87907c517dd6bae3cf800bf8238678e00567e
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Aug 30 09:33:50 2026 -0700
TK-10809: offload VERIFY — no false PASS on a sample, tally wrong --src
FIX1: show sampled/total + coverage% of the WHOLE store; a clean SAMPLE prints
'VERIFY PASS (SAMPLE ONLY …)' + a loud not-proof warning and, under --apply, exits
3 (distinct) so automation can't treat sample-only as a proven-clean copy. FULL+clean=0,
any dirty=1. FIX3: count abs-not-under-src rows separately (wrong --src, not a copy
failure) so the operator can tell it from an incomplete copy. Uses the shared resolver.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
ops/offload-snapshots.mjs | 65 ++++++++++++++++++++++++++++++++---------------
1 file changed, 45 insertions(+), 20 deletions(-)
diff --git a/ops/offload-snapshots.mjs b/ops/offload-snapshots.mjs
index 805696ed..baeac3f3 100644
--- a/ops/offload-snapshots.mjs
+++ b/ops/offload-snapshots.mjs
@@ -15,7 +15,8 @@
import { execFileSync } from "node:child_process";
import { readFileSync, statSync } from "node:fs";
import { createHash } from "node:crypto";
-import { isAbsolute, join, basename } from "node:path";
+import { isAbsolute } from "node:path";
+import { resolveAgainstDest } from "./lib/snapshot-resolve.mjs";
const argv = process.argv.slice(2);
const flag = (name) => argv.includes(`--${name}`);
@@ -95,33 +96,29 @@ if (APPLY) {
// ── VERIFY phase ───────────────────────────────────────────────────────
console.log("\n── VERIFY phase (READ-ONLY, resolves against DEST) ──");
+const totalRows = Number(psql(`select count(*) from "RawSnapshot"`)) || 0;
const orderClause = FULL ? "" : `order by random() limit ${SAMPLE}`;
const rows = psql(
`select "storagePath" || E'\\t' || "contentHash" from "RawSnapshot" ${orderClause}`,
)
.split("\n")
.filter(Boolean);
-console.log(` sampled ${rows.length} RawSnapshot rows (${FULL ? "FULL" : `sample ${SAMPLE}`})`);
-
-// resolve a stored path against DEST: relative → DEST/rel; absolute legacy → swap
-// its SRC prefix to DEST (the file was copied there under the same subtree).
-// New rows store sourceKey/hash.ext; some existing rows carry the store's own
-// "var/snapshots/" tail — strip it so the file resolves under DEST directly.
-const relTail = (p) => p.replace(/^(?:\.\/)?var\/snapshots\//, "");
-const resolveAgainstDest = (p) => {
- if (!isAbsolute(p)) return join(DEST, relTail(p));
- if (p.startsWith(SRC.replace(/\/$/, "") + "/")) return join(DEST, p.slice(SRC.replace(/\/$/, "").length + 1));
- // absolute path not under SRC — fall back to matching basename subtree
- return join(DEST, basename(p));
-};
+const sampledPct = totalRows > 0 ? ((rows.length / totalRows) * 100).toFixed(2) : "0.00";
+console.log(
+ FULL
+ ? ` verifying FULL store: ${rows.length}/${totalRows} rows (100% coverage)`
+ : ` SAMPLE run: ${rows.length}/${totalRows} rows (${sampledPct}% of the whole store)`,
+);
let ok = 0;
const misses = [];
const mismatches = [];
+let absNotUnderSrc = 0;
for (const line of rows) {
const [storagePath, contentHash] = line.split("\t");
if (!storagePath || !contentHash) continue;
- const dest = resolveAgainstDest(storagePath);
+ const { path: dest, absNotUnderSrc: notUnder } = resolveAgainstDest(storagePath, SRC, DEST);
+ if (notUnder) absNotUnderSrc++;
try {
statSync(dest);
} catch {
@@ -138,6 +135,9 @@ 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(
+ ` abs-not-under-src: ${absNotUnderSrc} (these need --src to match the absolute paths in legacy rows — not necessarily a copy failure)`,
+);
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))
@@ -145,10 +145,35 @@ for (const m of mismatches.slice(0, 20))
if (mismatches.length > 20) console.log(` … and ${mismatches.length - 20} more mismatches`);
const clean = misses.length === 0 && mismatches.length === 0;
-console.log(`\n=== ${clean ? "VERIFY PASS" : "VERIFY INCOMPLETE"} ===`);
-// under --apply, a non-100% verify is a hard failure — the copy is not proven exact.
-if (APPLY && !clean) {
- console.error("FAIL: verify coverage <100% under --apply; DO NOT proceed to repoint or delete.");
- process.exit(1);
+// 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
+// under --apply (downstream automation would treat sample-only as proven-clean).
+if (clean && FULL) {
+ console.log(`\n=== VERIFY PASS (FULL — ${rows.length} rows) ===`);
+} else if (clean && !FULL) {
+ console.log(`\n=== VERIFY PASS (SAMPLE ONLY — ${rows.length}/${totalRows} rows, ${sampledPct}%) ===`);
+ console.log(
+ "WARNING: sample verify is NOT proof of a complete copy. Re-run with --full before any gated delete.",
+ );
+} else if (!clean && FULL) {
+ console.log(`\n=== VERIFY FAIL ===`);
+} else {
+ console.log(`\n=== VERIFY FAIL (SAMPLE) ===`);
+}
+
+// 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).
+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.",
+ );
+ process.exit(3);
+ }
}
← d461d39d TK-10809: single shared snapshot-path resolver (ops/lib), ki
·
back to Homesonspec
·
Add Mungo and Chafin CPG collectors ca3c67b3 →