← back to Dw Sku Integrity
TK-11002: Harlequin DWHQ-->DWHF- prefix re-stamp (Option A) — dry-run-verified, reversible, CAS-guarded; live --apply is Steve-gated
289793bdd91ff5455d9be597eb06976eced6a2a1 · 2026-08-31 09:48:50 -0700 · steve
Files touched
A tk11002-harlequin-prefix-fix.mjs
Diff
commit 289793bdd91ff5455d9be597eb06976eced6a2a1
Author: steve <steve@designerwallcoverings.com>
Date: Mon Aug 31 09:48:50 2026 -0700
TK-11002: Harlequin DWHQ-->DWHF- prefix re-stamp (Option A) — dry-run-verified, reversible, CAS-guarded; live --apply is Steve-gated
---
tk11002-harlequin-prefix-fix.mjs | 128 +++++++++++++++++++++++++++++++++++++++
1 file changed, 128 insertions(+)
diff --git a/tk11002-harlequin-prefix-fix.mjs b/tk11002-harlequin-prefix-fix.mjs
new file mode 100644
index 0000000..647701b
--- /dev/null
+++ b/tk11002-harlequin-prefix-fix.mjs
@@ -0,0 +1,128 @@
+#!/usr/bin/env node
+// TK-11002 (parent TK-10979) — Harlequin DWHQ- -> DWHF- prefix re-stamp (Steve decision: Option A, 2026-08-31)
+// Fixes dw-prefix-integrity-canary FAIL: 39 ACTIVE Harlequin products wear DWHQ- (reserved to Alpha Work Shops,
+// 0 products). Re-stamp keeps the numeric tail (keep-old-never-mint) and the -Sample suffix; only the vendor prefix
+// changes DWHQ- -> DWHF- (Harlequin's own registered prefix).
+//
+// SAFETY / R4:
+// * DRY-RUN by default. Live writes ONLY with --apply (customer-facing LIVE store => Steve-gated `!` run).
+// * Fetches LIVE variants per product (mirror variant_id is stale) and CAS-verifies current sku starts DWHQ- before writing.
+// * Writes a restore-map JSON BEFORE any live write; undo = re-run with --rollback <map.json>.
+// * Order: Shopify (authoritative, customer-facing) THEN local mirror (follows). Never mirror-first (split-brain).
+// * After apply: re-runs the prefix-integrity canary; expects Harlequin/DWHQ- to clear.
+// Usage:
+// node tk11002-harlequin-prefix-fix.mjs # dry-run: show 78 variant diffs, write restore-map, no writes
+// node tk11002-harlequin-prefix-fix.mjs --apply # LIVE (Steve `!` only)
+// node tk11002-harlequin-prefix-fix.mjs --rollback data/tk11002-restore-<ts>.json # undo
+
+import { execSync } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const APPLY = process.argv.includes('--apply');
+const ROLLBACK = (() => { const i = process.argv.indexOf('--rollback'); return i > -1 ? process.argv[i + 1] : null; })();
+const DATADIR = path.join(process.cwd(), 'data');
+fs.mkdirSync(DATADIR, { recursive: true });
+
+function token() {
+ const env = fs.readFileSync(`${process.env.HOME}/Projects/secrets-manager/.env`, 'utf8');
+ const m = env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m);
+ if (!m) throw new Error('SHOPIFY_ADMIN_TOKEN not found');
+ return m[1].trim();
+}
+const TOKEN = token();
+
+async function gql(query, variables) {
+ const r = await fetch(`https://${SHOP}/admin/api/${API}/graphql.json`, {
+ method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query, variables }),
+ });
+ const j = await r.json();
+ if (j.errors) throw new Error('GraphQL: ' + JSON.stringify(j.errors));
+ return j.data;
+}
+
+function psql(sql) {
+ return execSync(`psql -h /tmp -d dw_unified -tAc ${JSON.stringify(sql)}`, { encoding: 'utf8' }).trim();
+}
+
+function targetSet() {
+ // Frozen from the mirror: 39 ACTIVE Harlequin products whose main sku wears DWHQ-.
+ const rows = psql(
+ "select id||chr(9)||shopify_id||chr(9)||coalesce(sku,'')||chr(9)||coalesce(variant_sku,'')||chr(9)||coalesce(dw_sku,'') " +
+ "from shopify_products where vendor='Harlequin' and sku like 'DWHQ-%' and status='ACTIVE' order by sku"
+ );
+ return rows.split('\n').filter(Boolean).map(l => {
+ const [id, shopify_id, sku, variant_sku, dw_sku] = l.split('\t');
+ return { id, shopify_id, mirror_sku: sku, mirror_variant_sku: variant_sku, mirror_dw_sku: dw_sku };
+ });
+}
+
+const restamp = s => (s && s.startsWith('DWHQ-')) ? s.replace(/^DWHQ-/, 'DWHF-') : s;
+
+async function runForward() {
+ const targets = targetSet();
+ console.log(`Target: ${targets.length} ACTIVE Harlequin products (DWHQ- -> DWHF-), fetching LIVE variants...`);
+ const plan = [];
+ for (const t of targets) {
+ const d = await gql(`query($id:ID!){ product(id:$id){ id title variants(first:20){ nodes{ id sku } } } }`, { id: t.shopify_id });
+ if (!d.product) { console.log(` !! ${t.shopify_id} not found live — SKIP`); continue; }
+ for (const v of d.product.variants.nodes) {
+ if (v.sku && v.sku.startsWith('DWHQ-')) {
+ plan.push({ product: t.shopify_id, title: d.product.title, variant: v.id, old_sku: v.sku, new_sku: restamp(v.sku),
+ mirror_id: t.id, mirror_sku: t.mirror_sku, mirror_variant_sku: t.mirror_variant_sku, mirror_dw_sku: t.mirror_dw_sku });
+ }
+ }
+ }
+ const ts = psql("select to_char(now(),'YYYYMMDD-HH24MISS')");
+ const mapPath = path.join(DATADIR, `tk11002-restore-${ts}.json`);
+ fs.writeFileSync(mapPath, JSON.stringify(plan, null, 2));
+ console.log(`\n${plan.length} variant writes across ${new Set(plan.map(p => p.product)).size} products. Restore-map: ${mapPath}`);
+ for (const p of plan.slice(0, 6)) console.log(` ${p.old_sku} -> ${p.new_sku} (${p.title})`);
+ if (plan.length > 6) console.log(` … +${plan.length - 6} more`);
+
+ if (!APPLY) { console.log('\nDRY-RUN — no writes. Re-run with --apply (Steve `!`) to execute.'); return; }
+
+ console.log('\n--apply: writing LIVE (Shopify first, then mirror, CAS-guarded)...');
+ let ok = 0, skip = 0;
+ for (const p of plan) {
+ // CAS: re-read this variant live; abort this variant if it drifted off DWHQ-
+ const chk = await gql(`query($id:ID!){ productVariant(id:$id){ id sku } }`, { id: p.variant });
+ if (!chk.productVariant || chk.productVariant.sku !== p.old_sku) { console.log(` CAS-drift SKIP ${p.old_sku}`); skip++; continue; }
+ const res = await gql(
+ `mutation($input:ProductVariantInput!){ productVariantUpdate(input:$input){ productVariant{ id sku } userErrors{ field message } } }`,
+ { input: { id: p.variant, sku: p.new_sku } }
+ );
+ const ue = res.productVariantUpdate.userErrors;
+ if (ue.length) { console.log(` ERROR ${p.old_sku}: ${JSON.stringify(ue)}`); skip++; continue; }
+ // mirror follows (CAS on old value)
+ psql(`update shopify_products set sku=case when sku=${q(p.mirror_sku)} then ${q(restamp(p.mirror_sku))} else sku end, ` +
+ `variant_sku=case when variant_sku like 'DWHQ-%' then replace(variant_sku,'DWHQ-','DWHF-') else variant_sku end, ` +
+ `dw_sku=case when dw_sku like 'DWHQ-%' then replace(dw_sku,'DWHQ-','DWHF-') else dw_sku end ` +
+ `where id=${p.mirror_id}`);
+ ok++;
+ }
+ console.log(`\nDone: ${ok} written, ${skip} skipped.`);
+ console.log('\nPost-check — re-running prefix-integrity canary:');
+ try { console.log(execSync('python3 ~/.claude/skills/dw-prefix-integrity-canary/check.py', { encoding: 'utf8' })); }
+ catch (e) { console.log('(canary run:', e.message, ')'); }
+}
+
+function q(s) { return `'${String(s).replace(/'/g, "''")}'`; }
+
+async function runRollback(mapPath) {
+ const plan = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
+ console.log(`ROLLBACK ${plan.length} variants from ${mapPath}`);
+ if (!APPLY) { console.log('DRY-RUN rollback — re-run with --apply to revert.'); return; }
+ for (const p of plan) {
+ await gql(`mutation($input:ProductVariantInput!){ productVariantUpdate(input:$input){ userErrors{ message } } }`,
+ { input: { id: p.variant, sku: p.old_sku } });
+ psql(`update shopify_products set sku=${q(p.mirror_sku)}, variant_sku=${q(p.mirror_variant_sku)}, dw_sku=${q(p.mirror_dw_sku)} where id=${p.mirror_id}`);
+ }
+ console.log('Rollback complete.');
+}
+
+(ROLLBACK ? runRollback(ROLLBACK) : runForward()).catch(e => { console.error('FATAL', e); process.exit(1); });
← 0d133ca TK-10900: contrarian-2 fix — guard title-mode cross-class ho
·
back to Dw Sku Integrity
·
TK-11002: fix live write — productVariantUpdate removed in A 233fb7f →