[object Object]

← back to Dw Sku Integrity

TK-10896: add post-apply verify mode + turnkey per-vendor firing RUNBOOK

e9416141e5a98b607fe8aad6a1713ac0e09c945d · 2026-08-31 00:27:17 -0700 · codex-10896

preflight-check.mjs gains --mode verify (after firing: GO iff every planned row now
equals its candidate; still-blank rows flag NOT_APPLIED). RUNBOOK.md documents the
full gated closed loop preflight->apply->verify->rollback, per-vendor, canary-first,
with the scope/safety reminders. 47 tests green. All read-only; writes stay Steve-gated.

Files touched

Diff

commit e9416141e5a98b607fe8aad6a1713ac0e09c945d
Author: codex-10896 <steve@designerwallcoverings.com>
Date:   Mon Aug 31 00:27:17 2026 -0700

    TK-10896: add post-apply verify mode + turnkey per-vendor firing RUNBOOK
    
    preflight-check.mjs gains --mode verify (after firing: GO iff every planned row now
    equals its candidate; still-blank rows flag NOT_APPLIED). RUNBOOK.md documents the
    full gated closed loop preflight->apply->verify->rollback, per-vendor, canary-first,
    with the scope/safety reminders. 47 tests green. All read-only; writes stay Steve-gated.
---
 RUNBOOK.md                    | 67 +++++++++++++++++++++++++++++++++++++++++++
 preflight-check.mjs           | 32 +++++++++++++++------
 test/preflight-check.test.mjs | 18 ++++++++++++
 3 files changed, 109 insertions(+), 8 deletions(-)

