← 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
M data/TK-10405-restore-map.jsonA scripts/TK-10405-fullfix-undo.mjsA scripts/TK-10405-fullfix.mjs
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 →