← back to Designerwallcoverings
TK-11635: Fentucci $0 Per-Yard variant sweep + dry-default guarded delete executor + rollback
cce7427d9ed7dd025c2eec17795d832c9c556ac1 · 2026-09-13 16:12:34 -0700 · Steve Abrams
Read-only sweep emits the complete restore-map (product_id/variant_id/inventory_item_id)
the gated memo said was still missing, and refuses to emit on an incomplete enumeration.
Delete executor is DRY-RUN by default; --apply stays Steve-gated. Five guards re-check LIVE
state per variant before acting, and a variant that has become orderable/re-stamped since the
sweep HARD-STOPS the whole run (TK-11357 signature). Ships a negative test (--test) proving
all 11 injected faults go red and a clean row passes.
Rollback recreates each placeholder from the restore-map, skipping any product where the
placeholder still exists so it can never double-create.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXQjayvMLrRoUmj3pFfcAB
Files touched
A scripts/tk11635-fentucci-zerovariant-delete.mjsA scripts/tk11635-fentucci-zerovariant-restore.mjsA scripts/tk11635-fentucci-zerovariant-sweep.mjs
Diff
commit cce7427d9ed7dd025c2eec17795d832c9c556ac1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Sep 13 16:12:34 2026 -0700
TK-11635: Fentucci $0 Per-Yard variant sweep + dry-default guarded delete executor + rollback
Read-only sweep emits the complete restore-map (product_id/variant_id/inventory_item_id)
the gated memo said was still missing, and refuses to emit on an incomplete enumeration.
Delete executor is DRY-RUN by default; --apply stays Steve-gated. Five guards re-check LIVE
state per variant before acting, and a variant that has become orderable/re-stamped since the
sweep HARD-STOPS the whole run (TK-11357 signature). Ships a negative test (--test) proving
all 11 injected faults go red and a clean row passes.
Rollback recreates each placeholder from the restore-map, skipping any product where the
placeholder still exists so it can never double-create.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXQjayvMLrRoUmj3pFfcAB
---
scripts/tk11635-fentucci-zerovariant-delete.mjs | 168 +++++++++++++++++++++++
scripts/tk11635-fentucci-zerovariant-restore.mjs | 71 ++++++++++
scripts/tk11635-fentucci-zerovariant-sweep.mjs | 99 +++++++++++++
3 files changed, 338 insertions(+)
diff --git a/scripts/tk11635-fentucci-zerovariant-delete.mjs b/scripts/tk11635-fentucci-zerovariant-delete.mjs
new file mode 100644
index 0000000..c2abdbf
--- /dev/null
+++ b/scripts/tk11635-fentucci-zerovariant-delete.mjs
@@ -0,0 +1,168 @@
+// TK-11635 — Fentucci Naturals $0 "Per Yard" variant DELETE executor.
+//
+// DRY-RUN BY DEFAULT. `--apply` is the GATED live customer-facing destructive Shopify write
+// (Steve go only, per the memo at ~/.claude/yolo-queue/pending-approval/2026-09-13-TK-11635-*.md).
+//
+// SAFETY MODEL (every one of these is a hard refusal, not a warning):
+// G1 the variant must STILL be titled exactly "Per Yard" AND priced 0.00 live, right now
+// G2 the variant must STILL be inert: availableForSale=false AND qty=0 AND policy=DENY
+// -> if it has been re-stamped orderable since the sweep, we STOP the whole run
+// G3 deleting it must leave >=1 sibling variant, AND >=1 sibling priced > 0 (the Sample)
+// G4 the product must still be the expected vendor
+// G5 the restore-map row must carry product_id + variant_id (else it is not reversible -> skip)
+// Anything that fails a guard is SKIPPED and reported; guard failures never silently pass.
+//
+// Rollback: scripts/tk11635-fentucci-zerovariant-restore.mjs <manifest> --apply
+// Negative test: `--test` injects faults and proves each guard goes RED (ships with the change).
+import { gql } from './lib/shopify.mjs';
+import fs from 'node:fs';
+
+const args = process.argv.slice(2);
+const APPLY = args.includes('--apply');
+const TEST = args.includes('--test');
+const LIMIT = Number((args.find(a => a.startsWith('--limit=')) || '--limit=0').split('=')[1]);
+const BATCH = Number((args.find(a => a.startsWith('--batch=')) || '--batch=50').split('=')[1]);
+const PACE_MS = Number((args.find(a => a.startsWith('--pace-ms=')) || '--pace-ms=90000').split('=')[1]);
+const MANIFEST = (args.find(a => a.startsWith('--manifest=')) || '--manifest=/tmp/tk11635_fentucci_sweep.json').split('=')[1];
+const VENDOR = 'Fentucci Naturals';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+// ---------- the guard, isolated so the negative test can hit it directly ----------
+export function guard(row, live) {
+ if (!row.product_id || !row.variant_id) return 'G5 restore-map row missing product_id/variant_id (not reversible)';
+ if (!live) return 'G0 variant not found live (already gone / changed)';
+ if (live.vendor !== VENDOR) return `G4 vendor changed: ${live.vendor}`;
+ if (live.title !== 'Per Yard') return `G1 variant title changed: ${JSON.stringify(live.title)}`;
+ if (Number(live.price) !== 0) return `G1 variant price no longer 0.00: ${live.price}`;
+ if (live.availableForSale !== false) return 'G2 variant went AVAILABLE FOR SALE since sweep (re-stamper?) — HARD STOP';
+ if (Number(live.inventoryQuantity) !== 0) return `G2 variant qty no longer 0: ${live.inventoryQuantity} (re-stamper?) — HARD STOP`;
+ if (live.inventoryPolicy !== 'DENY') return `G2 inventoryPolicy no longer DENY: ${live.inventoryPolicy} — HARD STOP`;
+ const sibs = (live.siblings || []);
+ if (sibs.length < 1) return 'G3 delete would leave product with 0 variants';
+ if (!sibs.some(s => Number(s.price) > 0)) return 'G3 no priced sibling would survive';
+ return null; // clear
+}
+const isHardStop = reason => !!reason && reason.includes('HARD STOP');
+
+// ---------- negative test: prove the guard goes RED on injected faults ----------
+if (TEST) {
+ const ok = { product_id: '1', variant_id: '2' };
+ const base = { vendor: VENDOR, title: 'Per Yard', price: '0.00', availableForSale: false,
+ inventoryQuantity: 0, inventoryPolicy: 'DENY', siblings: [{ title: 'Sample', price: '4.25' }] };
+ const cases = [
+ ['clean row passes', ok, base, null],
+ ['G0 missing live variant', ok, null, 'G0'],
+ ['G1 title changed', ok, { ...base, title: 'Sample' }, 'G1'],
+ ['G1 price no longer 0', ok, { ...base, price: '59.06' }, 'G1'],
+ ['G2 went orderable (re-stamper)', ok, { ...base, availableForSale: true }, 'G2'],
+ ['G2 qty re-stamped 2026', ok, { ...base, inventoryQuantity: 2026 }, 'G2'],
+ ['G2 policy flipped CONTINUE', ok, { ...base, inventoryPolicy: 'CONTINUE' }, 'G2'],
+ ['G3 would leave 0 variants', ok, { ...base, siblings: [] }, 'G3'],
+ ['G3 no priced survivor', ok, { ...base, siblings: [{ title: 'x', price: '0.00' }] }, 'G3'],
+ ['G4 vendor changed', ok, { ...base, vendor: 'Someone Else' }, 'G4'],
+ ['G5 row missing ids', { product_id: null, variant_id: null }, base, 'G5'],
+ ];
+ let fail = 0;
+ for (const [name, row, live, want] of cases) {
+ const got = guard(row, live);
+ const pass = want === null ? got === null : (got || '').startsWith(want);
+ if (!pass) fail++;
+ console.log(`${pass ? 'RED-OK ' : 'BROKEN '} ${name.padEnd(34)} want=${want ?? 'clear'} got=${got ?? 'clear'}`);
+ }
+ console.log(fail === 0
+ ? '\nNEGATIVE TEST PASSED — every injected fault is refused, clean row passes.'
+ : `\nNEGATIVE TEST FAILED — ${fail} case(s) did not behave.`);
+ process.exit(fail === 0 ? 0 : 1);
+}
+
+// ---------- load restore-map ----------
+if (!fs.existsSync(MANIFEST)) { console.error(`FATAL: manifest not found: ${MANIFEST}\nRun scripts/tk11635-fentucci-zerovariant-sweep.mjs first.`); process.exit(2); }
+const man = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
+if (!man.enumeration_complete) { console.error('FATAL: manifest came from an INCOMPLETE enumeration — refusing.'); process.exit(3); }
+let rows = man.rows.filter(r => r.delete_safe);
+if (LIMIT > 0) rows = rows.slice(0, LIMIT);
+
+console.error(`TK-11635 delete executor mode=${APPLY ? 'APPLY (LIVE DESTRUCTIVE WRITE)' : 'DRY-RUN'} candidates=${rows.length} batch=${BATCH} pace=${PACE_MS}ms`);
+
+// ---------- LIVE re-verify (100 variants per read) ----------
+const byVid = new Map(rows.map(r => [r.variant_gid, r]));
+const VQ = `query($ids:[ID!]!){ nodes(ids:$ids){ ... on ProductVariant {
+ id title price availableForSale inventoryPolicy inventoryQuantity
+ product{ id vendor status variants(first:20){ nodes{ id title price } } } } } }`;
+const cleared = [], skipped = [];
+let hardStop = null;
+const vids = [...byVid.keys()];
+for (let i = 0; i < vids.length && !hardStop; i += 100) {
+ const d = await gql(VQ, { ids: vids.slice(i, i + 100) });
+ if (!d || d.__err) { console.error('FATAL read error:', JSON.stringify(d?.__err || d)); process.exit(4); }
+ const seen = new Set();
+ for (const n of d.nodes) {
+ if (!n) continue;
+ seen.add(n.id);
+ const row = byVid.get(n.id);
+ const live = { vendor: n.product.vendor, title: n.title, price: n.price, availableForSale: n.availableForSale,
+ inventoryPolicy: n.inventoryPolicy, inventoryQuantity: n.inventoryQuantity,
+ siblings: n.product.variants.nodes.filter(v => v.id !== n.id) };
+ const reason = guard(row, live);
+ if (reason) { skipped.push({ handle: row.handle, variant_id: row.variant_id, reason });
+ if (isHardStop(reason)) { hardStop = `${row.handle}: ${reason}`; break; } continue; }
+ cleared.push({ ...row, live_siblings: live.siblings.length });
+ }
+ for (const id of vids.slice(i, i + 100)) if (!seen.has(id)) {
+ const row = byVid.get(id);
+ skipped.push({ handle: row.handle, variant_id: row.variant_id, reason: guard(row, null) });
+ }
+ process.stderr.write(`\r re-verified ${cleared.length} cleared / ${skipped.length} skipped `);
+}
+process.stderr.write('\n');
+
+console.log(`\nCLEARED to delete : ${cleared.length}`);
+console.log(`SKIPPED (guard refused) : ${skipped.length}`);
+if (skipped.length) {
+ const byReason = {};
+ for (const s of skipped) { const k = s.reason.split(':')[0].slice(0, 40); byReason[k] = (byReason[k] || 0) + 1; }
+ console.log(' reasons:', JSON.stringify(byReason, null, 1));
+ console.log(' sample :', JSON.stringify(skipped.slice(0, 5), null, 1));
+}
+if (hardStop) { console.error(`\nHARD STOP — an inert $0 variant has become orderable/re-stamped since the sweep:\n ${hardStop}\nThis is the TK-11357 re-stamper signature. Refusing the whole run. Investigate before deleting anything.`); process.exit(5); }
+
+console.log('\nSample of what would be deleted (variant stays recoverable from the manifest):');
+for (const c of cleared.slice(0, 5)) console.log(` ${c.handle} product ${c.product_id} variant ${c.variant_id} "${c.v_title}" $${c.price} (siblings surviving: ${c.live_siblings})`);
+
+const planPath = '/tmp/tk11635_delete_plan.json';
+fs.writeFileSync(planPath, JSON.stringify({ ticket: 'TK-11635', generated_at: new Date().toISOString(), mode: APPLY ? 'apply' : 'dry-run',
+ manifest: MANIFEST, cleared: cleared.length, skipped, rows: cleared }, null, 1));
+console.log('\nplan ->', planPath);
+console.log('restore-map (rollback source) ->', MANIFEST);
+
+if (!APPLY) { console.log('\nDRY-RUN. NOTHING FIRED. `--apply` is GATED — Steve go only.'); process.exit(0); }
+
+// ---------- GATED LIVE DESTRUCTIVE WRITE ----------
+const ledger = process.env.HOME + '/.claude/yolo-queue/executed-reversible/ledger.jsonl';
+fs.mkdirSync(ledger.replace(/\/[^/]+$/, ''), { recursive: true });
+const M = `mutation($productId:ID!,$ids:[ID!]!){ productVariantsBulkDelete(productId:$productId, variantsIds:$ids){
+ product{ id variants(first:5){ nodes{ id title price } } } userErrors{ field message } } }`;
+let ok = 0, err = 0;
+for (let i = 0; i < cleared.length; i += BATCH) {
+ const batch = cleared.slice(i, i + BATCH);
+ console.error(`\nbatch ${Math.floor(i / BATCH) + 1}/${Math.ceil(cleared.length / BATCH)} (${batch.length} products)`);
+ for (const c of batch) {
+ const d = await gql(M, { productId: c.product_gid, ids: [c.variant_gid] });
+ const ue = d?.productVariantsBulkDelete?.userErrors || [];
+ if (d?.__err || ue.length) { err++; console.error(` ERR ${c.handle}:`, JSON.stringify(d?.__err || ue)); continue; }
+ const left = d.productVariantsBulkDelete.product?.variants?.nodes || [];
+ if (left.length === 0) { err++; console.error(` ALARM ${c.handle}: product now has 0 variants — STOPPING`); process.exit(6); }
+ ok++;
+ fs.appendFileSync(ledger, JSON.stringify({ ts: new Date().toISOString(), agent: 'claude-run-11635', ticket: 'TK-11635',
+ action: 'delete $0 "Per Yard" placeholder variant (Fentucci Naturals)', product_id: c.product_id, handle: c.handle,
+ variant_id: c.variant_id, blast_radius: 1,
+ undo_cmd: `node ~/Projects/designerwallcoverings/scripts/tk11635-fentucci-zerovariant-restore.mjs --manifest=${MANIFEST} --only=${c.variant_id} --apply`,
+ restore_map: MANIFEST,
+ verify: `product ${c.product_id} has exactly 1 variant "Sample" $4.25; JSON-LD emits 4.25/InStock` }) + '\n');
+ process.stderr.write(`\r deleted ${ok} errors ${err} `);
+ }
+ if (i + BATCH < cleared.length) { console.error(`\n pacing ${PACE_MS / 1000}s before next batch ...`); await sleep(PACE_MS); }
+}
+console.log(`\n\nDONE. deleted=${ok} errors=${err}`);
+console.log(`ledger -> ${ledger}`);
+console.log(`rollback: node scripts/tk11635-fentucci-zerovariant-restore.mjs --manifest=${MANIFEST} --apply`);
diff --git a/scripts/tk11635-fentucci-zerovariant-restore.mjs b/scripts/tk11635-fentucci-zerovariant-restore.mjs
new file mode 100644
index 0000000..59c390b
--- /dev/null
+++ b/scripts/tk11635-fentucci-zerovariant-restore.mjs
@@ -0,0 +1,71 @@
+// TK-11635 — ROLLBACK for the $0 "Per Yard" variant delete.
+// Recreates each deleted placeholder variant from the restore-map produced by
+// scripts/tk11635-fentucci-zerovariant-sweep.mjs.
+//
+// DRY-RUN BY DEFAULT. `--apply` fires the live write (this is the UNDO of an approved action,
+// so it inherits that approval; it is still a live Shopify write and is logged to the ledger).
+//
+// NOTE ON FIDELITY, stated rather than hidden: a recreated variant gets a NEW variant_id.
+// These are valueless onboarder placeholders (price 0, DENY, qty 0, never orderable, no
+// downstream references), so the id change costs nothing — but the undo is "equivalent
+// variant restored", not "byte-identical variant resurrected". Say that, don't overclaim.
+import { gql } from './lib/shopify.mjs';
+import fs from 'node:fs';
+
+const args = process.argv.slice(2);
+const APPLY = args.includes('--apply');
+const MANIFEST = (args.find(a => a.startsWith('--manifest=')) || '--manifest=/tmp/tk11635_fentucci_sweep.json').split('=')[1];
+const ONLY = (args.find(a => a.startsWith('--only=')) || '--only=').split('=')[1]; // comma-sep variant_ids
+
+if (!fs.existsSync(MANIFEST)) { console.error(`FATAL: restore-map not found: ${MANIFEST}`); process.exit(2); }
+const man = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
+let rows = man.rows.filter(r => r.delete_safe);
+if (ONLY) { const want = new Set(ONLY.split(',').map(s => s.trim())); rows = rows.filter(r => want.has(String(r.variant_id))); }
+
+console.error(`TK-11635 RESTORE mode=${APPLY ? 'APPLY (LIVE WRITE)' : 'DRY-RUN'} rows=${rows.length}`);
+
+// Only restore where the placeholder is genuinely ABSENT — never double-create.
+const Q = `query($ids:[ID!]!){ nodes(ids:$ids){ ... on Product { id handle vendor
+ variants(first:20){ nodes{ id title price } } } } }`;
+const need = [], already = [], missingProduct = [];
+const pids = [...new Set(rows.map(r => r.product_gid))];
+const byPid = new Map(); for (const r of rows) byPid.set(r.product_gid, r);
+for (let i = 0; i < pids.length; i += 100) {
+ const d = await gql(Q, { ids: pids.slice(i, i + 100) });
+ if (!d || d.__err) { console.error('FATAL read error:', JSON.stringify(d?.__err || d)); process.exit(4); }
+ const seen = new Set();
+ for (const n of d.nodes) {
+ if (!n) continue; seen.add(n.id);
+ const row = byPid.get(n.id);
+ if (n.variants.nodes.some(v => v.title === 'Per Yard' && Number(v.price) === 0)) { already.push(row.handle); continue; }
+ need.push(row);
+ }
+ for (const id of pids.slice(i, i + 100)) if (!seen.has(id)) missingProduct.push(byPid.get(id).handle);
+}
+console.log(`\nWILL RECREATE : ${need.length}`);
+console.log(`SKIP (placeholder still there): ${already.length}`);
+console.log(`SKIP (product not found) : ${missingProduct.length}`);
+if (!APPLY) { console.log('\nDRY-RUN. Nothing fired. Re-run with --apply to restore.'); process.exit(0); }
+
+const M = `mutation($productId:ID!,$variants:[ProductVariantsBulkInput!]!){
+ productVariantsBulkCreate(productId:$productId, variants:$variants){
+ productVariants{ id title price } userErrors{ field message } } }`;
+const ledger = process.env.HOME + '/.claude/yolo-queue/executed-reversible/ledger.jsonl';
+let ok = 0, err = 0;
+for (const r of need) {
+ const opts = (r.selected_options || []).map(o => ({ optionName: o.name, name: o.value }));
+ const v = { price: '0.00', inventoryPolicy: 'DENY', optionValues: opts.length ? opts : [{ optionName: 'Title', name: 'Per Yard' }] };
+ if (r.sku) v.inventoryItem = { sku: r.sku, tracked: false };
+ const d = await gql(M, { productId: r.product_gid, variants: [v] });
+ const ue = d?.productVariantsBulkCreate?.userErrors || [];
+ if (d?.__err || ue.length) { err++; console.error(` ERR ${r.handle}:`, JSON.stringify(d?.__err || ue)); continue; }
+ ok++;
+ fs.appendFileSync(ledger, JSON.stringify({ ts: new Date().toISOString(), agent: 'claude-run-11635', ticket: 'TK-11635',
+ action: 'RESTORE $0 "Per Yard" placeholder variant (rollback of the TK-11635 delete)',
+ product_id: r.product_id, handle: r.handle, old_variant_id: r.variant_id,
+ new_variant_id: d.productVariantsBulkCreate.productVariants?.[0]?.id, blast_radius: 1,
+ undo_cmd: `node scripts/tk11635-fentucci-zerovariant-delete.mjs --manifest=${MANIFEST} --apply`,
+ verify: `product ${r.product_id} again has a "Per Yard" $0.00 variant` }) + '\n');
+ process.stderr.write(`\r restored ${ok} errors ${err} `);
+}
+console.log(`\n\nDONE. restored=${ok} errors=${err}`);
diff --git a/scripts/tk11635-fentucci-zerovariant-sweep.mjs b/scripts/tk11635-fentucci-zerovariant-sweep.mjs
new file mode 100644
index 0000000..37ee3f0
--- /dev/null
+++ b/scripts/tk11635-fentucci-zerovariant-sweep.mjs
@@ -0,0 +1,99 @@
+// TK-11635 — READ-ONLY sweep: enumerate every ACTIVE "Fentucci Naturals" product and its FULL
+// variant set, and emit the COMPLETE restore-map the gated memo says is still missing
+// (product_id + variant_id + inventoryItem_id + every field needed to recreate the variant).
+//
+// $0 — GraphQL reads only. Fires NO write. Refuses to emit a manifest on an incomplete walk
+// (CLAUDE.md TK-11431 amendment 1: an unmeasured input is never PASS).
+import { gql } from './lib/shopify.mjs';
+import fs from 'node:fs';
+
+const OUT = process.env.TK11635_OUT || '/tmp/tk11635_fentucci_sweep.json';
+const VENDOR = 'Fentucci Naturals';
+
+const Q = `query($cursor:String){
+ products(first:50, after:$cursor, query:"vendor:'Fentucci Naturals' status:active"){
+ pageInfo{ hasNextPage endCursor }
+ nodes{
+ id handle title status vendor
+ variants(first:20){
+ nodes{
+ id title price position sku barcode taxable availableForSale
+ inventoryPolicy inventoryQuantity
+ inventoryItem{ id tracked measurement{ weight{ value unit } } }
+ selectedOptions{ name value }
+ }
+ }
+ }
+ }
+}`;
+
+let cursor = null, page = 0, complete = false;
+const products = [];
+for (;;) {
+ const d = await gql(Q, { cursor });
+ if (!d || d.__err) { console.error('GraphQL error:', JSON.stringify(d?.__err || d)); process.exit(2); }
+ products.push(...d.products.nodes);
+ page++;
+ process.stderr.write(`\r page ${page} products ${products.length} `);
+ if (!d.products.pageInfo.hasNextPage) { complete = true; break; }
+ cursor = d.products.pageInfo.endCursor;
+ if (page > 100) break; // runaway guard -> leaves complete=false
+}
+process.stderr.write('\n');
+
+if (!complete) { console.error('FATAL: enumeration did NOT reach hasNextPage=false — NOT-MEASURED, refusing to emit a manifest.'); process.exit(3); }
+
+// classify
+const rows = [], oddities = [];
+let zeroInert = 0, zeroOrderable = 0, sampleOnly = 0;
+for (const p of products) {
+ const vs = p.variants.nodes;
+ const perYard = vs.filter(v => v.title === 'Per Yard' && Number(v.price) === 0);
+ const others = vs.filter(v => !(v.title === 'Per Yard' && Number(v.price) === 0));
+ if (vs.length > 20) oddities.push({ handle: p.handle, why: 'variant page cap hit (>20) — NOT-MEASURED' });
+ if (perYard.length === 0) { if (vs.length === 1) sampleOnly++; continue; }
+ // survivor guard: deleting perYard must leave >=1 variant, and a real sellable one
+ const survivors = others.filter(v => Number(v.price) > 0);
+ const safe = others.length >= 1 && survivors.length >= 1;
+ for (const v of perYard) {
+ const orderable = v.availableForSale === true || Number(v.inventoryQuantity) > 0 || v.inventoryPolicy === 'CONTINUE';
+ if (orderable) zeroOrderable++; else zeroInert++;
+ rows.push({
+ product_gid: p.id, product_id: p.id.split('/').pop(), handle: p.handle, title: p.title, status: p.status,
+ variant_gid: v.id, variant_id: v.id.split('/').pop(),
+ inventory_item_gid: v.inventoryItem?.id || null, inventory_item_tracked: v.inventoryItem?.tracked ?? null,
+ v_title: v.title, price: v.price, position: v.position, sku: v.sku, barcode: v.barcode, taxable: v.taxable,
+ available_for_sale: v.availableForSale, inventory_policy: v.inventoryPolicy, inventory_quantity: v.inventoryQuantity,
+ weight: v.inventoryItem?.measurement?.weight ?? null,
+ selected_options: v.selectedOptions,
+ sibling_variants: others.map(o => ({ id: o.id, title: o.title, price: o.price, position: o.position, available: o.availableForSale })),
+ delete_safe: safe,
+ unsafe_reason: safe ? null : (others.length === 0 ? 'would leave product with 0 variants' : 'no priced sibling survivor'),
+ });
+ }
+}
+
+const manifest = {
+ ticket: 'TK-11635', generated_at: new Date().toISOString(), vendor: VENDOR,
+ enumeration_complete: complete, pages: page,
+ active_products_total: products.length,
+ products_with_zero_peryard: new Set(rows.map(r => r.product_id)).size,
+ zero_variants_total: rows.length,
+ zero_inert: zeroInert, zero_ORDERABLE: zeroOrderable,
+ sample_only_control: sampleOnly,
+ delete_safe: rows.filter(r => r.delete_safe).length,
+ delete_UNSAFE_held: rows.filter(r => !r.delete_safe).length,
+ oddities,
+ rows,
+};
+fs.writeFileSync(OUT, JSON.stringify(manifest, null, 1));
+
+console.log(`\nACTIVE "${VENDOR}" products enumerated : ${manifest.active_products_total} (complete=${complete}, ${page} pages)`);
+console.log(`products with a $0 "Per Yard" variant : ${manifest.products_with_zero_peryard}`);
+console.log(` of those, variant INERT : ${zeroInert}`);
+console.log(` of those, variant ORDERABLE at $0 : ${zeroOrderable} <-- must be 0`);
+console.log(`sample-only control (correct model) : ${sampleOnly}`);
+console.log(`delete-SAFE (priced sibling survives) : ${manifest.delete_safe}`);
+console.log(`delete-UNSAFE (held, never touched) : ${manifest.delete_UNSAFE_held}`);
+if (oddities.length) console.log('ODDITIES (not-measured):', JSON.stringify(oddities));
+console.log('\nrestore-map / manifest ->', OUT);
← 102998b TK-11076: harden the undo the gated memo rests on
·
back to Designerwallcoverings
·
TK-11076: undo verify compares tag SETS, not containment+len 8de1831 →