← 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
M apps/workers/src/snapshot-path.test.tsM apps/workers/src/snapshot-path.tsA ops/lib/snapshot-resolve.mjsM ops/verify-snapshots.mjsM scripts/backfill-drh-geo-from-cache.mjs
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 →