← 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
A RUNBOOK.mdM preflight-check.mjsM test/preflight-check.test.mjs
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 →