[object Object]

← back to Rebel Walls Push

TK-10405 full-fix EXECUTED: reordered Mural(per m²)->pos1 + Sample->pos2, DELETED the 2 Default-Title orphans on dwrw-76016 + dwrw-76264

359cbbf8992d6b031f0df6a4b1a24d567712c3bd · 2026-09-04 23:59:00 -0700 · Steve Abrams

Supersedes the A' mitigation. End state verified live (Admin API + storefront):
both PDPs now read offers[0]=$117.47, Default-Title absent. Reversible via
scripts/TK-10405-fullfix-undo.mjs (recreates orphan from captured pre-state).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 359cbbf8992d6b031f0df6a4b1a24d567712c3bd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 4 23:59:00 2026 -0700

    TK-10405 full-fix EXECUTED: reordered Mural(per m²)->pos1 + Sample->pos2, DELETED the 2 Default-Title orphans on dwrw-76016 + dwrw-76264
    
    Supersedes the A' mitigation. End state verified live (Admin API + storefront):
    both PDPs now read offers[0]=$117.47, Default-Title absent. Reversible via
    scripts/TK-10405-fullfix-undo.mjs (recreates orphan from captured pre-state).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 data/TK-10405-restore-map.json    | 106 +++++++++++++++++++++++++++++
 scripts/TK-10405-fullfix-undo.mjs |  67 ++++++++++++++++++
 scripts/TK-10405-fullfix.mjs      | 138 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 311 insertions(+)

diff --git a/data/TK-10405-restore-map.json b/data/TK-10405-restore-map.json
index c70c9b9..453311e 100644
--- a/data/TK-10405-restore-map.json
+++ b/data/TK-10405-restore-map.json
@@ -210,5 +210,111 @@
         "tracked": true
       }
     }
+  },
+  "full_fix_reorder_delete": {
+    "ts": "2026-09-05T06:57:58.317Z",
+    "approved_by": "Steve in-session TK-10405",
+    "action": "FULL FIX: reorder Mural(per m²)->1, Sample->2, orphan->3, then DELETE orphan",
+    "undo": "node scripts/TK-10405-fullfix-undo.mjs — recreates orphan (NEW id) via productVariantsBulkCreate from prestate + restores original positions",
+    "products": {
+      "6679729078323": {
+        "product_gid": "gid://shopify/Product/6679729078323",
+        "handle": "dwrw-76016",
+        "label": "A City Rises",
+        "options": [
+          {
+            "name": "Size",
+            "position": 1,
+            "values": [
+              "Default Title",
+              "Mural (per m²)",
+              "Sample"
+            ]
+          }
+        ],
+        "original_positions": [
+          {
+            "id": "gid://shopify/ProductVariant/44453186830387",
+            "title": "Default Title",
+            "position": 1
+          },
+          {
+            "id": "gid://shopify/ProductVariant/44486915063859",
+            "title": "Mural (per m²)",
+            "position": 2
+          },
+          {
+            "id": "gid://shopify/ProductVariant/40347630403635",
+            "title": "Sample",
+            "position": 3
+          }
+        ],
+        "orphan_recreate": {
+          "id": "gid://shopify/ProductVariant/44453186830387",
+          "title": "Default Title",
+          "price": "80.48",
+          "sku": "DWRW-76016-Sample-DUP",
+          "taxable": true,
+          "inventoryPolicy": "DENY",
+          "inventoryQuantity": 0,
+          "selectedOptions": [
+            {
+              "name": "Size",
+              "value": "Default Title"
+            }
+          ],
+          "tracked": true
+        }
+      },
+      "6679739629619": {
+        "product_gid": "gid://shopify/Product/6679739629619",
+        "handle": "dwrw-76264",
+        "label": "A Priori",
+        "options": [
+          {
+            "name": "Size",
+            "position": 1,
+            "values": [
+              "Default Title",
+              "Mural (per m²)",
+              "Sample"
+            ]
+          }
+        ],
+        "original_positions": [
+          {
+            "id": "gid://shopify/ProductVariant/44453188206643",
+            "title": "Default Title",
+            "position": 1
+          },
+          {
+            "id": "gid://shopify/ProductVariant/44490271719475",
+            "title": "Mural (per m²)",
+            "position": 2
+          },
+          {
+            "id": "gid://shopify/ProductVariant/40347693940787",
+            "title": "Sample",
+            "position": 3
+          }
+        ],
+        "orphan_recreate": {
+          "id": "gid://shopify/ProductVariant/44453188206643",
+          "title": "Default Title",
+          "price": "4.25",
+          "sku": "DWRW-76264-Sample-DUP",
+          "taxable": true,
+          "inventoryPolicy": "DENY",
+          "inventoryQuantity": 0,
+          "selectedOptions": [
+            {
+              "name": "Size",
+              "value": "Default Title"
+            }
+          ],
+          "tracked": true
+        }
+      }
+    }
   }
 }
