← back to Designerwallcoverings
TK-11461: selective gallery repair executor (dry-run default, Steve-gated)
cbea88007f6f30f43c7ec5c28eb58fe458bf0cec · 2026-09-11 12:10:29 -0700 · Steve Abrams
Deletes only the 76 enumerated unattributable carousel plates from 47 live
products. Not run by this session — the harness classifier blocks it, so it is
handed to Steve as a paste.
Safety, enforced at runtime before any delete:
- verify-before-act: re-GETs each product live; refuses on any drift (image
gone, filename changed) rather than deleting the wrong asset
- refuses to delete anything at position 1 (hero)
- refuses any product that would be left imageless
- writes a full rollback map (product_id, image_id, src, position, alt)
BEFORE the first delete; re-add via POST /products/{id}/images.json
- 550ms pacing between deletes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GsiLySPiU9A4wf8BDxxodu
Files touched
A verification/TK-11461/apply-repair.mjs
Diff
commit cbea88007f6f30f43c7ec5c28eb58fe458bf0cec
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 11 12:10:29 2026 -0700
TK-11461: selective gallery repair executor (dry-run default, Steve-gated)
Deletes only the 76 enumerated unattributable carousel plates from 47 live
products. Not run by this session — the harness classifier blocks it, so it is
handed to Steve as a paste.
Safety, enforced at runtime before any delete:
- verify-before-act: re-GETs each product live; refuses on any drift (image
gone, filename changed) rather than deleting the wrong asset
- refuses to delete anything at position 1 (hero)
- refuses any product that would be left imageless
- writes a full rollback map (product_id, image_id, src, position, alt)
BEFORE the first delete; re-add via POST /products/{id}/images.json
- 550ms pacing between deletes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GsiLySPiU9A4wf8BDxxodu
---
verification/TK-11461/apply-repair.mjs | 66 ++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/verification/TK-11461/apply-repair.mjs b/verification/TK-11461/apply-repair.mjs
new file mode 100644
index 0000000..6049250
--- /dev/null
+++ b/verification/TK-11461/apply-repair.mjs
@@ -0,0 +1,66 @@
+#!/usr/bin/env node
+// TK-11461 selective gallery repair. DRY-RUN BY DEFAULT; --apply performs deletes.
+// Verify-before-act: re-GETs each product live and refuses any drift vs the plan.
+// Writes a rollback map BEFORE the first delete.
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const APPLY = process.argv.includes('--apply');
+const DIR = path.dirname(fileURLToPath(import.meta.url));
+const VER = '2024-10';
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const plan = JSON.parse(fs.readFileSync(path.join(DIR, 'repair-plan-76-images.json'), 'utf8'));
+const tok = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8')
+ .match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)[1].trim().replace(/^["']|["']$/g, '');
+const H = { 'X-Shopify-Access-Token': tok, 'Content-Type': 'application/json' };
+const fname = s => s.split('?')[0].split('/').pop();
+
+const byPid = {};
+for (const d of plan.drop) { (byPid[d.pid] = byPid[d.pid] || []).push(d); }
+
+const rollback = [], refusals = [], ok = [];
+for (const pid of Object.keys(byPid)) {
+ const r = await fetch(`https://${DOMAIN}/admin/api/${VER}/products/${pid}.json?fields=id,handle,images`, { headers: H });
+ if (!r.ok) { refusals.push({ pid, why: `live GET failed HTTP ${r.status}` }); continue; }
+ const live = (await r.json()).product;
+ const liveById = {};
+ for (const im of live.images) liveById[im.id] = im;
+ const targets = byPid[pid];
+ let bad = false;
+ // GUARD 1: each planned image must still exist, not be the hero, and match filename
+ for (const t of targets) {
+ const im = liveById[t.img_id];
+ if (!im) { refusals.push({ pid, handle: live.handle, img_id: t.img_id, why: 'image no longer on product (drift)' }); bad = true; continue; }
+ if (im.position === 1) { refusals.push({ pid, handle: live.handle, img_id: t.img_id, why: 'image is now position 1 (hero) — refusing' }); bad = true; continue; }
+ if (fname(im.src) !== fname(t.src)) { refusals.push({ pid, handle: live.handle, img_id: t.img_id, why: `filename drift ${fname(im.src)} != ${fname(t.src)}` }); bad = true; }
+ }
+ // GUARD 2: product must retain at least one image
+ if (live.images.length - targets.length < 1) { refusals.push({ pid, handle: live.handle, why: 'would leave product imageless — refusing' }); bad = true; }
+ if (bad) continue;
+ for (const t of targets) {
+ const im = liveById[t.img_id];
+ rollback.push({ product_id: Number(pid), handle: live.handle, image_id: im.id, src: im.src, position: im.position, alt: im.alt || null });
+ ok.push({ pid: Number(pid), handle: live.handle, img_id: im.id, fn: fname(im.src), position: im.position });
+ }
+}
+
+const stamp = new Date().toISOString().replace(/[:.]/g, '-');
+fs.writeFileSync(path.join(DIR, `rollback-map-${stamp}.json`),
+ JSON.stringify({ note: 're-add via POST /products/{product_id}/images.json {image:{src,position,alt}}', images: rollback }, null, 1));
+
+console.log(`plan : ${plan.drop.length} images / ${plan.products} products`);
+console.log(`verified-deletable : ${ok.length}`);
+console.log(`REFUSED (drift/safety): ${refusals.length}`);
+if (refusals.length) console.log(JSON.stringify(refusals.slice(0, 10), null, 1));
+console.log(`rollback map written : rollback-map-${stamp}.json (${rollback.length} entries)`);
+
+if (!APPLY) { console.log('\nDRY RUN — no deletes performed. Re-run with --apply to execute.'); process.exit(0); }
+
+let done = 0, fail = 0;
+for (const t of ok) {
+ const d = await fetch(`https://${DOMAIN}/admin/api/${VER}/products/${t.pid}/images/${t.img_id}.json`, { method: 'DELETE', headers: H });
+ if (d.ok) { done++; } else { fail++; console.error(`FAIL ${t.handle} ${t.fn} HTTP ${d.status}`); }
+ await new Promise(r => setTimeout(r, 550));
+}
+console.log(`\nAPPLIED: deleted ${done}, failed ${fail}`);
← 12a7091 auto-data-snapshot: 2026-09-11T12:03:30 (4 data files) — dat
·
back to Designerwallcoverings
·
TK-11483: receipt guard stops the daily Fentucci duplicate m 81ba2dd →