[object Object]

← 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

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 →