← back to Rebel Walls Push

scripts/confirm-rollbolt.js

86 lines

#!/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); });