diff --git a/RUNBOOK.md b/RUNBOOK.md
new file mode 100644
index 0000000..f01a516
--- /dev/null
+++ b/RUNBOOK.md
@@ -0,0 +1,67 @@
+# TK-10896 — gated dw_sku self-copy firing runbook
+
+Turnkey, per-vendor execution of the ledger-verified self-copy plan. Every write
+here is **canonical Kamatera dw_unified = HARD-GATED**: Steve runs each step; agents
+never fire it. Recovers an existing code (no mint). Full closed loop:
+**preflight (read-only) → apply (gated write) → verify (read-only) → rollback if needed.**
+
+## Prerequisites (Steve, once)
+1. **Confirm the ledger's chain of custody.** The plan's provenance rests on
+   `/tmp/TK-10896-phase4-ledger.jsonl` being the authorized export of Kamatera's
+   `sku_repair_p4_20260826` undo table. Integrity is verified (SHA-256 + row-count +
+   internal consistency); confirm it's the real export before proceeding.
+2. **Set the canonical target** for the read-only checks (same wrapper the scanner uses):
+   ```bash
+   export DWSKU_PSQL='ssh <kamatera-host> psql'      # points read-only checks at Kamatera
+   cd ~/Projects/dw-sku-integrity
+   ```
+3. **(Optional) regenerate the plan on canonical.** The committed apply-plans were built
+   from the Mac2 mirror. shopify_id is stable cross-machine, so they port directly — but
+   to be exact you may re-run the scan+join+gen against Kamatera first.
+
+## Per-vendor cycle (start with the smallest vendor as a canary)
+Order by size ascending; `apply-plans/SUMMARY.json` → `per_vendor` has the counts.
+
+```bash
+V=<vendor-slug>      # e.g. a-s-cr-ation (small) before carnegie (10,707)
+
+# 1. PREFLIGHT (read-only) — must print "verdict": "GO"
+node preflight-check.mjs --vendor "$V" --out /tmp/pf-$V.json
+#    GO   = every row exists + still blank on Kamatera -> safe to apply
+#    NO_GO= inspect flags: MISSING (row not on target / drift) or
+#           ALREADY_OTHER (row holds a different code — do NOT overwrite). STOP, investigate.
+
+# 2. APPLY (GATED WRITE — Steve fires). Every UPDATE is blank-guarded + shopify_id-keyed.
+ssh <kamatera-host> "psql dw_unified" < apply-plans/$V/apply.sql
+#    (or run apply.sql however Steve normally applies a canonical migration)
+
+# 3. VERIFY (read-only) — must print "verdict": "GO"
+node preflight-check.mjs --vendor "$V" --mode verify --out /tmp/vf-$V.json
+#    GO   = every planned row now equals its candidate (applied cleanly)
+#    NO_GO= NOT_APPLIED rows (write didn't land) or ALREADY_OTHER — inspect + reconcile.
+
+# 4. ROLLBACK (only if a problem is found) — reverses exactly what apply set.
+ssh <kamatera-host> "psql dw_unified" < apply-plans/$V/undo.sql
+#    undo.sql sets dw_sku=NULL WHERE shopify_id=... AND dw_sku='<candidate>' (targeted).
+#    restore-map.json is the human-readable record of every (shopify_id -> candidate).
+```
+
+## Scope reminder (what this plan does / does NOT touch)
+- **Applies (self-copy, 33,762 rows / ~38,712 statements across 93 vendors):** recovers the
+  existing DW/source/Cork code already sitting in each row's `sku`. No mint.
+- **Does NOT apply — routed elsewhere:**
+  - **~837 rescrape** (Knoll 492 + Hollywood 266 confirmed reverted-mint residue, Pixels 52,
+    Steve Abrams 13, Jeffrey Stevens 12, PR 2, + 65 truly bare) → the re-scrape program
+    (TK-10900/10901), owned by each vendor's scraper-manager. NEVER self-copied (would re-mint).
+  - **9 collisions** → dedup (TK-10649).
+  - **2 shape-rejected** garbage candidates (a leaked title + a "Sold Per Bolt" suffix) →
+    manual/rescrape, excluded from apply.
+
+## Safety properties (independently verified)
+- Every apply UPDATE keyed on `shopify_id` (stable cross-machine), NOT the local serial `id`.
+- Every apply UPDATE carries the blank-guard `(dw_sku IS NULL OR btrim(dw_sku)='')` — idempotent.
+- 0 reverted-mint ledger codes appear in any apply.sql (verified across all assigned codes).
+- undo.sql reverses apply.sql exactly; restore-map.json records every mapping.
+- 45 tests green. `preflight-check.mjs` is SELECT-only.
+
+Parent ticket: **TK-10896**.
diff --git a/preflight-check.mjs b/preflight-check.mjs
index 4d9d50a..f8b1722 100644
--- a/preflight-check.mjs
+++ b/preflight-check.mjs
@@ -54,20 +54,32 @@ function defaultChunkReader(shopifyIds) {
 
 // Classify planned targets against a resolver (shopify_id -> dw_sku on target, or
 // undefined if absent). Pure — testable without a DB.
-export function classifyTargets(planned, targetLookup) {
+//   mode 'preflight' (BEFORE firing): GO iff every row exists + is still blank
+//     (READY) or already equals its candidate (ALREADY_TARGET, idempotent). A row
+//     holding a DIFFERENT code (ALREADY_OTHER) or absent (MISSING) blocks.
+//   mode 'verify' (AFTER firing): GO iff every row now equals its candidate
+//     (ALREADY_TARGET). A still-blank row (READY) means the apply didn't land;
+//     ALREADY_OTHER / MISSING also block.
+export function classifyTargets(planned, targetLookup, mode = 'preflight') {
   const stats = { total: planned.length, READY: 0, ALREADY_TARGET: 0, ALREADY_OTHER: 0, MISSING: 0 };
   const flags = [];
   for (const p of planned) {
     const dw = targetLookup(p.shopify_id);
     if (dw === undefined) { stats.MISSING += 1; flags.push({ ...p, status: 'MISSING' }); continue; }
     const cur = String(dw || '').trim();
-    if (cur === '') { stats.READY += 1; continue; }
+    if (cur === '') {
+      stats.READY += 1;
+      if (mode === 'verify') flags.push({ ...p, status: 'NOT_APPLIED' }); // should be set post-apply
+      continue;
+    }
     if (cur === String(p.candidate)) { stats.ALREADY_TARGET += 1; continue; }
     stats.ALREADY_OTHER += 1;
     flags.push({ ...p, status: 'ALREADY_OTHER', current_dw_sku: cur });
   }
-  const go = stats.MISSING === 0 && stats.ALREADY_OTHER === 0;
-  return { stats, flags, go };
+  const go = mode === 'verify'
+    ? (stats.ALREADY_TARGET === stats.total)
+    : (stats.MISSING === 0 && stats.ALREADY_OTHER === 0);
+  return { stats, flags, go, mode };
 }
 
 // Load planned targets from apply-plans/<vendor>/restore-map.json.
@@ -94,19 +106,23 @@ if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).
   const planDir = arg('--plan-dir', join(HERE, 'apply-plans'));
   const vendor = arg('--vendor', null);
   const outPath = arg('--out', null);
+  const mode = arg('--mode', 'preflight'); // 'preflight' (before) | 'verify' (after)
+  if (mode !== 'preflight' && mode !== 'verify') { console.error(`bad --mode: ${mode}`); process.exit(3); }
   const planned = loadPlanned(planDir, vendor);
-  console.error(`[preflight] target: ${PSQL.join(' ')} | planned rows: ${planned.length}`);
+  console.error(`[${mode}] target: ${PSQL.join(' ')} | planned rows: ${planned.length}`);
 
   // Resolve all target dw_sku values in chunks (READ-ONLY).
   const found = new Map();
   const ids = [...new Set(planned.map((p) => p.shopify_id))];
   for (const c of chunk(ids, 500)) for (const [k, v] of readTargetChunk(c)) found.set(k, v);
 
-  const result = classifyTargets(planned, (sid) => (found.has(sid) ? found.get(sid) : undefined));
+  const result = classifyTargets(planned, (sid) => (found.has(sid) ? found.get(sid) : undefined), mode);
   const report = {
-    ticket: 'TK-10896', target: PSQL.join(' '), scope: vendor || 'ALL',
+    ticket: 'TK-10896', mode, target: PSQL.join(' '), scope: vendor || 'ALL',
     verdict: result.go ? 'GO' : 'NO_GO',
-    note: 'READ-ONLY preflight. GO = every planned row exists + is still blank on target. NO_GO = MISSING or ALREADY_OTHER present (inspect flags). Nothing was written.',
+    note: mode === 'verify'
+      ? 'READ-ONLY post-apply verify. GO = every planned row now equals its candidate. NO_GO = NOT_APPLIED / ALREADY_OTHER / MISSING present. Nothing was written.'
+      : 'READ-ONLY preflight. GO = every planned row exists + is still blank (or already its candidate) on target. NO_GO = MISSING or ALREADY_OTHER present. Nothing was written.',
     ...result.stats, flags_sample: result.flags.slice(0, 50), flag_count: result.flags.length,
   };
   console.log(JSON.stringify(report, null, 2));
diff --git a/test/preflight-check.test.mjs b/test/preflight-check.test.mjs
index 88ce753..c131ddb 100644
--- a/test/preflight-check.test.mjs
+++ b/test/preflight-check.test.mjs
@@ -55,3 +55,21 @@ test('whitespace-only target dw_sku counts as blank/READY', () => {
   assert.equal(r.stats.READY, 1);
   assert.equal(r.go, true);
 });
+
+test('verify mode: GO only when every row now equals its candidate', () => {
+  const rows = [{ shopify_id: 'a', candidate: 'DWX-1' }, { shopify_id: 'b', candidate: 'DWX-2' }];
+  const applied = new Map([['a', 'DWX-1'], ['b', 'DWX-2']]);
+  const r = classifyTargets(rows, (s) => (applied.has(s) ? applied.get(s) : undefined), 'verify');
+  assert.equal(r.go, true);
+  assert.equal(r.stats.ALREADY_TARGET, 2);
+  assert.equal(r.flags.length, 0);
+});
+
+test('verify mode: a still-blank row is flagged NOT_APPLIED and blocks GO', () => {
+  const rows = [{ shopify_id: 'a', candidate: 'DWX-1' }, { shopify_id: 'b', candidate: 'DWX-2' }];
+  const applied = new Map([['a', 'DWX-1'], ['b', '']]); // b never got written
+  const r = classifyTargets(rows, (s) => (applied.has(s) ? applied.get(s) : undefined), 'verify');
+  assert.equal(r.go, false);
+  assert.equal(r.stats.READY, 1);
+  assert.equal(r.flags[0].status, 'NOT_APPLIED');
+});

← 600aa83 TK-10896: read-only preflight GO/NO-GO checker for the gated  ·  back to Dw Sku Integrity  ·  TK-10896: fix remote-shell quoting — feed SQL via stdin not cab2766 →