[object Object]

← back to Designer Wallcoverings

fix(versa-20oz): make dwc-sync rollback a true inverse (delete absent metafields)

c1d89e49ffc936b802be4d0ebc4d9ab122ac71e4 · 2026-09-14 14:22:51 -0700 · Steve

Code-review finding C2: on rollback, entries whose dwc.dw_sku was ABSENT before
apply were skipped (`continue`), leaving the XZW value written — so rollback was
not a true inverse. Now metafieldsDelete removes the metafield apply created for
those entries. Committed standalone (per /dtd verdict): this is a new UNTRACKED
file, so leaving it in the working tree risked silent loss to `git clean -fd`.
A3 (lib/shopify.ts fail-closed guard) intentionally left in the working tree —
committing it would bundle 238 lines of the branch owner's gate-wiring WIP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U39xV6JiBLLjXBYbfbSxgw

Files touched

Diff

commit c1d89e49ffc936b802be4d0ebc4d9ab122ac71e4
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Sep 14 14:22:51 2026 -0700

    fix(versa-20oz): make dwc-sync rollback a true inverse (delete absent metafields)
    
    Code-review finding C2: on rollback, entries whose dwc.dw_sku was ABSENT before
    apply were skipped (`continue`), leaving the XZW value written — so rollback was
    not a true inverse. Now metafieldsDelete removes the metafield apply created for
    those entries. Committed standalone (per /dtd verdict): this is a new UNTRACKED
    file, so leaving it in the working tree risked silent loss to `git clean -fd`.
    A3 (lib/shopify.ts fail-closed guard) intentionally left in the working tree —
    committing it would bundle 238 lines of the branch owner's gate-wiring WIP.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01U39xV6JiBLLjXBYbfbSxgw
---
 scripts/versa-20oz-hollywood-fix/dwc-sync.mjs | 74 +++++++++++++++++++++++++++
 1 file changed, 74 insertions(+)

