[object Object]

← back to Rebel Walls Push

Add read-only confirm for soft roll/bolt cohort (TK-10029 DTD verdict C): flags mural-typed products hiding under a Single Roll label

3fda4bb80aefd4d1ba130db345339d78b3cc2b84 · 2026-09-03 13:56:13 -0700 · Steve Abrams

Files touched

Diff

commit 3fda4bb80aefd4d1ba130db345339d78b3cc2b84
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 3 13:56:13 2026 -0700

    Add read-only confirm for soft roll/bolt cohort (TK-10029 DTD verdict C): flags mural-typed products hiding under a Single Roll label
---
 scripts/confirm-rollbolt.js | 85 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 85 insertions(+)

diff --git a/scripts/confirm-rollbolt.js b/scripts/confirm-rollbolt.js
new file mode 100644
index 0000000..94ccc54
--- /dev/null
+++ b/scripts/confirm-rollbolt.js
@@ -0,0 +1,85 @@
+#!/usr/bin/env node
+/**
+ * TK-10029 (DTD verdict C): READ-ONLY positive confirmation of the ~130 "soft"
+ * roll/bolt products (Single Roll + Roll) — the cohort that rests on title-silence
+ * inference, NOT on a bolt-SKU string. For each, pull productType + every variant's
+ * price / availableForSale / selectedOptions and FLAG anomalies:
+ *   - no sellable variant with a real (>0) price  -> possible unbuyable listing
+ *   - a per-m² / "mural (per m" option value present -> a mural hiding under a roll label
+ *   - productType that says "mural"                -> mis-categorized
+ * No writes. $0.  Usage: node scripts/confirm-rollbolt.js
+ */
+const https = require('https');
+const fs = require('fs');
+const SECRETS_ENV = '/Users/macstudio3/Projects/secrets-manager/.env';
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API_VERSION = '2024-10';
+const TOKEN = (() => { const e = fs.readFileSync(SECRETS_ENV, 'utf8'); const m = e.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m); if (!m) throw new Error('token'); return m[1].trim(); })();
+const SOFT_LABELS = new Set(['single roll', 'roll']); // the 130 non-bolt-SKU cohort
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function gqlOnce(q, v) {
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify({ query: q, variables: v || {} });
+    const req = https.request({ hostname: DOMAIN, path: `/admin/api/${API_VERSION}/graphql.json`, method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
+      res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error(`non-JSON ${res.statusCode}`)); } }); });
+    req.on('error', reject); req.write(body); req.end();
+  });
+}
+async function gql(q, v) { let last; for (let a = 1; a <= 8; a++) { try { const r = await gqlOnce(q, v); if (r && r.data) return r; last = new Error('no data: ' + JSON.stringify(r.errors || r).slice(0, 100)); } catch (e) { last = e; } if (a < 8) await sleep(Math.min(10000, 500 * 2 ** (a - 1))); } throw last; }
+
+async function fetchAll() {
+  const out = []; let cursor = null;
+  while (true) {
+    const r = await gql(`query($after:String){ products(first:60, query:"vendor:Rebel Walls", after:$after){
+      pageInfo{hasNextPage endCursor}
+      edges{ node{ id title productType
+        variants(first:15){ edges{ node{ sku availableForSale price selectedOptions{ name value } } } } } } } }`, { after: cursor });
+    const p = r.data.products;
+    for (const e of p.edges) out.push(e.node);
+    if (!p.pageInfo.hasNextPage) break;
+    cursor = p.pageInfo.endCursor; await sleep(300);
+  }
+  return out;
+}
+
+(async () => {
+  const all = await fetchAll();
+  const soft = [];
+  for (const p of all) {
+    const tl = p.title.toLowerCase();
+    if (tl.includes(' sample') || tl.endsWith('sample')) continue;
+    const vals = p.variants.edges.map(e => e.node).flatMap(v => v.selectedOptions.map(o => o.value.toLowerCase()));
+    if (vals.some(x => SOFT_LABELS.has(x))) soft.push(p);
+  }
+  console.log(`[confirm] soft cohort (Single Roll/Roll): ${soft.length} products\n`);
+
+  let clean = 0; const flags = [];
+  for (const p of soft) {
+    const vs = p.variants.edges.map(e => e.node);
+    const sellablePriced = vs.filter(v => v.availableForSale && parseFloat(v.price) > 0);
+    const anySellable = vs.some(v => v.availableForSale);
+    const anyPriced = vs.some(v => parseFloat(v.price) > 0);
+    const muralOpt = vs.some(v => v.selectedOptions.some(o => /mural \(per m|per m²|per square met|sold per square/i.test(o.value)));
+    const muralType = /mural/i.test(p.productType || '');
+    const issues = [];
+    if (!sellablePriced.length) issues.push(`NO sellable+priced variant (sellable=${anySellable}, priced=${anyPriced})`);
+    if (muralOpt) issues.push('per-m²/mural OPTION value present under roll label');
+    if (muralType) issues.push(`productType="${p.productType}"`);
+    if (issues.length) flags.push({ title: p.title, sku: vs[0]?.sku, issues });
+    else clean++;
+  }
+
+  console.log(`[confirm] CLEAN (real roll/bolt, sellable, no mural variant): ${clean}/${soft.length}`);
+  if (flags.length) {
+    console.log(`\n[confirm] ⚠ ${flags.length} FLAGGED for a look:`);
+    for (const f of flags) console.log(`  ${(f.sku || '?').padEnd(14)} ${f.title}\n      → ${f.issues.join(' | ')}`);
+  } else {
+    console.log(`\n[confirm] ✅ ZERO anomalies — every soft-cohort product is a genuine, sellable roll/bolt listing with no per-m² variant hiding under it.`);
+  }
+  // productType histogram for context
+  const types = {}; for (const p of soft) types[p.productType || '(none)'] = (types[p.productType || '(none)'] || 0) + 1;
+  console.log('\n[confirm] productType distribution across soft cohort:');
+  for (const [k, v] of Object.entries(types).sort((a, b) => b[1] - a[1])) console.log(`  ${String(v).padStart(4)}  ${k}`);
+})().catch(e => { console.error('[confirm] FATAL', e.message); process.exit(1); });

← d710728 Add read-only roll/bolt audit (TK-10029): group the 280 held  ·  back to Rebel Walls Push  ·  Relabel 2 mural-typed Rebel Walls products Single Roll -> Mu 5688941 →