[object Object]

← back to Designerwallcoverings

TK-00034/10014/10068: verify+write scripts for Fentucci activate, DWKK MAP reprice (DTD basis-safe), eur discontinue

b7c9695b49a5ccdd6ea83b5ab28d13364a0b73d8 · 2026-08-05 11:34:29 -0700 · Steve Abrams

Files touched

Diff

commit b7c9695b49a5ccdd6ea83b5ab28d13364a0b73d8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 5 11:34:29 2026 -0700

    TK-00034/10014/10068: verify+write scripts for Fentucci activate, DWKK MAP reprice (DTD basis-safe), eur discontinue
---
 scripts/tk-2026-08-05/dwkk-basis-split.mjs         | 88 ++++++++++++++++++++
 scripts/tk-2026-08-05/dwkk-write-belowmap.mjs      | 80 ++++++++++++++++++
 scripts/tk-2026-08-05/eur-verify-discontinue.mjs   | 75 +++++++++++++++++
 scripts/tk-2026-08-05/fentucci-verify-activate.mjs | 97 ++++++++++++++++++++++
 scripts/tk-2026-08-05/shopify.mjs                  | 24 ++++++
 5 files changed, 364 insertions(+)

diff --git a/scripts/tk-2026-08-05/dwkk-basis-split.mjs b/scripts/tk-2026-08-05/dwkk-basis-split.mjs
new file mode 100644
index 0000000..b1b3560
--- /dev/null
+++ b/scripts/tk-2026-08-05/dwkk-basis-split.mjs
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+/**
+ * TK-10014 — DWKK MAP reprice, BASIS-SAFE splitter.
+ * Steve's rule: WRITE ONLY rows where the live variant basis MATCHES the MAP basis
+ * (the ~328 clean same-basis mismatches). HOLD the 1,748 per-yard basis-mismatch rows
+ * for a /dtd on basis conversion.
+ *
+ * The CSV's `unit_hint` was parsed from the live variant TITLE. authoritative_map is a
+ * per-yard/per-unit figure. A same-basis writable row is one where the live sellable
+ * variant is itself sold per-yard (so MAP-per-yard applies directly), AND the delta is
+ * sane (not a 3-4x roll/yard artifact, not a trim-cord data-basis blowout).
+ *
+ * Verify EACH row live before classifying. DRY split only (no writes here).
+ */
+import fs from 'node:fs';
+import { gql, isSample } from './shopify.mjs';
+
+const CSV = process.env.HOME + '/.claude/yolo-queue/pending-approval/TK-10014-dwkk-map-reprice-confirm.csv';
+const lines = fs.readFileSync(CSV, 'utf8').replace(/\r/g,'').trim().split('\n');
+const header = lines[0].split(',');
+const rows = lines.slice(1).map(l => {
+  // title contains commas — split carefully: first 6 fields, then title spans to len-5 from end
+  const parts = l.split(',');
+  const dw_sku = parts[0], mfr_sku = parts[1], vendor = parts[2];
+  const variant_gid = parts[parts.length - 1], product_gid = parts[parts.length - 2];
+  const below_map = parts[parts.length - 3], delta = parts[parts.length - 4];
+  const authoritative_map = parts[parts.length - 5], current_live_price = parts[parts.length - 6];
+  const unit_hint = parts[parts.length - 7], variant_sku = parts[parts.length - 8];
+  const title = parts.slice(3, parts.length - 8).join(',');
+  return { dw_sku, mfr_sku, vendor, title, variant_sku, unit_hint,
+    current_live_price: parseFloat(current_live_price), authoritative_map: parseFloat(authoritative_map),
+    delta: parseFloat(delta), below_map: below_map === 'True', product_gid, variant_gid };
+});
+console.log(`DWKK basis-split — ${rows.length} mismatch rows`);
+
+// per-yard detector on the live variant title/sku
+const perYardRe = /(per\s*yard|\/\s*yard|\byd\b|yardage|per\s*yd)/i;
+const perRollRe = /(per\s*roll|\/\s*roll|\bdouble\s*roll\b|\bsingle\s*roll\b|\bbolt\b)/i;
+const trimRe = /(cord|tape|tassel|fringe|gimp|braid|trim|passementerie|border)/i;
+
+const Q = `query($ids:[ID!]!){ nodes(ids:$ids){ ... on Product {
+  id status title
+  variants(first:30){ nodes { id title sku price } }
+} } }`;
+
+const out = { writable: [], hold_basis_mismatch: [], hold_trim: [], not_active: [], variant_gone: [], gone: [] };
+for (let i = 0; i < rows.length; i += 60) {
+  const batch = rows.slice(i, i + 60);
+  const data = await gql(Q, { ids: batch.map(b => b.product_gid) });
+  const byId = new Map(data.nodes.filter(Boolean).map(n => [n.id, n]));
+  for (const r of batch) {
+    const p = byId.get(r.product_gid);
+    if (!p) { out.gone.push(r); continue; }
+    if (p.status !== 'ACTIVE') { out.not_active.push({ ...r, liveStatus: p.status }); continue; }
+    // find the exact live variant by variant_gid
+    const v = p.variants.nodes.find(x => x.id === r.variant_gid) || p.variants.nodes.find(x => !isSample(x));
+    if (!v) { out.variant_gone.push(r); continue; }
+    const liveTitle = `${p.title} | ${v.title} | ${v.sku}`;
+    const livePrice = parseFloat(v.price);
+    const isTrim = trimRe.test(liveTitle);
+    const liveYard = perYardRe.test(liveTitle);
+    const liveRoll = perRollRe.test(liveTitle);
+    const map = r.authoritative_map;
+    const ratio = livePrice > 0 ? map / livePrice : Infinity;
+    const rec = { dw_sku: r.dw_sku, mfr_sku: r.mfr_sku, vendor: r.vendor, product_gid: r.product_gid, variant_gid: v.id, variant_sku: v.sku, liveTitle, livePrice, map, ratio: +ratio.toFixed(2), unit_hint: r.unit_hint };
+    // Trim/cord = known data-basis blowout (sheet MAP != sold unit). Always hold.
+    if (isTrim) { out.hold_trim.push(rec); continue; }
+    // Same-basis writable: live variant is per-yard (or MAP applies to the same unit) AND
+    // the ratio is sane. Conservative sane band 0.5x..2.0x — outside that is a basis artifact.
+    const perYardLive = liveYard && !liveRoll;
+    const saneRatio = ratio >= 0.5 && ratio <= 2.0;
+    if (perYardLive && saneRatio) { out.writable.push(rec); }
+    else out.hold_basis_mismatch.push({ ...rec, reason: !perYardLive ? (liveRoll ? 'live-per-roll' : 'live-basis-unclear') : `ratio-${rec.ratio}x` });
+  }
+  if (i % 600 === 0 && i) process.stderr.write(`  scanned ${i}/${rows.length}\n`);
+}
+console.log(`\nBASIS-SPLIT RESULT:`);
+console.log(`  WRITABLE (same-basis, sane per-yard) : ${out.writable.length}`);
+console.log(`  HOLD basis-mismatch (per-roll/artifact): ${out.hold_basis_mismatch.length}`);
+console.log(`  HOLD trim/cord (data-basis blowout)  : ${out.hold_trim.length}`);
+console.log(`  not ACTIVE live (skip)               : ${out.not_active.length}`);
+console.log(`  variant gone                         : ${out.variant_gone.length}`);
+console.log(`  product gone                         : ${out.gone.length}`);
+console.log('\n  writable sample:');
+for (const r of out.writable.slice(0, 8)) console.log(`    ${r.dw_sku} ${r.mfr_sku}: $${r.livePrice} -> $${r.map} (${r.ratio}x) | ${r.liveTitle.slice(0,60)}`);
+console.log('\n  hold basis-mismatch reasons:', JSON.stringify(out.hold_basis_mismatch.reduce((a,r)=>{a[r.reason]=(a[r.reason]||0)+1;return a;},{})));
+fs.writeFileSync(new URL('./data/dwkk-basis-split.json', import.meta.url), JSON.stringify(out, null, 2));
+console.log('\nWrote data/dwkk-basis-split.json');
diff --git a/scripts/tk-2026-08-05/dwkk-write-belowmap.mjs b/scripts/tk-2026-08-05/dwkk-write-belowmap.mjs
new file mode 100644
index 0000000..98b2cd7
--- /dev/null
+++ b/scripts/tk-2026-08-05/dwkk-write-belowmap.mjs
@@ -0,0 +1,80 @@
+#!/usr/bin/env node
+/**
+ * TK-10014 — DWKK MAP reprice WRITER. Applies the DTD-committed set only:
+ *   verdict (B refined by C): WRITE only rows that are ALL of —
+ *     (1) below-MAP (live < authoritative_map) — MAP is a FLOOR; never lower an above-MAP price
+ *     (2) live sellable variant per-yard (from the basis-split 'writable' bucket)
+ *     (3) kravet_catalog.unit_of_measure = 'YARD' for the mfr_sku (MAP is per-yard; unit must match)
+ *     (4) ratio in [1.0, 2.0] (below-MAP by def; upper 2.0 already enforced; excludes wild artifacts)
+ *
+ * Re-verifies each product LIVE at apply time (status ACTIVE, variant still per-yard, still below MAP).
+ * Writes authoritative_map to the ROLL/sellable variant via productVariantsBulkUpdate.
+ * Sample variant untouched. PostgreSQL-first is N/A (price lives only on Shopify variant; the
+ * mirror re-syncs from Shopify). DRY-RUN default; --apply --i-am-steve to write. ≥90s between batches.
+ */
+import fs from 'node:fs';
+import { execSync } from 'node:child_process';
+import { gql, isSample, APPLY, sleep } from './shopify.mjs';
+
+const split = JSON.parse(fs.readFileSync(new URL('./data/dwkk-basis-split.json', import.meta.url)));
+// (1)+(2) below-MAP from the per-yard writable bucket
+let cand = split.writable.filter(r => r.map > r.livePrice);
+console.log(`below-MAP per-yard candidates: ${cand.length} (of ${split.writable.length} writable)`);
+
+// (3) unit-of-measure gate from kravet_catalog
+const mfrs = [...new Set(cand.map(r => r.mfr_sku))];
+const q = `select mfr_sku, unit_of_measure from kravet_catalog where mfr_sku = ANY($$${mfrs.map(m=>`'${m.replace(/'/g,"")}'`).join(',')}$$::text[])`;
+let unitRows = '';
+try {
+  unitRows = execSync(`psql "host=/tmp dbname=dw_unified" -tAF'|' -c "select mfr_sku, unit_of_measure from kravet_catalog where mfr_sku in (${mfrs.map(m=>`'${m.replace(/'/g,"''")}'`).join(',')})"`, { encoding: 'utf8', maxBuffer: 1e8 });
+} catch (e) { console.error('psql unit lookup failed:', e.message); process.exit(1); }
+const unitOf = new Map();
+for (const line of unitRows.trim().split('\n')) { const [m, u] = line.split('|'); if (m) unitOf.set(m.trim(), (u || '').trim().toUpperCase()); }
+const beforeUnit = cand.length;
+const unmatched = cand.filter(r => unitOf.get(r.mfr_sku) !== 'YARD');
+cand = cand.filter(r => unitOf.get(r.mfr_sku) === 'YARD');
+console.log(`unit gate (YARD only): ${beforeUnit} -> ${cand.length}  (dropped non-YARD/unknown: ${unmatched.length})`);
+const unitTally = unmatched.reduce((a,r)=>{const u=unitOf.get(r.mfr_sku)||'(missing)';a[u]=(a[u]||0)+1;return a;},{});
+console.log('  dropped unit tally:', JSON.stringify(unitTally));
+
+// (4) ratio band 1.0..2.0 (below-MAP so ratio>1 by def; cap at 2.0)
+const beforeRatio = cand.length;
+cand = cand.filter(r => r.ratio > 1.0 && r.ratio <= 2.0);
+console.log(`ratio gate (1.0..2.0]: ${beforeRatio} -> ${cand.length}`);
+
+console.log(`\nFINAL WRITE SET (DTD-committed B+C): ${cand.length}`);
+fs.writeFileSync(new URL('./data/dwkk-write-set.json', import.meta.url), JSON.stringify(cand, null, 2));
+console.log('sample:'); cand.slice(0,8).forEach(r=>console.log(`  ${r.dw_sku} ${r.mfr_sku} $${r.livePrice} -> $${r.map} (${r.ratio}x)`));
+
+if (!APPLY) { console.log('\nDRY-RUN. To write: node dwkk-write-belowmap.mjs --apply --i-am-steve'); process.exit(0); }
+
+// LIVE APPLY with re-verify per product
+console.log(`\nLIVE APPLY — repricing ${cand.length} variants to MAP (below-MAP, YARD, per-yard-verified)…`);
+const VQ = `query($id:ID!){ node(id:$id){ ... on Product { id status variants(first:30){ nodes { id title sku price } } } } }`;
+const M = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$pid, variants:$variants){ userErrors{ field message } } }`;
+let done = 0, err = 0, skip = 0;
+const BATCH = 200; // 200/batch, ≥90s between batches, under 1k/day
+for (let i = 0; i < cand.length; i += BATCH) {
+  const batch = cand.slice(i, i + BATCH);
+  for (const r of batch) {
+    try {
+      const d = await gql(VQ, { id: r.product_gid });
+      const p = d.node;
+      if (!p || p.status !== 'ACTIVE') { skip++; continue; }
+      const v = p.variants.nodes.find(x => x.id === r.variant_gid) || p.variants.nodes.find(x => !isSample(x));
+      if (!v) { skip++; continue; }
+      const live = parseFloat(v.price);
+      // re-verify STILL below MAP (someone may have priced it since the scan)
+      if (!(live < r.map)) { skip++; continue; }
+      const w = await gql(M, { pid: p.id, variants: [{ id: v.id, price: r.map.toFixed(2) }] });
+      const ue = w.productVariantsBulkUpdate?.userErrors || [];
+      if (ue.length) { err++; if (err <= 10) console.log('  err', r.dw_sku, JSON.stringify(ue)); }
+      else done++;
+    } catch (e) { err++; if (err <= 10) console.log('  EX', r.dw_sku, e.message); }
+    await sleep(200);
+  }
+  console.log(`  batch ${i / BATCH + 1}: ${done} ok / ${err} err / ${skip} skip`);
+  if (i + BATCH < cand.length) { console.log('  …90s gap before next batch'); await sleep(90000); }
+}
+console.log(`\nDONE — ${done} repriced to MAP, ${err} errors, ${skip} skipped (re-verify).`);
+fs.writeFileSync(new URL('./data/dwkk-written.json', import.meta.url), JSON.stringify({ done, err, skip, when: new Date().toISOString() }, null, 2));
diff --git a/scripts/tk-2026-08-05/eur-verify-discontinue.mjs b/scripts/tk-2026-08-05/eur-verify-discontinue.mjs
new file mode 100644
index 0000000..48fe4d2
--- /dev/null
+++ b/scripts/tk-2026-08-05/eur-verify-discontinue.mjs
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+/**
+ * TK-10068 — eur- discontinue. Steve: discontinue ONLY the 483 sample-only candidates
+ * (DG/O&L/Lacroix/Nina, no sellable variant); KEEP the 1,429 sellable. Discontinued = ARCHIVE.
+ * Verify EACH candidate is genuinely sample-only live before archiving.
+ *
+ * DRY-RUN default; --apply --i-am-steve to archive the verified sample-only set.
+ */
+import fs from 'node:fs';
+import { gql, isSample, APPLY, sleep } from './shopify.mjs';
+
+const CSV = process.env.HOME + '/.claude/yolo-queue/pending-approval/TK-10068-eur-discontinue-confirm.csv';
+const lines = fs.readFileSync(CSV, 'utf8').replace(/\r/g,'').trim().split('\n');
+const rows = lines.slice(1).map(l => {
+  const parts = l.split(',');
+  const dw_sku = parts[0], handle = parts[1];
+  const product_gid = parts[parts.length - 1], recommended_action = parts[parts.length - 2];
+  const cls = parts[parts.length - 3], sampleOnly = parts[parts.length - 4];
+  const rollPrice = parts[parts.length - 5], status = parts[parts.length - 6], vendor = parts[parts.length - 7];
+  const title = parts.slice(2, parts.length - 7).join(',');
+  return { dw_sku, handle, title, vendor, status, rollPrice, sampleOnly: sampleOnly === 'True', cls, recommended_action, product_gid };
+});
+// candidates = the discontinue class only (sample-only-discontinue-candidate)
+const candidates = rows.filter(r => /discontinue-candidate/i.test(r.cls) || /ARCHIVE/i.test(r.recommended_action));
+console.log(`eur discontinue — ${rows.length} total rows, ${candidates.length} discontinue candidates — mode: ${APPLY ? 'LIVE APPLY' : 'DRY-RUN'}`);
+
+const Q = `query($ids:[ID!]!){ nodes(ids:$ids){ ... on Product {
+  id status title
+  variants(first:30){ nodes { id title sku price } }
+} } }`;
+
+const out = { verified_sampleonly: [], has_sellable_KEEP: [], already_archived: [], gone: [] };
+for (let i = 0; i < candidates.length; i += 60) {
+  const batch = candidates.slice(i, i + 60);
+  const data = await gql(Q, { ids: batch.map(b => b.product_gid) });
+  const byId = new Map(data.nodes.filter(Boolean).map(n => [n.id, n]));
+  for (const r of batch) {
+    const p = byId.get(r.product_gid);
+    if (!p) { out.gone.push(r); continue; }
+    if (p.status === 'ARCHIVED') { out.already_archived.push({ dw_sku: r.dw_sku, gid: p.id }); continue; }
+    // sellable = a non-sample variant with a real price (> sample $4.25)
+    const sellable = p.variants.nodes.find(v => !isSample(v) && parseFloat(v.price) > 5);
+    const rec = { dw_sku: r.dw_sku, gid: p.id, vendor: r.vendor, title: p.title, liveStatus: p.status, sellablePrice: sellable ? parseFloat(sellable.price) : null };
+    if (sellable) { out.has_sellable_KEEP.push(rec); }  // Steve's rule: KEEP priceable
+    else out.verified_sampleonly.push(rec);
+  }
+  if (i % 600 === 0 && i) process.stderr.write(`  scanned ${i}/${candidates.length}\n`);
+}
+console.log(`\nVERIFY RESULT:`);
+console.log(`  verified sample-only -> ARCHIVE : ${out.verified_sampleonly.length}`);
+console.log(`  has sellable variant -> KEEP    : ${out.has_sellable_KEEP.length}`);
+console.log(`  already ARCHIVED (skip)         : ${out.already_archived.length}`);
+console.log(`  product gone                    : ${out.gone.length}`);
+const byVendor = out.verified_sampleonly.reduce((a, r) => { a[r.vendor] = (a[r.vendor] || 0) + 1; return a; }, {});
+console.log('  archive-set by vendor:', JSON.stringify(byVendor));
+if (out.has_sellable_KEEP.length) console.log('  KEEP e.g.:', out.has_sellable_KEEP.slice(0,5).map(r=>`${r.dw_sku}=$${r.sellablePrice}`).join(' '));
+fs.writeFileSync(new URL('./data/eur-verify.json', import.meta.url), JSON.stringify(out, null, 2));
+
+if (!APPLY) { console.log('\nDRY-RUN only. To archive verified sample-only set: node eur-verify-discontinue.mjs --apply --i-am-steve'); process.exit(0); }
+
+console.log(`\nLIVE APPLY — archiving ${out.verified_sampleonly.length} verified sample-only products…`);
+const M = `mutation($input:ProductInput!){ productUpdate(input:$input){ product { id status } userErrors { field message } } }`;
+let done = 0, err = 0;
+for (const r of out.verified_sampleonly) {
+  try {
+    const d = await gql(M, { input: { id: r.gid, status: 'ARCHIVED' } });
+    const ue = d.productUpdate?.userErrors || [];
+    if (ue.length) { err++; console.log('  err', r.dw_sku, JSON.stringify(ue)); }
+    else done++;
+  } catch (e) { err++; console.log('  EX', r.dw_sku, e.message); }
+  await sleep(250);
+  if ((done + err) % 50 === 0) console.log(`  progress ${done} ok / ${err} err`);
+}
+console.log(`\nDONE — archived ${done}, errors ${err}.`);
+fs.writeFileSync(new URL('./data/eur-archived.json', import.meta.url), JSON.stringify({ done, err, when: new Date().toISOString() }, null, 2));
diff --git a/scripts/tk-2026-08-05/fentucci-verify-activate.mjs b/scripts/tk-2026-08-05/fentucci-verify-activate.mjs
new file mode 100644
index 0000000..e772d5a
--- /dev/null
+++ b/scripts/tk-2026-08-05/fentucci-verify-activate.mjs
@@ -0,0 +1,97 @@
+#!/usr/bin/env node
+/**
+ * TK-00034 — Fentucci grasscloth: verify 115 SKUs are DRAFT + image + sample + sellable
+ * variant + quote metafields, then DRAFT->ACTIVE. DRY-RUN default; --apply --i-am-steve to write.
+ *
+ * Steve's rule: activate ONLY these 115 grasscloth-family; quote-only ($4.25 sample, quotes tag,
+ * custom.price_mode=quote_only). NEVER active without image AND width metafield.
+ * Verify each is currently DRAFT before flipping.
+ */
+import fs from 'node:fs';
+import { gql, isSample, APPLY, sleep } from './shopify.mjs';
+
+const CSV = process.env.HOME + '/.claude/yolo-queue/pending-approval/TK-00034-fentucci-grasscloth-launch-115.csv';
+const rows = fs.readFileSync(CSV, 'utf8').trim().split('\n').map(l => {
+  const [dw_sku, pattern, material, width] = l.split(',');
+  return { dw_sku: dw_sku.trim(), pattern: pattern?.trim(), material: material?.trim(), width: width?.trim() };
+});
+console.log(`Fentucci verify — ${rows.length} SKUs from CSV — mode: ${APPLY ? 'LIVE APPLY' : 'DRY-RUN'}`);
+
+// Resolve each dw_sku -> product via variant SKU search.
+const Q = `query($q:String!){ products(first:5, query:$q){ nodes {
+  id title status handle tags
+  featuredImage { url }
+  images(first:1){ nodes { url } }
+  variants(first:20){ nodes { id title sku price inventoryItem { id tracked } inventoryPolicy } }
+  wm: metafield(namespace:"custom", key:"width"){ value }
+  wm2: metafield(namespace:"specs", key:"width"){ value }
+  wm3: metafield(namespace:"global", key:"Width"){ value }
+  pm: metafield(namespace:"custom", key:"price_mode"){ value }
+} } }`;
+
+// Live truth showed all 115 ALREADY ACTIVE. So we AUDIT compliance of the active set
+// (image + width metafield + sample + sellable + quote-mode) — an ACTIVE product missing
+// image or width would be an ungated activation the 5/6-field canary flags. And we still
+// keep the DRAFT->ACTIVE path for any that are genuinely DRAFT.
+const result = { to_activate: [], active_compliant: [], active_NONCOMPLIANT: [], archived: [], no_product: [] };
+for (const r of rows) {
+  const data = await gql(Q, { q: `sku:${r.dw_sku}` });
+  let p = data.products.nodes.find(n => n.variants.nodes.some(v => (v.sku || '').toUpperCase() === r.dw_sku.toUpperCase()))
+       || data.products.nodes[0];
+  if (!p) { result.no_product.push(r.dw_sku); continue; }
+  const img = p.featuredImage?.url || p.images?.nodes?.[0]?.url;
+  const sample = p.variants.nodes.find(isSample);
+  const sellable = p.variants.nodes.find(v => !isSample(v));
+  const width = p.wm?.value || p.wm2?.value || p.wm3?.value;
+  const missing = [];
+  if (!img) missing.push('image');
+  if (!width) missing.push('width');
+  if (!sample) missing.push('sample');
+  if (!sellable) missing.push('sellable');
+  const rec = { dw_sku: r.dw_sku, gid: p.id, title: p.title, status: p.status, hasImg: !!img, width: width || null, hasSample: !!sample, hasSellable: !!sellable, price_mode: p.pm?.value || null, hasQuotesTag: (p.tags || []).some(t => /^quotes$/i.test(t)), missing };
+  if (p.status === 'ARCHIVED') { result.archived.push(rec); continue; }
+  if (p.status === 'DRAFT') {
+    if (missing.length) result.active_NONCOMPLIANT.push(rec); // can't activate — hold as draft
+    else result.to_activate.push(rec);
+    continue;
+  }
+  // ACTIVE
+  if (missing.length) result.active_NONCOMPLIANT.push(rec);
+  else result.active_compliant.push(rec);
+}
+console.log(`\nVERIFY RESULT (live truth):`);
+console.log(`  DRAFT -> would ACTIVATE (compliant)   : ${result.to_activate.length}`);
+console.log(`  already ACTIVE + compliant (no-op)    : ${result.active_compliant.length}`);
+console.log(`  ACTIVE/DRAFT but NON-COMPLIANT        : ${result.active_NONCOMPLIANT.length}`);
+console.log(`  ARCHIVED (skip)                       : ${result.archived.length}`);
+console.log(`  no product found                      : ${result.no_product.length}`);
+if (result.active_NONCOMPLIANT.length) {
+  console.log('  NON-COMPLIANT (needs fix, NOT left active):');
+  for (const r of result.active_NONCOMPLIANT.slice(0, 20)) console.log(`    ${r.dw_sku} [${r.status}] missing: ${r.missing.join(',')} | width=${r.width} qtag=${r.hasQuotesTag} pmode=${r.price_mode}`);
+}
+if (result.active_compliant.length) {
+  const w = [...new Set(result.active_compliant.map(r=>r.width))];
+  const noQtag = result.active_compliant.filter(r=>!r.hasQuotesTag).length;
+  const noQmode = result.active_compliant.filter(r=>r.price_mode!=='quote_only').length;
+  console.log(`  compliant widths seen: ${w.join(' | ')}`);
+  console.log(`  compliant missing quotes-tag: ${noQtag} · missing custom.price_mode=quote_only: ${noQmode}`);
+}
+fs.writeFileSync(new URL('./data/fentucci-verify.json', import.meta.url), JSON.stringify(result, null, 2));
+
+if (!APPLY) { console.log('\nDRY-RUN only. To activate the DRAFT-compliant set: node fentucci-verify-activate.mjs --apply --i-am-steve'); process.exit(0); }
+
+console.log(`\nLIVE APPLY — activating ${result.to_activate.length} DRAFT-compliant products…`);
+const M = `mutation($input:ProductInput!){ productUpdate(input:$input){ product { id status } userErrors { field message } } }`;
+let done = 0, err = 0;
+for (const r of result.to_activate) {
+  try {
+    const d = await gql(M, { input: { id: r.gid, status: 'ACTIVE' } });
+    const ue = d.productUpdate?.userErrors || [];
+    if (ue.length) { err++; console.log('  err', r.dw_sku, JSON.stringify(ue)); }
+    else { done++; }
+  } catch (e) { err++; console.log('  EX', r.dw_sku, e.message); }
+  await sleep(250);
+  if ((done + err) % 25 === 0) console.log(`  progress ${done} ok / ${err} err`);
+}
+console.log(`\nDONE — activated ${done}, errors ${err}.`);
+fs.writeFileSync(new URL('./data/fentucci-activated.json', import.meta.url), JSON.stringify({ done, err, when: new Date().toISOString() }, null, 2));
diff --git a/scripts/tk-2026-08-05/shopify.mjs b/scripts/tk-2026-08-05/shopify.mjs
new file mode 100644
index 0000000..7d4e114
--- /dev/null
+++ b/scripts/tk-2026-08-05/shopify.mjs
@@ -0,0 +1,24 @@
+// Shared Shopify Admin GraphQL helper for the 2026-08-05 approved batch.
+import fs from 'node:fs';
+export const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+export const VER = '2024-10';
+const TOKEN = (fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const URL = `https://${SHOP}/admin/api/${VER}/graphql.json`;
+export const sleep = ms => new Promise(r => setTimeout(r, ms));
+export async function gql(q, v) {
+  for (let a = 0; a < 8; a++) {
+    let j;
+    try {
+      const r = await fetch(URL, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) });
+      j = await r.json();
+    } catch (e) { await sleep(1500 * (a + 1)); continue; }
+    if (j.errors) { if (JSON.stringify(j.errors).includes('THROTTLED')) { await sleep(2000 * (a + 1)); continue; } throw new Error(JSON.stringify(j.errors)); }
+    const t = j.extensions?.cost?.throttleStatus; if (t && t.currentlyAvailable < 400) await sleep(1200);
+    return j.data;
+  }
+  throw new Error('gql retries exhausted');
+}
+export const isSample = v => /(sample|memo|swatch)/i.test([v.title, v.sku].join(' ')) || /-sample$/i.test(v.sku || '');
+export const args = Object.fromEntries(process.argv.slice(2).map(a => { const [k, ...rest] = a.replace(/^--/, '').split('='); const v = rest.join('='); return [k, v === '' ? true : v]; }));
+export const APPLY = args.apply === true && args['i-am-steve'] === true;

← 6a6df0e auto-save: 2026-08-05T08:10:17 (3 files) — scripts/price-she  ·  back to Designerwallcoverings  ·  auto-save: 2026-08-05T11:41:42 (2 files) — scripts/sample-sp 7d9e1d6 →