diff --git a/scripts/versa-20oz-hollywood-fix/dwc-sync.mjs b/scripts/versa-20oz-hollywood-fix/dwc-sync.mjs
new file mode 100755
index 00000000..da3cd35e
--- /dev/null
+++ b/scripts/versa-20oz-hollywood-fix/dwc-sync.mjs
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+/* TK-11337 follow-up — sync dwc.dw_sku to the XZW DW-series code for the 919 Versa 20oz products.
+ * global.dw_sku is already XZW; the legacy dwc.dw_sku still holds the old raw mfr. Grep confirmed the
+ * only code touching dwc.dw_sku WRITES it = the DW sku, so this corrects a deviation (no reader breaks).
+ * REVERSIBLE: reads each product's current dwc.dw_sku into data/dwc-sync-undo.jsonl BEFORE writing.
+ * GATED: requires CONFIRM_DWC_SYNC=TK-11337-STEVE-APPROVED. Steve runs via !.
+ *   node dwc-sync.mjs apply | rollback
+ */
+import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url';
+const DIR = path.dirname(fileURLToPath(import.meta.url));
+const RESTORE = path.join(DIR, 'data', 'restore-map.jsonl');
+const UNDO = path.join(DIR, 'data', 'dwc-sync-undo.jsonl');
+const LEDGER = path.join(process.env.HOME, '.claude/yolo-queue/executed-reversible/ledger.jsonl');
+const ENV = fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8');
+const tok = (k) => (ENV.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.trim();
+const TOKEN = tok('SHOPIFY_FULL_ACCESS_TOKEN') || tok('SHOPIFY_ADMIN_TOKEN');
+const API = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+async function gql(q, v, tries = 6) {
+  for (let i = 0; i < tries; i++) {
+    let r; try { r = await fetch(API, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) }); }
+    catch { await sleep(1500 * (i + 1)); continue; }
+    if ([429, 502, 503].includes(r.status)) { await sleep(2000 * (i + 1)); continue; }
+    const j = await r.json(); if (j.errors) throw new Error(JSON.stringify(j.errors)); return j.data;
+  }
+  throw new Error('gql retries exhausted');
+}
+const Q = `query($id:ID!){ product(id:$id){ dwc: metafield(namespace:"dwc",key:"dw_sku"){ value } } }`;
+const M = `mutation($mf:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$mf){ userErrors{field message} } }`;
+const MDEL = `mutation($mf:[MetafieldIdentifierInput!]!){ metafieldsDelete(metafields:$mf){ deletedMetafields{ key } userErrors{ field message } } }`;
+function rows() { return fs.readFileSync(RESTORE, 'utf8').trim().split('\n').map(JSON.parse).filter((p) => !p.error && p.new && p.new.dw_sku_mf); }
+async function apply() {
+  if (process.env.CONFIRM_DWC_SYNC !== 'TK-11337-STEVE-APPROVED') throw new Error('GATED: set CONFIRM_DWC_SYNC=TK-11337-STEVE-APPROVED');
+  const rs = rows(); const undo = fs.createWriteStream(UNDO, { flags: 'w' });
+  let n = 0, fails = 0;
+  for (const p of rs) {
+    const gid = p.gid || `gid://shopify/Product/${p.pid}`;
+    try {
+      const cur = (await gql(Q, { id: gid })).product;                       // capture BEFORE write
+      undo.write(JSON.stringify({ pid: p.pid, gid, old_dwc_dw_sku: cur?.dwc?.value ?? null, new: p.new.dw_sku_mf }) + '\n');
+      const r = await gql(M, { mf: [{ ownerId: gid, namespace: 'dwc', key: 'dw_sku', value: p.new.dw_sku_mf, type: 'single_line_text_field' }] });
+      const e = r.metafieldsSet.userErrors; if (e.length) throw new Error(JSON.stringify(e));
+      fails = 0; if (++n % 25 === 0) { process.stderr.write(`synced ${n}/${rs.length}\n`); await sleep(800); }
+    } catch (e) {
+      process.stderr.write(`FAIL ${p.pid}: ${e.message}\n`); fails++;
+      if (n === 0 && fails >= 3) { undo.end(); throw new Error(`ABORT: first ${fails} failed (systemic) — last: ${e.message}`); }
+    }
+  }
+  undo.end();
+  fs.appendFileSync(LEDGER, JSON.stringify({ ts: new Date().toISOString(), agent: 'vp-dw-commerce', ticket: 'TK-11337',
+    action: `dwc.dw_sku synced to XZW for ${n} products`, blast_radius: n,
+    undo_cmd: `node ${path.relative(process.env.HOME, path.join(DIR, 'dwc-sync.mjs'))} rollback`, verify: 'spot-check dwc.dw_sku == global.dw_sku' }) + '\n');
+  process.stderr.write(`SYNCED ${n}/${rs.length}; undo -> ${UNDO}\n`);
+}
+async function rollback() {
+  if (!fs.existsSync(UNDO)) throw new Error('no undo file');
+  const us = fs.readFileSync(UNDO, 'utf8').trim().split('\n').map(JSON.parse); let n = 0;
+  for (const u of us) {
+    const val = u.old_dwc_dw_sku;
+    if (val === null) {
+      // was ABSENT before apply → the true inverse is to DELETE the metafield apply
+      // created, not leave the XZW value in place (review 2026-09-14, makes rollback exact).
+      const r = await gql(MDEL, { mf: [{ ownerId: u.gid, namespace: 'dwc', key: 'dw_sku' }] });
+      const e = r.metafieldsDelete.userErrors; if (e.length) throw new Error(JSON.stringify(e));
+      n++; continue;
+    }
+    await gql(M, { mf: [{ ownerId: u.gid, namespace: 'dwc', key: 'dw_sku', value: val, type: 'single_line_text_field' }] }); n++;
+  }
+  process.stderr.write(`ROLLED BACK ${n}/${us.length} (restored old dwc.dw_sku)\n`);
+}
+const MODE = process.argv[2];
+if (MODE === 'apply') apply();
+else if (MODE === 'rollback') rollback();
+else console.error('usage: dwc-sync.mjs apply|rollback');

← 35260255 Revert "fix(TK-11738): pass --apply so the sample-variant cr  ·  back to Designer Wallcoverings  ·  fix(weight-guard): use real product_type on import so fabric 0c4f5771 →