\ No newline at end of file
diff --git a/scripts/TK-10405-fullfix-undo.mjs b/scripts/TK-10405-fullfix-undo.mjs
new file mode 100644
index 0000000..3f79527
--- /dev/null
+++ b/scripts/TK-10405-fullfix-undo.mjs
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+// UNDO for TK-10405-fullfix.mjs. Recreates each deleted Default-Title orphan (NEW variant id)
+// from the restore map's full_fix_reorder_delete.orphan_recreate payload, restores its inventory
+// qty, and reorders variants back to their original positions. A recreated variant gets a NEW id
+// (documented old->new in output). Run:  node scripts/TK-10405-fullfix-undo.mjs
+import fs from 'node:fs';
+
+const DOM = 'designer-laboratory-sandbox.myshopify.com';
+const VER = '2024-10';
+const RESTORE = new URL('../data/TK-10405-restore-map.json', import.meta.url).pathname;
+const env = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)?.[1] || '').trim().replace(/^["']|["']$/g, '');
+if (!TOKEN) { console.error('no SHOPIFY_FULL_ACCESS_TOKEN'); process.exit(1); }
+
+async function gql(query, variables, tries = 6) {
+  for (let i = 0; i < tries; i++) {
+    const r = await fetch(`https://${DOM}/admin/api/${VER}/graphql.json`, {
+      method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+      body: JSON.stringify({ query, variables }) });
+    if ([429, 500, 502, 503].includes(r.status) && i < tries - 1) { await new Promise(s => setTimeout(s, 1500 * (i + 1))); continue; }
+    return r.json();
+  }
+}
+const Q_PROD = `query($id:ID!){ product(id:$id){ variants(first:20){ nodes{ id title inventoryItem{ id } } } } }`;
+const M_CREATE = `mutation($productId:ID!,$variants:[ProductVariantsBulkInput!]!){
+  productVariantsBulkCreate(productId:$productId,variants:$variants){ productVariants{ id title inventoryItem{ id } } userErrors{ field message } } }`;
+const M_REORDER = `mutation($productId:ID!,$positions:[ProductVariantPositionInput!]!){
+  productVariantsBulkReorder(productId:$productId,positions:$positions){ product{ id } userErrors{ field message } } }`;
+const M_INV = `mutation($input:InventorySetQuantitiesInput!){
+  inventorySetQuantities(input:$input){ inventoryAdjustmentGroup{ createdAt } userErrors{ field message } } }`;
+const short = g => g.split('/').pop();
+
+(async () => {
+  const rm = JSON.parse(fs.readFileSync(RESTORE, 'utf8'));
+  const ff = rm.full_fix_reorder_delete;
+  if (!ff) { console.error('no full_fix_reorder_delete block in restore map — nothing to undo'); process.exit(1); }
+  for (const pid of Object.keys(ff.products)) {
+    const rec = ff.products[pid];
+    const productId = rec.product_gid;
+    const o = rec.orphan_recreate;
+    if (!o) { console.log(`${rec.label}: no orphan payload captured — skip`); continue; }
+    console.log(`\n=== UNDO ${rec.label} (${pid}) ===`);
+    // 1. recreate orphan
+    const input = { price: o.price, taxable: o.taxable, inventoryPolicy: o.inventoryPolicy,
+      inventoryItem: { sku: o.sku, tracked: o.tracked },
+      optionValues: (o.selectedOptions || []).map(so => ({ name: so.value, optionName: so.name })) };
+    const rc = await gql(M_CREATE, { productId, variants: [input] });
+    const dc = rc?.data?.productVariantsBulkCreate;
+    if (dc?.userErrors?.length || rc?.errors) { console.error('  RECREATE FAILED:', JSON.stringify(dc?.userErrors || rc.errors)); continue; }
+    const newVid = dc.productVariants[0].id;
+    const newInv = dc.productVariants[0].inventoryItem?.id;
+    console.log(`  recreated orphan: OLD ${short(o.id)} -> NEW ${short(newVid)}`);
+    // 2. restore inventory qty
+    if (o.tracked && newInv && typeof o.inventoryQuantity === 'number') {
+      await gql(M_INV, { input: { name: 'available', reason: 'correction', ignoreCompareQuantity: true,
+        quantities: [{ inventoryItemId: newInv, locationId: 'gid://shopify/Location/5795643504', quantity: o.inventoryQuantity }] } });
+      console.log(`  restored inventory available=${o.inventoryQuantity}`);
+    }
+    // 3. restore original positions (map old orphan id -> new id)
+    const positions = rec.original_positions.map(p => ({ id: p.id === o.id ? newVid : p.id, position: p.position }));
+    const rr = await gql(M_REORDER, { productId, positions });
+    const dr = rr?.data?.productVariantsBulkReorder;
+    if (dr?.userErrors?.length || rr?.errors) console.error('  REORDER-BACK FAILED:', JSON.stringify(dr?.userErrors || rr.errors));
+    else console.log('  restored original positions');
+  }
+  console.log('\nUNDO complete (recreated orphans have NEW ids — see mapping above).');
+})();
diff --git a/scripts/TK-10405-fullfix.mjs b/scripts/TK-10405-fullfix.mjs
new file mode 100644
index 0000000..5257971
--- /dev/null
+++ b/scripts/TK-10405-fullfix.mjs
@@ -0,0 +1,138 @@
+#!/usr/bin/env node
+// TK-10405 FULL FIX (supersedes A') for the 2 held Rebel Walls Default-Title orphans.
+// Steve-approved in-session (TK-10405). Per product: REORDER Mural(per m²)->pos1,
+// Sample->pos2, Default-Title orphan->pos3 (productVariantsBulkReorder, all positions),
+// THEN DELETE the orphan (productVariantsBulkDelete). End state = sellable Mural at pos1,
+// so PDP JSON-LD offers[0] reads $117.47. Reversible: restore map captures the orphan's
+// full recreate payload + original positions; undo script recreates + reorders back.
+//
+// SCOPE = exactly these 2 products / delete exactly these 2 orphan variants:
+//   6679729078323 dwrw-76016 A City Rises -> delete 44453186830387
+//   6679739629619 dwrw-76264 A Priori     -> delete 44453188206643
+//
+// Run:  node scripts/TK-10405-fullfix.mjs
+import fs from 'node:fs';
+import { execFileSync } from 'node:child_process';
+
+const DOM = 'designer-laboratory-sandbox.myshopify.com';
+const VER = '2024-10';
+const RESTORE = new URL('../data/TK-10405-restore-map.json', import.meta.url).pathname;
+const LOGEXEC = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';
+
+const env = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)?.[1] || '').trim().replace(/^["']|["']$/g, '');
+if (!TOKEN) { console.error('no SHOPIFY_FULL_ACCESS_TOKEN'); process.exit(1); }
+
+const TARGETS = [
+  { pid: 'gid://shopify/Product/6679729078323', orphan: 'gid://shopify/ProductVariant/44453186830387', handle: 'dwrw-76016', label: 'A City Rises' },
+  { pid: 'gid://shopify/Product/6679739629619', orphan: 'gid://shopify/ProductVariant/44453188206643', handle: 'dwrw-76264', label: 'A Priori' },
+];
+
+async function gql(query, variables, tries = 6) {
+  for (let i = 0; i < tries; i++) {
+    const r = await fetch(`https://${DOM}/admin/api/${VER}/graphql.json`, {
+      method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+      body: JSON.stringify({ query, variables }),
+    });
+    if ([429, 500, 502, 503].includes(r.status) && i < tries - 1) { await new Promise(s => setTimeout(s, 1500 * (i + 1))); continue; }
+    return r.json();
+  }
+}
+const Q_PROD = `query($id:ID!){ product(id:$id){ id options{ name position values }
+  variants(first:20){ nodes{ id title position price sku taxable inventoryPolicy inventoryQuantity
+    selectedOptions{ name value } inventoryItem{ id tracked } } } } }`;
+const M_REORDER = `mutation($productId:ID!,$positions:[ProductVariantPositionInput!]!){
+  productVariantsBulkReorder(productId:$productId,positions:$positions){ product{ id } userErrors{ field message } } }`;
+const M_DELETE = `mutation($productId:ID!,$variantsIds:[ID!]!){
+  productVariantsBulkDelete(productId:$productId,variantsIds:$variantsIds){ product{ id } userErrors{ field message } } }`;
+
+function ledger(rec) { execFileSync('node', [LOGEXEC], { input: JSON.stringify(rec), stdio: ['pipe', 'inherit', 'inherit'] }); }
+const short = g => g.split('/').pop();
+
+(async () => {
+  const rm = JSON.parse(fs.readFileSync(RESTORE, 'utf8'));
+  const ff = { ts: new Date().toISOString(), approved_by: 'Steve in-session TK-10405',
+    action: 'FULL FIX: reorder Mural(per m²)->1, Sample->2, orphan->3, then DELETE orphan',
+    undo: 'node scripts/TK-10405-fullfix-undo.mjs — recreates orphan (NEW id) via productVariantsBulkCreate from prestate + restores original positions',
+    products: {} };
+
+  // 1. CAPTURE pre-state (positions of all variants + full orphan recreate payload) BEFORE any write
+  for (const t of TARGETS) {
+    const p = (await gql(Q_PROD, { id: t.pid }))?.data?.product;
+    if (!p) { console.error(`ABORT: product ${t.label} not found`); process.exit(1); }
+    const vs = p.variants.nodes;
+    const orphan = vs.find(v => v.id === t.orphan);
+    const mural = vs.find(v => /^Mural/.test(v.title));
+    const sample = vs.find(v => v.title === 'Sample');
+    if (!orphan) { console.log(`SKIP ${t.label}: orphan ${short(t.orphan)} already absent (idempotent)`); t.orphanGone = true; }
+    if (!mural || !sample) { console.error(`ABORT: ${t.label} missing Mural/Sample survivor`); process.exit(1); }
+    t.mural = mural.id; t.sample = sample.id;
+    ff.products[short(t.pid)] = {
+      product_gid: t.pid, handle: t.handle, label: t.label,
+      options: p.options,
+      original_positions: vs.map(v => ({ id: v.id, title: v.title, position: v.position })),
+      orphan_recreate: orphan ? { id: orphan.id, title: orphan.title, price: orphan.price, sku: orphan.sku,
+        taxable: orphan.taxable, inventoryPolicy: orphan.inventoryPolicy, inventoryQuantity: orphan.inventoryQuantity,
+        selectedOptions: orphan.selectedOptions, tracked: orphan.inventoryItem?.tracked } : null,
+    };
+  }
+  rm.full_fix_reorder_delete = ff;
+  fs.writeFileSync(RESTORE, JSON.stringify(rm, null, 2));
+  console.log('PRE-STATE (positions + orphan recreate payload) recorded to restore map for', TARGETS.map(t => t.label).join(' + '));
+
+  // 2. REORDER then DELETE per product
+  for (const t of TARGETS) {
+    console.log(`\n=== ${t.label} (${short(t.pid)}) ===`);
+    const positions = [{ id: t.mural, position: 1 }, { id: t.sample, position: 2 }];
+    if (!t.orphanGone) positions.push({ id: t.orphan, position: 3 });
+    const rr = await gql(M_REORDER, { productId: t.pid, positions });
+    const dr = rr?.data?.productVariantsBulkReorder;
+    if (dr?.userErrors?.length || rr?.errors) { console.error('REORDER FAILED:', JSON.stringify(dr?.userErrors || rr.errors)); process.exit(1); }
+    console.log('  reordered -> Mural pos1, Sample pos2' + (t.orphanGone ? '' : ', orphan pos3'));
+    if (!t.orphanGone) {
+      const rd = await gql(M_DELETE, { productId: t.pid, variantsIds: [t.orphan] });
+      const dd = rd?.data?.productVariantsBulkDelete;
+      if (dd?.userErrors?.length || rd?.errors) { console.error('DELETE FAILED:', JSON.stringify(dd?.userErrors || rd.errors)); process.exit(1); }
+      console.log('  deleted orphan', short(t.orphan));
+    }
+  }
+
+  // 3. VERIFY (Admin API) — orphan absent, Mural pos1 $117.47, Sample pos2
+  console.log('\n===== VERIFY (Admin API) =====');
+  let allok = true;
+  for (const t of TARGETS) {
+    const p = (await gql(Q_PROD, { id: t.pid })).data.product;
+    const vs = p.variants.nodes.sort((a, b) => a.position - b.position);
+    const orphanGone = !vs.find(v => v.id === t.orphan);
+    const pos1 = vs[0];
+    const muralPos1 = /^Mural/.test(pos1?.title || '') && pos1?.price === '117.47';
+    const ok = orphanGone && muralPos1;
+    allok = allok && ok;
+    console.log(`${t.label}: orphan-absent=${orphanGone} pos1='${pos1?.title}' $${pos1?.price} (${muralPos1 ? 'OK' : 'BAD'})`);
+    for (const v of vs) console.log(`    pos${v.position} ${short(v.id)} '${v.title}' $${v.price} sku=${v.sku}`);
+  }
+
+  // 4. VERIFY storefront (.json order + .js availability)
+  console.log('\n===== VERIFY (rendered storefront) =====');
+  for (const t of TARGETS) {
+    try {
+      const j = await (await fetch(`https://${DOM}/products/${t.handle}.json`, { headers: { 'User-Agent': 'Mozilla/5.0' } })).json();
+      const v0 = j.product.variants[0];
+      const hasOrphan = j.product.variants.some(x => x.title === 'Default Title');
+      console.log(`${t.handle}: storefront pos1='${v0.title}' $${v0.price} | Default-Title present=${hasOrphan}` +
+        (hasOrphan ? '  (CDN cache may lag a few min)' : ''));
+    } catch (e) { console.error(`${t.handle} storefront fetch failed:`, e.message); }
+  }
+
+  // 5. LEDGER (one entry per product: reorder+delete)
+  console.log('\n===== LEDGER =====');
+  for (const t of TARGETS) {
+    ledger({ agent: 'vp-dw-commerce', ticket: 'TK-10405',
+      action: `FULL FIX on ${t.label} (${short(t.pid)}): reordered Mural(per m²)->pos1 + Sample->pos2, DELETED Default-Title orphan ${short(t.orphan)}`,
+      blast_radius: 1, target: short(t.pid),
+      undo_cmd: `node ~/Projects/rebel-walls-push/scripts/TK-10405-fullfix-undo.mjs  # recreates orphan (new id) + restores positions from restore-map.full_fix_reorder_delete`,
+      verify: `product ${short(t.pid)}: orphan ${short(t.orphan)} absent; pos1=Mural(per m²) $117.47; Sample pos2; storefront offers[0]=$117.47` });
+  }
+  console.log('\n' + (allok ? 'FULL FIX COMPLETE — verification PASSED for both products.' : 'FULL FIX RAN but verification found a mismatch — review above.'));
+})();

← d6494a0 auto-data-snapshot: 2026-09-04T14:52:10 (1 data files) — dat  ·  back to Rebel Walls Push  ·  guard TK-11357 Fix D: rebel-walls push.js — refuse to MINT a 68b2721 →