← back to Designerwallcoverings
TK-11357 lane-16: latigo cork $0 'Sold Per Yard' placeholder delete executor (mirrors negative-tested TK-11635 guard set; dry-run default; restore-map-first; rollback included). Deleted 6 CORK-927xx placeholders 2026-09-18, gmc-feed-guard FAIL6->PASS0.
c36585882e308c923a220595151a5b3f1c8b9c52 · 2026-09-17 19:15:27 -0700 · Steve Abrams
Files touched
A scripts/tk11357-zero-price-stopgap/probe-latigo.mjsA scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-delete.mjsA scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-restore.mjsA scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-sweep.mjs
Diff
commit c36585882e308c923a220595151a5b3f1c8b9c52
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 17 19:15:27 2026 -0700
TK-11357 lane-16: latigo cork $0 'Sold Per Yard' placeholder delete executor (mirrors negative-tested TK-11635 guard set; dry-run default; restore-map-first; rollback included). Deleted 6 CORK-927xx placeholders 2026-09-18, gmc-feed-guard FAIL6->PASS0.
---
.../tk11357-zero-price-stopgap/probe-latigo.mjs | 56 ++++++
.../tk11357-latigo-zerovariant-delete.mjs | 191 +++++++++++++++++++++
.../tk11357-latigo-zerovariant-restore.mjs | 74 ++++++++
.../tk11357-latigo-zerovariant-sweep.mjs | 97 +++++++++++
4 files changed, 418 insertions(+)
diff --git a/scripts/tk11357-zero-price-stopgap/probe-latigo.mjs b/scripts/tk11357-zero-price-stopgap/probe-latigo.mjs
new file mode 100644
index 0000000..de43183
--- /dev/null
+++ b/scripts/tk11357-zero-price-stopgap/probe-latigo.mjs
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+/**
+ * TK-11357 lane-16 — READ-ONLY probe of the 6 latigo cork gmc-feed-guard offenders.
+ * Captures full prestate: variants (price/sku/availability/inventory), tags, publications.
+ * Writes probe-latigo.out.json next to this script. Never mutates.
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { gql, getLocations, SHOP, TOKEN_LAST4 } from './lib.mjs';
+
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+const HANDLES = [
+ 'latigo-real-cork-wallpaper-cork-92700',
+ 'latigo-real-cork-wallpaper-cork-92702',
+ 'latigo-real-cork-wallpaper-cork-92703',
+ 'latigo-real-cork-wallpaper-cork-92704',
+ 'latigo-real-cork-wallpaper-cork-92705',
+ 'latigo-real-cork-wallpaper-cork-92706',
+];
+
+const Q = `
+ query($handle:String!){
+ productByHandle(handle:$handle){
+ id title handle vendor status tags
+ publishedOnPublication(publicationId:"gid://shopify/Publication/29646651457")
+ variants(first:100){
+ nodes{
+ id title price sku availableForSale inventoryPolicy inventoryQuantity
+ inventoryItem{ id tracked sku }
+ selectedOptions{ name value }
+ }
+ }
+ }
+ }`;
+
+async function main() {
+ console.log(`[probe] store=${SHOP} token=…${TOKEN_LAST4} (READ-ONLY)`);
+ const locNames = await getLocations();
+ const out = { ts: new Date().toISOString(), store: SHOP, publication_google: 'gid://shopify/Publication/29646651457', locations: locNames, products: [] };
+ for (const h of HANDLES) {
+ const d = await gql(Q, { handle: h });
+ if (d?.__err) { console.error(`GraphQL error on ${h}:`, JSON.stringify(d.__err).slice(0, 300)); process.exit(1); }
+ const p = d.productByHandle;
+ if (!p) { console.error(`NOT FOUND: ${h}`); out.products.push({ handle: h, missing: true }); continue; }
+ const norm = v => ({ ...v, inventoryItem: v.inventoryItem ? { ...v.inventoryItem, id: v.inventoryItem.id } : null });
+ out.products.push(p);
+ console.log(`\n=== ${h} [${p.vendor}] status=${p.status} onGoogle=${p.publishedOnPublication} tags=${JSON.stringify(p.tags)}`);
+ for (const v of p.variants.nodes) {
+ console.log(` variant ${v.id.split('/').pop()} title="${v.title}" price=$${v.price} sku=${v.sku} aFS=${v.availableForSale} policy=${v.inventoryPolicy} qty=${v.inventoryQuantity} invItem=${v.inventoryItem?.id?.split('/').pop()} tracked=${v.inventoryItem?.tracked} opts=${v.selectedOptions.map(o => `${o.name}:${o.value}`).join('|')}`);
+ }
+ }
+ fs.writeFileSync(path.join(HERE, 'probe-latigo.out.json'), JSON.stringify(out, null, 2));
+ console.log(`\n[probe] wrote probe-latigo.out.json`);
+}
+main().catch(e => { console.error(e); process.exit(1); });
\ No newline at end of file
diff --git a/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-delete.mjs b/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-delete.mjs
new file mode 100644
index 0000000..dbb4003
--- /dev/null
+++ b/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-delete.mjs
@@ -0,0 +1,191 @@
+#!/usr/bin/env node
+/**
+ * TK-11357 lane-16 — latigo "Sold Per Yard" $0 placeholder DELETE executor.
+ * Mirrors scripts/tk11635-fentucci-zerovariant-delete.mjs (proven, negative-tested) with:
+ * - variant title "Sold Per Yard" (not "Per Yard")
+ * - vendor "Phillipe Romano" (not "Fentucci Naturals")
+ * - manifest from tk11357-latigo-zerovariant-sweep.mjs
+ * - G4' additionally pins the handle (belt+braces; sweep already pinned by handle)
+ *
+ * DRY-RUN BY DEFAULT. `--apply` performs the LIVE destructive write. Sanctioned shape:
+ * CMO-Paris precedent / fentucci go-live.py step 3a (Steve-approved 2026-07-27): DELETE the
+ * $0 per-yard placeholder variant, keep the Sample @ $4.25 — removes the JSON-LD price:0.0
+ * offer (gmc-feed-guard sample-leak FAIL) without touching price/policy/visibility of survivors.
+ *
+ * SAFETY MODEL (hard refusals, mirroring TK-11635):
+ * G1 variant still titled "Sold Per Yard" AND priced 0.00 live
+ * G2 variant still inert: availableForSale=false AND qty=0 AND policy=DENY
+ * -> if re-stamped orderable, HARD STOP the whole run (TK-11357 re-stamper signature)
+ * G3 deleting leaves >=1 sibling AND >=1 sibling priced > 0 (the Sample)
+ * G4 vendor still "Phillipe Romano" AND handle matches the pinned set
+ * G5 restore-map row carries product_id + variant_id (reversible)
+ * Rollback: tk11357-latigo-zerovariant-restore.mjs <manifest> --apply
+ */
+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 MANIFEST = (args.find(a => a.startsWith('--manifest=')) || '--manifest=/tmp/tk11357_latigo_sweep.json').split('=')[1];
+const VENDOR = 'Phillipe Romano';
+const HANDLES = new Set([
+ 'latigo-real-cork-wallpaper-cork-92700',
+ 'latigo-real-cork-wallpaper-cork-92702',
+ 'latigo-real-cork-wallpaper-cork-92703',
+ 'latigo-real-cork-wallpaper-cork-92704',
+ 'latigo-real-cork-wallpaper-cork-92705',
+ 'latigo-real-cork-wallpaper-cork-92706',
+]);
+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 (!row.handle || !HANDLES.has(row.handle)) return 'G4 handle not in pinned set';
+ if (!live) return 'G0 variant not found live (already gone / changed)';
+ if (live.vendor !== VENDOR) return `G4 vendor changed: ${live.vendor}`;
+ if (live.handle && live.handle !== row.handle) return `G4 handle changed: ${live.handle}`;
+ if (!/per yard/i.test(live.title || '')) 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`;
+ if (live.productStatus !== 'ACTIVE') return `G4 product status changed: ${live.productStatus}`;
+ 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 ----------
+if (TEST) {
+ const ok = { product_id: '1', variant_id: '2', handle: 'latigo-real-cork-wallpaper-cork-92700' };
+ const base = { vendor: VENDOR, handle: 'latigo-real-cork-wallpaper-cork-92700', title: 'Sold Per Yard', price: '0.00',
+ availableForSale: false, inventoryQuantity: 0, inventoryPolicy: 'DENY', productStatus: 'ACTIVE',
+ 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'],
+ ['G4 handle not pinned', { ...ok, handle: 'some-other-handle' }, base, 'G4'],
+ ['G4 product un-published', ok, { ...base, productStatus: 'DRAFT' }, '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(30)} 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 manifest ----------
+if (!fs.existsSync(MANIFEST)) { console.error(`FATAL: manifest not found: ${MANIFEST}\nRun tk11357-latigo-zerovariant-sweep.mjs first.`); process.exit(2); }
+const man = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
+if (!man.enumeration_complete) { console.error('FATAL: manifest from INCOMPLETE enumeration — refusing.'); process.exit(3); }
+if (man.zero_ORDERABLE !== 0) { console.error(`FATAL: ${man.zero_ORDERABLE} placeholder variant(s) ORDERABLE at $0 — this is the TK-11357 re-stamper signature; fix orderability first (tripwire), delete later.`); process.exit(8); }
+let rows = man.rows.filter(r => r.delete_safe);
+if (rows.length !== 6) { console.error(`FATAL: expected exactly 6 delete-safe rows, got ${rows.length} — refusing.`); process.exit(9); }
+
+console.error(`TK-11357 latigo delete executor mode=${APPLY ? 'APPLY (LIVE DESTRUCTIVE WRITE, 6 pinned variants)' : 'DRY-RUN'} candidates=${rows.length}`);
+
+// ---------- LIVE re-verify ----------
+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 handle 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 += 6) {
+ const d = await gql(VQ, { ids: vids.slice(i, i + 6) });
+ 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, handle: n.product.handle, productStatus: n.product.status,
+ 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 + 6)) if (!seen.has(id)) {
+ const row = byVid.get(id);
+ skipped.push({ handle: row.handle, variant_id: row.variant_id, reason: guard(row, null) });
+ }
+}
+
+console.log(`\nCLEARED to delete : ${cleared.length}`);
+console.log(`SKIPPED (guard refused) : ${skipped.length}`);
+if (skipped.length) { console.log(' reasons:', JSON.stringify(skipped, null, 1)); }
+if (hardStop) { console.error(`\nHARD STOP — an inert $0 variant has become orderable/re-stamped since the sweep:\n ${hardStop}\nRefusing the whole run.`); process.exit(5); }
+
+console.log('\nWhat would be deleted (variant recoverable from the manifest):');
+for (const c of cleared) console.log(` ${c.handle} product ${c.product_id} variant ${c.variant_id} "${c.v_title}" $${c.price} sku=${c.sku} (siblings surviving: ${c.live_siblings})`);
+
+const planPath = '/tmp/tk11357_latigo_delete_plan.json';
+fs.writeFileSync(planPath, JSON.stringify({ ticket: 'TK-11357-lane16', 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 executes the 6 deletions.'); process.exit(0); }
+
+// ---------- LIVE DESTRUCTIVE WRITE (6 variants, one batch, re-check first) ----------
+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 (const c of cleared) {
+ // RE-CHECK IMMEDIATELY BEFORE THE DELETE (stale-upfront-verdict protection, mirrors TK-11635).
+ const rc = await gql(VQ, { ids: [c.variant_gid] });
+ const rn = rc?.nodes?.[0];
+ const rlive = rn ? { vendor: rn.product.vendor, handle: rn.product.handle, productStatus: rn.product.status,
+ title: rn.title, price: rn.price, availableForSale: rn.availableForSale,
+ inventoryPolicy: rn.inventoryPolicy, inventoryQuantity: rn.inventoryQuantity,
+ siblings: rn.product.variants.nodes.filter(v => v.id !== rn.id) } : null;
+ const rReason = guard(c, rlive);
+ if (rReason) {
+ console.error(`\n SKIP (changed since pre-flight) ${c.handle}: ${rReason}`);
+ if (isHardStop(rReason)) { console.error(' HARD STOP mid-run — a $0 variant became orderable. Stopping.'); process.exit(7); }
+ err++; continue;
+ }
+ 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); }
+ const sampleLeft = left.some(v => Number(v.price) > 0);
+ if (!sampleLeft) { err++; console.error(` ALARM ${c.handle}: no priced variant left — STOPPING`); process.exit(6); }
+ ok++;
+ fs.appendFileSync(ledger, JSON.stringify({ ts: new Date().toISOString(), agent: 'yoloforever-night-lane16', ticket: 'TK-11357',
+ action: 'delete $0 "Sold Per Yard" placeholder variant (latigo cork / Phillipe Romano) — removes JSON-LD price:0.0 offer; Sample @$4.25 survives (CMO-Paris/go-live.py sanctioned shape)',
+ product_id: c.product_id, handle: c.handle, variant_id: c.variant_id, sku: c.sku, blast_radius: 1,
+ restore_map: MANIFEST,
+ undo_cmd: `node ~/Projects/designerwallcoverings/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-restore.mjs --manifest=${MANIFEST} --only=${c.variant_id} --apply`,
+ verify: `product ${c.product_id} has exactly 1 variant "Sample" $4.25; JSON-LD emits 4.25/InStock` }) + '\n');
+ console.error(` deleted ${c.handle} (variant ${c.variant_id})`);
+}
+console.log(`\nDONE. deleted=${ok} errors=${err}`);
+console.log(`ledger -> ${ledger}`);
+console.log(`rollback: node scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-restore.mjs --manifest=${MANIFEST} --apply`);
\ No newline at end of file
diff --git a/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-restore.mjs b/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-restore.mjs
new file mode 100644
index 0000000..3435c3c
--- /dev/null
+++ b/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-restore.mjs
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+/**
+ * TK-11357 lane-16 — ROLLBACK for the latigo $0 "Sold Per Yard" placeholder delete.
+ * Recreates each deleted placeholder from the sweep manifest (full fidelity: sku, tracked,
+ * weight, options, position, price 0.00, DENY). Mirrors tk11635-fentucci-zerovariant-restore.mjs.
+ *
+ * DRY-RUN BY DEFAULT. `--apply` fires the live write (UNDO of an approved action; ledgered).
+ * Fidelity note (same as TK-11635): a recreated variant gets a NEW variant_id.
+ */
+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/tk11357_latigo_sweep.json').split('=')[1];
+const ONLY = (args.find(a => a.startsWith('--only=')) || '--only=').split('=')[1];
+
+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-11357 latigo 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 += 6) {
+ const d = await gql(Q, { ids: pids.slice(i, i + 6) });
+ 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 => /per yard/i.test(v.title) && Number(v.price) === 0)) { already.push(row.handle); continue; }
+ need.push(row);
+ }
+ for (const id of pids.slice(i, i + 6)) 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: 'Size', name: 'Sold Per Yard' }] };
+ if (r.position != null) v.position = r.position;
+ const inv = {};
+ if (r.sku) inv.sku = r.sku;
+ if (r.inventory_item_tracked !== null && r.inventory_item_tracked !== undefined) inv.tracked = r.inventory_item_tracked;
+ if (r.weight && Number(r.weight.value) > 0) inv.measurement = { weight: { value: Number(r.weight.value), unit: r.weight.unit } };
+ if (Object.keys(inv).length) v.inventoryItem = inv;
+ 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: 'yoloforever-night-lane16', ticket: 'TK-11357',
+ action: 'RESTORE $0 "Sold Per Yard" placeholder variant (rollback of the lane-16 latigo 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 ~/Projects/designerwallcoverings/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-delete.mjs --manifest=${MANIFEST} --apply`,
+ verify: `product ${r.product_id} again has a "Sold Per Yard" $0.00 variant` }) + '\n');
+ process.stderr.write(`\r restored ${ok} errors ${err} `);
+}
+console.log(`\n\nDONE. restored=${ok} errors=${err}`);
\ No newline at end of file
diff --git a/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-sweep.mjs b/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-sweep.mjs
new file mode 100644
index 0000000..2c00ac5
--- /dev/null
+++ b/scripts/tk11357-zero-price-stopgap/tk11357-latigo-zerovariant-sweep.mjs
@@ -0,0 +1,97 @@
+#!/usr/bin/env node
+/**
+ * TK-11357 lane-16 — READ-ONLY sweep of the 6 latigo "Sold Per Yard" $0 placeholders
+ * (vendor Phillipe Romano, CORK-927xx family). Mirrors scripts/tk11635-fentucci-zerovariant-sweep.mjs
+ * (enumeration_complete guard, full per-row restore payload), scoped to the 6 pinned handles.
+ *
+ * Emits /tmp/tk11357_latigo_sweep.json — the restore map for tk11357-latigo-zerovariant-delete.mjs.
+ * $0, no writes.
+ */
+import { gql } from '../lib/shopify.mjs';
+import fs from 'node:fs';
+
+const OUT = process.env.TK11357_LATIGO_OUT || '/tmp/tk11357_latigo_sweep.json';
+const VENDOR = 'Phillipe Romano';
+const HANDLES = [
+ 'latigo-real-cork-wallpaper-cork-92700',
+ 'latigo-real-cork-wallpaper-cork-92702',
+ 'latigo-real-cork-wallpaper-cork-92703',
+ 'latigo-real-cork-wallpaper-cork-92704',
+ 'latigo-real-cork-wallpaper-cork-92705',
+ 'latigo-real-cork-wallpaper-cork-92706',
+];
+
+const Q = `
+ query($handle:String!){
+ productByHandle(handle:$handle){
+ 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 }
+ }
+ }
+ }
+ }`;
+
+const products = [];
+for (const h of HANDLES) {
+ const d = await gql(Q, { handle: h });
+ if (!d || d.__err) { console.error('GraphQL error on', h, ':', JSON.stringify(d?.__err || d)); process.exit(2); }
+ const p = d.productByHandle;
+ if (!p) { console.error('FATAL: product not found:', h); process.exit(2); }
+ products.push(p);
+}
+
+// classify — placeholder = title "Sold Per Yard" AND price 0
+const rows = [], oddities = [];
+let zeroInert = 0, zeroOrderable = 0;
+for (const p of products) {
+ const vs = p.variants.nodes;
+ const perYard = vs.filter(v => /per yard/i.test(v.title) && Number(v.price) === 0);
+ const others = vs.filter(v => !(/per yard/i.test(v.title) && 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) continue;
+ 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, vendor: p.vendor,
+ 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-11357-lane16', generated_at: new Date().toISOString(), vendor: VENDOR,
+ handles: HANDLES, enumeration_complete: true, 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,
+ 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(`products enumerated : ${manifest.products_total}`);
+console.log(`products with $0 per-yard placeholder: ${manifest.products_with_zero_peryard}`);
+console.log(` INERT : ${zeroInert}`);
+console.log(` ORDERABLE at $0 : ${zeroOrderable} <-- must be 0`);
+console.log(`delete-SAFE (priced sibling survives): ${manifest.delete_safe}`);
+console.log(`delete-UNSAFE (held) : ${manifest.delete_UNSAFE_held}`);
+if (oddities.length) console.log('ODDITIES:', JSON.stringify(oddities));
+console.log('restore-map / manifest ->', OUT);
\ No newline at end of file
← 5c9b9f2 auto-data-snapshot: 2026-09-17T19:01:39 (25 data files) — ev
·
back to Designerwallcoverings
·
auto-data-snapshot: 2026-09-17T20:05:20 (8 data files) — evi 5ede52b →