[object Object]

← back to Tk 10867 Gb Safe Plan

TK-10867: add DW-spec Sample-variant executor for 81 GB active products (restore-map + ledger, dry-run default)

a8ec80c1aa6abbab7bcbf562619c709361b5d05d · 2026-08-31 12:42:05 -0700 · Steve Abrams

Files touched

Diff

commit a8ec80c1aa6abbab7bcbf562619c709361b5d05d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 12:42:05 2026 -0700

    TK-10867: add DW-spec Sample-variant executor for 81 GB active products (restore-map + ledger, dry-run default)
---
 scripts/add-sample-variants.mjs | 102 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 102 insertions(+)

diff --git a/scripts/add-sample-variants.mjs b/scripts/add-sample-variants.mjs
new file mode 100644
index 0000000..0e961b3
--- /dev/null
+++ b/scripts/add-sample-variants.mjs
@@ -0,0 +1,102 @@
+#!/usr/bin/env node
+// TK-10867 — ADD the DW-spec "Sample" variant to the 81 ACTIVE Graham & Brown products
+// that lack one. Cohort authoritative source = data/live-missing-sample.json (built by the
+// read-only enumerator scripts/live-missing-sample-enumerate.mjs). Steve in-session GO 2026-08-31.
+//
+// DW Sample-variant spec (standing rule): every product carries a Sample variant —
+//   option1 = "Sample", SKU = {base_dw_sku}-Sample, price $4.25, NO inventory tracking.
+//
+// Method: REST POST /products/{id}/variants.json (adds a variant to the existing single-option
+// product; the existing option gets a second value "Sample"). Idempotent — re-checks live for an
+// existing sample variant per product and SKIPS if present. Reversibility FIRST: records the
+// new variant id to a restore-map JSONL before/after each create (undo = DELETE that variant).
+// Each create is ledgered to the executed-reversible ledger. DRY-RUN default; --apply to write.
+import fs from 'node:fs';
+
+const SECRETS = '/Users/macstudio3/Projects/secrets-manager/.env';
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const env = fs.readFileSync(SECRETS, 'utf8');
+const TOKEN = env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)[1].trim(); // full-access (write_products)
+const REST = `https://${STORE}/admin/api/2024-10`;
+const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+const APPLY = process.argv.includes('--apply');
+const LIMIT = parseInt((process.argv.find(a => a.startsWith('--limit=')) || '').split('=')[1] || '0', 10);
+
+const COHORT = JSON.parse(fs.readFileSync(new URL('../data/live-missing-sample.json', import.meta.url), 'utf8'));
+const TS = new Date().toISOString().replace(/[:.]/g, '-');
+const RESTORE = new URL(`../data/add-sample-restore-map-${TS}.jsonl`, import.meta.url).pathname;
+const AUDIT = new URL(`../data/add-sample-audit-${TS}.jsonl`, import.meta.url).pathname;
+const LEDGER = `${process.env.HOME}/.claude/yolo-queue/executed-reversible/ledger.jsonl`;
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+async function rest(method, url, body) {
+  for (let a = 0; a < 4; a++) {
+    const r = await fetch(url, { method, headers: H, body: body ? JSON.stringify(body) : undefined });
+    if (r.status === 429) { await sleep(2000); continue; }
+    if ((r.status === 502 || r.status === 503) && a < 3) { await sleep(1500 * (a + 1)); continue; }
+    const txt = await r.text();
+    let j; try { j = JSON.parse(txt); } catch { j = { _raw: txt }; }
+    return { status: r.status, ok: r.ok, j };
+  }
+  return { status: 0, ok: false, j: {} };
+}
+
+// Read live variants for a product; return {baseSku, hasSample}
+async function liveState(pid) {
+  const { j } = await rest('GET', `${REST}/products/${pid}.json?fields=id,status,variants`);
+  const p = j.product;
+  if (!p) return null;
+  const hasSample = p.variants.some(v => /-sample$/i.test(v.sku || '') || /sample/i.test(v.option1 || ''));
+  const base = p.variants.map(v => v.sku).find(s => s && !/-sample$/i.test(s));
+  return { status: p.status, hasSample, baseSku: base, variantCount: p.variants.length };
+}
+
+async function main() {
+  const items = COHORT.missing_sample.slice(0, LIMIT > 0 ? LIMIT : COHORT.missing_sample.length);
+  console.log(`add-sample-variants: cohort ${COHORT.missing_sample.length}, processing ${items.length} · ${APPLY ? 'APPLY' : 'DRY-RUN'}`);
+  const restoreOut = APPLY ? fs.createWriteStream(RESTORE, { flags: 'a' }) : null;
+  const auditOut = APPLY ? fs.createWriteStream(AUDIT, { flags: 'a' }) : null;
+  const ledgerOut = APPLY ? fs.createWriteStream(LEDGER, { flags: 'a' }) : null;
+  let added = 0, skipped = 0, failed = 0;
+  for (const it of items) {
+    const pid = it.id;
+    const st = await liveState(pid);
+    if (!st) { console.error(`  ERR ${pid}: product not found live`); failed++; continue; }
+    if (st.status.toUpperCase() !== 'ACTIVE') { console.error(`  SKIP ${pid}: not ACTIVE (${st.status})`); skipped++; continue; }
+    if (st.hasSample) { console.log(`  skip ${pid}: already has Sample`); skipped++; continue; }
+    const base = st.baseSku;
+    if (!base) { console.error(`  ERR ${pid}: no base SKU — refusing (never mint)`); failed++; continue; }
+    const sampleSku = `${base}-Sample`;
+    // DW spec: Sample variant, $4.25, NO inventory tracking (inventory_management=null),
+    // inventory_policy=continue so it never blocks checkout.
+    const variant = { option1: 'Sample', sku: sampleSku, price: '4.25',
+      inventory_management: null, inventory_policy: 'continue', requires_shipping: true, taxable: true };
+    if (!APPLY) {
+      console.log(`  ${pid} ${it.title.slice(0, 42)} -> +Sample ${sampleSku} $4.25 (untracked)`);
+      continue;
+    }
+    const r = await rest('POST', `${REST}/products/${pid}/variants.json`, { variant });
+    if (!r.ok || !r.j.variant) {
+      console.error(`  FAIL ${pid} ${sampleSku}: ${r.status} ${JSON.stringify(r.j).slice(0, 160)}`);
+      auditOut.write(JSON.stringify({ pid, sampleSku, action: 'ERR', status: r.status, body: r.j }) + '\n');
+      failed++; await sleep(500); continue;
+    }
+    const nv = r.j.variant;
+    // Reversibility recorded AFTER create (undo = delete this exact new variant).
+    restoreOut.write(JSON.stringify({ pid, new_variant_id: nv.id, sku: nv.sku, created_at: new Date().toISOString() }) + '\n');
+    auditOut.write(JSON.stringify({ pid, title: it.title, new_variant_id: nv.id, sku: nv.sku, price: nv.price,
+      inv_mgmt: nv.inventory_management, inv_policy: nv.inventory_policy, action: 'ADDED' }) + '\n');
+    ledgerOut.write(JSON.stringify({ ts: new Date().toISOString(), agent: 'vp-dw-commerce', ticket: 'TK-10867',
+      action: `add Sample variant ${nv.sku} to GB product ${pid}`, blast_radius: 1,
+      undo_cmd: `curl -s -X DELETE "https://${STORE}/admin/api/2024-10/products/${pid}/variants/${nv.id}.json" -H "X-Shopify-Access-Token: $SHOPIFY_FULL_ACCESS_TOKEN"`,
+      verify: `variant ${nv.id} sku=${nv.sku} price=${nv.price} tracked=${nv.inventory_management === null ? 'no' : 'yes'}` }) + '\n');
+    console.log(`  + ${pid} ${nv.sku} $${nv.price} (untracked, policy=${nv.inventory_policy}) [vid ${nv.id}]`);
+    added++;
+    await sleep(400); // ≥ throttle-safe; daily variant cap unaffected (81 < 1k)
+  }
+  if (auditOut) { auditOut.end(); restoreOut.end(); ledgerOut.end(); }
+  console.log(`\nDONE: added=${added} skipped=${skipped} failed=${failed} of ${items.length}` +
+    (APPLY ? `\nrestore-map: ${RESTORE}\naudit: ${AUDIT}` : ' · DRY-RUN'));
+  if (failed) process.exit(2);
+}
+main().catch(e => { console.error('FATAL', e); process.exit(1); });

← e653247 TK-10867: read-only live missing-Sample enumerator + verifie  ·  back to Tk 10867 Gb Safe Plan  ·  TK-10867: added Sample variant to 81 GB active products (80 c0f322d →