[object Object]

← back to Homesonspec

TK-10809: single shared snapshot-path resolver (ops/lib), kill 3-way drift

d461d39d924a1ca52ab1506125a561b2d3d5d974 · 2026-08-30 09:33:39 -0700 · Steve

Extract relTail/resolveAgainstDir/resolveAgainstDest into ops/lib/snapshot-resolve.mjs
and import it in verify-snapshots.mjs + backfill-drh-geo-from-cache.mjs. Make the TS
resolveSnapshotPath strip the same var/snapshots/ tail so it stops lying about being
the single source of truth; add the tail-strip test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit d461d39d924a1ca52ab1506125a561b2d3d5d974
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Aug 30 09:33:39 2026 -0700

    TK-10809: single shared snapshot-path resolver (ops/lib), kill 3-way drift
    
    Extract relTail/resolveAgainstDir/resolveAgainstDest into ops/lib/snapshot-resolve.mjs
    and import it in verify-snapshots.mjs + backfill-drh-geo-from-cache.mjs. Make the TS
    resolveSnapshotPath strip the same var/snapshots/ tail so it stops lying about being
    the single source of truth; add the tail-strip test.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 apps/workers/src/snapshot-path.test.ts  |  8 ++++++++
 apps/workers/src/snapshot-path.ts       |  7 ++++++-
 ops/lib/snapshot-resolve.mjs            | 32 ++++++++++++++++++++++++++++++++
 ops/verify-snapshots.mjs                |  8 +++-----
 scripts/backfill-drh-geo-from-cache.mjs |  6 +++---
 5 files changed, 52 insertions(+), 9 deletions(-)

diff --git a/apps/workers/src/snapshot-path.test.ts b/apps/workers/src/snapshot-path.test.ts
index ed523b33..2879c2af 100644
--- a/apps/workers/src/snapshot-path.test.ts
+++ b/apps/workers/src/snapshot-path.test.ts
@@ -44,6 +44,14 @@ describe("resolveSnapshotPath", () => {
       "/mnt/vol/snapshots/k/h.html",
     );
   });
+  it("strips a leading var/snapshots/ (and ./var/snapshots/) mirror-prefix tail", () => {
+    expect(resolveSnapshotPath("var/snapshots/k/h.html", "/mnt/vol/snapshots")).toBe(
+      "/mnt/vol/snapshots/k/h.html",
+    );
+    expect(resolveSnapshotPath("./var/snapshots/k/h.html", "/mnt/vol/snapshots")).toBe(
+      "/mnt/vol/snapshots/k/h.html",
+    );
+  });
 });
 
 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 e151047d..8e3f46fd 100644
--- a/apps/workers/src/snapshot-path.ts
+++ b/apps/workers/src/snapshot-path.ts
@@ -14,9 +14,14 @@ export function snapshotRelativePath(sourceKey: string, contentHash: string, con
   return `${sourceKey}/${contentHash}${snapshotExt(contentType)}`;
 }
 
+// Strip a leading "./" then a leading "var/snapshots/" (the mirror-prefix some
+// local dw rows carry) so the tail resolves under the active snapshot dir directly.
+// ops/*.mjs mirror this in ops/lib/snapshot-resolve.mjs (kept in sync; JS/TS boundary).
+const relTail = (p: string) => p.replace(/^(?:\.\/)?var\/snapshots\//, "");
+
 // Legacy rows stored an ABSOLUTE storagePath (pre-offload); those must keep
 // 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, storagePath);
+  return isAbsolute(storagePath) ? storagePath : join(snapshotDir, relTail(storagePath));
 }
