[object Object]

← back to Rebel Walls Push

TK-10029: one-command rollback executor (reads durable ledger, reverts canonical->old_value)

72b8edd7985aaf8dcebe504b5cea252a8ae1c54a · 2026-09-03 11:09:51 -0700 · Steve Abrams

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

Files touched

Diff

commit 72b8edd7985aaf8dcebe504b5cea252a8ae1c54a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 3 11:09:51 2026 -0700

    TK-10029: one-command rollback executor (reads durable ledger, reverts canonical->old_value)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01VA9ekkiMa6vAC2RaDP11zU
---
 scripts/rollback-relabel.js | 65 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 65 insertions(+)

diff --git a/scripts/rollback-relabel.js b/scripts/rollback-relabel.js
new file mode 100644
index 0000000..4322e77
--- /dev/null
+++ b/scripts/rollback-relabel.js
@@ -0,0 +1,65 @@
+#!/usr/bin/env node
+/**
+ * TK-10029 ROLLBACK: reverse the Roll->"Mural (per m²)" relabel using a durable
+ * pre-apply rollback ledger. For each recorded rename, renames the canonical option
+ * value BACK to its captured old_value. Dry-run by default; --apply to execute.
+ *
+ * Usage:
+ *   node scripts/rollback-relabel.js --ledger data/relabel-rollback-map-<ISO>.json
+ *   node scripts/rollback-relabel.js --ledger <file> --apply
+ */
+const https = require('https');
+const fs = require('fs');
+
+const SECRETS_ENV = '/Users/macstudio3/Projects/secrets-manager/.env';
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API_VERSION = '2024-10';
+
+function getToken() {
+  const env = fs.readFileSync(SECRETS_ENV, 'utf8');
+  const m = env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m);
+  if (!m) throw new Error('SHOPIFY_FULL_ACCESS_TOKEN not found');
+  return m[1].trim();
+}
+const TOKEN = getToken();
+const args = process.argv.slice(2);
+const DRY = !args.includes('--apply');
+const CANONICAL = 'Mural (per m²)';
+const li = args.indexOf('--ledger');
+if (li < 0) { console.error('need --ledger <file>'); process.exit(1); }
+const ledger = JSON.parse(fs.readFileSync(args[li + 1], 'utf8'));
+
+function gql(query, vars) {
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify({ query, variables: vars || {} });
+    const req = https.request({
+      hostname: DOMAIN, path: `/admin/api/${API_VERSION}/graphql.json`, method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
+    }, res => { let d=''; res.on('data',c=>d+=c); res.on('end',()=>{try{resolve(JSON.parse(d))}catch(e){reject(e)}}); });
+    req.on('error', reject); req.write(body); req.end();
+  });
+}
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+(async () => {
+  console.log(`[rollback] mode=${DRY?'DRY-RUN':'LIVE'} rows=${ledger.renames.length}`);
+  let done=0, skip=0, err=0;
+  for (const row of ledger.renames) {
+    const detailQ = `query { product(id: "${row.productId}") { options { id name optionValues { id name } } } }`;
+    const detail = await gql(detailQ);
+    const opt = detail.data?.product?.options?.find(o => o.id === row.optionId);
+    if (!opt) { console.log(`  SKIP ${row.title} — option gone`); skip++; continue; }
+    const ov = opt.optionValues.find(v => v.name === CANONICAL);
+    if (!ov) { console.log(`  SKIP ${row.title} — canonical not present (already reverted?)`); skip++; continue; }
+    console.log(`  REVERT ${row.title}: "${CANONICAL}" -> "${row.old_value}"`);
+    if (DRY) { done++; continue; }
+    const mutation = `mutation($productId: ID!, $option: OptionUpdateInput!, $ovs: [OptionValueUpdateInput!]!) {
+      productOptionUpdate(productId:$productId, option:$option, optionValuesToUpdate:$ovs) { userErrors { message } } }`;
+    const res = await gql(mutation, { productId: row.productId,
+      option: { id: row.optionId, name: row.optionName }, ovs: [{ id: ov.id, name: row.old_value }] });
+    const errs = res.data?.productOptionUpdate?.userErrors;
+    if (errs && errs.length) { console.log(`  ERR ${row.title}: ${errs.map(e=>e.message).join('; ')}`); err++; }
+    else { done++; await sleep(500); }
+  }
+  console.log(`[rollback] done=${done} skip=${skip} err=${err} dry=${DRY}`);
+})().catch(e => { console.error('[rollback] FATAL', e.message); process.exit(1); });

← 789f983 TK-10029: fresh complete pre-apply rollback ledger (1500 ren  ·  back to Rebel Walls Push  ·  TK-10029: durable scope-B rollback map (1220 rows) + generat dd06ad8 →