diff --git a/ops/lib/snapshot-resolve.mjs b/ops/lib/snapshot-resolve.mjs
new file mode 100644
index 00000000..26cb1fba
--- /dev/null
+++ b/ops/lib/snapshot-resolve.mjs
@@ -0,0 +1,32 @@
+// 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";
+
+const stripTrailingSlash = (p) => p.replace(/\/$/, "");
+
+// Strip a leading "./" then a leading "var/snapshots/" (the mirror-prefix some
+// local dw rows carry) so the tail resolves under the active snapshot dir directly.
+export const relTail = (p) => p.replace(/^(?:\.\/)?var\/snapshots\//, "");
+
+// Resolve a stored storagePath against an arbitrary snapshot dir (verify path).
+export const resolveAgainstDir = (storagePath, dir) =>
+  isAbsolute(storagePath) ? storagePath : join(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"
+// class (an absolute legacy row that is NOT under SRC) distinctly from a real
+// incomplete copy — see FIX 3.
+export const resolveAgainstDest = (storagePath, src, dest) => {
+  const destBase = stripTrailingSlash(dest);
+  if (!isAbsolute(storagePath)) {
+    return { path: join(destBase, relTail(storagePath)), absNotUnderSrc: false };
+  }
+  const srcBase = stripTrailingSlash(src);
+  if (storagePath.startsWith(srcBase + "/")) {
+    return { path: join(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 };
+};
diff --git a/ops/verify-snapshots.mjs b/ops/verify-snapshots.mjs
index 21f23dab..fc3be0ac 100644
--- a/ops/verify-snapshots.mjs
+++ b/ops/verify-snapshots.mjs
@@ -8,7 +8,8 @@
 import { execFileSync } from "node:child_process";
 import { readFileSync, statSync, mkdirSync, writeFileSync } from "node:fs";
 import { createHash } from "node:crypto";
-import { isAbsolute, join } from "node:path";
+import { join } from "node:path";
+import { resolveAgainstDir } from "./lib/snapshot-resolve.mjs";
 
 const argv = process.argv.slice(2);
 const flag = (name) => argv.includes(`--${name}`);
@@ -20,10 +21,7 @@ const opt = (name, def) => {
 const FULL = flag("full");
 const SAMPLE = Number(opt("sample", "500"));
 const SNAPSHOT_DIR = process.env.SNAPSHOT_DIR ?? join(import.meta.dirname, "../var/snapshots");
-// New rows store sourceKey/hash.ext; some existing rows carry the store's own
-// "var/snapshots/" tail — strip it so both resolve under the active SNAPSHOT_DIR.
-const relTail = (p) => p.replace(/^(?:\.\/)?var\/snapshots\//, "");
-const resolveSnapshot = (p) => (isAbsolute(p) ? p : join(SNAPSHOT_DIR, relTail(p)));
+const resolveSnapshot = (p) => resolveAgainstDir(p, SNAPSHOT_DIR);
 
 const DB = process.env.DATABASE_URL || "postgresql://macstudio3@localhost/homesonspec?host=/tmp";
 const psql = (sql) =>
diff --git a/scripts/backfill-drh-geo-from-cache.mjs b/scripts/backfill-drh-geo-from-cache.mjs
index abf66a8d..c2e06dfb 100644
--- a/scripts/backfill-drh-geo-from-cache.mjs
+++ b/scripts/backfill-drh-geo-from-cache.mjs
@@ -7,7 +7,8 @@
 // State-gated + coarse US bbox (a wrong pin is worse than null).
 import { execFileSync } from "node:child_process";
 import { readFileSync } from "node:fs";
-import { isAbsolute, join } from "node:path";
+import { join } from "node:path";
+import { resolveAgainstDir } from "../ops/lib/snapshot-resolve.mjs";
 
 const DB = process.env.DATABASE_URL || "postgresql://macstudio3@localhost/homesonspec?host=/tmp";
 const psql = (sql) => execFileSync("psql", [DB, "-tAc", sql], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 }).trim();
@@ -16,8 +17,7 @@ const q = (s) => String(s).replace(/'/g, "''");
 // storagePath is stored absolute on legacy rows, POSIX-relative on new rows
 // (post snapshot-store offload). Resolve both against the active snapshot dir.
 const SNAPSHOT_DIR = process.env.SNAPSHOT_DIR ?? join(import.meta.dirname, "../var/snapshots");
-const relTail = (p) => p.replace(/^(?:\.\/)?var\/snapshots\//, "");
-const resolveSnapshot = (p) => (isAbsolute(p) ? p : join(SNAPSHOT_DIR, relTail(p)));
+const resolveSnapshot = (p) => resolveAgainstDir(p, SNAPSHOT_DIR);
 
 // Same community-coord logic as the dr-horton adapter (lowercase ld+json lat/lon; state-gated + US bbox).
 function communityGeo(html, state) {

← 58cad18e ops: non-destructive snapshot offload+verify script + read-o  ·  back to Homesonspec  ·  TK-10809: offload VERIFY — no false PASS on a sample, tally aff87907 →