[object Object]

← back to Dw Yolo Loop

Add read-only Active/Draft gate-integrity audit script (ACTIVE+DRAFT GraphQL paging)

20610f5dea090d40346e93e43ba5308bb3353464 · 2026-06-14 22:23:41 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 20610f5dea090d40346e93e43ba5308bb3353464
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jun 14 22:23:41 2026 -0700

    Add read-only Active/Draft gate-integrity audit script (ACTIVE+DRAFT GraphQL paging)
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 .gitignore               |   1 +
 gate-integrity-audit.mjs | 187 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 188 insertions(+)

diff --git a/.gitignore b/.gitignore
index 1fdebd3..b157c45 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ build/
 uploads/
 *.bak
 .newwall-ref/
+audit.progress.log
diff --git a/gate-integrity-audit.mjs b/gate-integrity-audit.mjs
new file mode 100644
index 0000000..8a84d1f
--- /dev/null
+++ b/gate-integrity-audit.mjs
@@ -0,0 +1,187 @@
+#!/usr/bin/env node
+// Active/Draft gate-integrity audit — READ-ONLY. No writes, no status changes.
+// Scope: ACTIVE + DRAFT only (ARCHIVED skipped per scope; April restore-prep covers it).
+import fs from 'node:fs';
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const VER = '2024-10';
+
+// Load token from secrets .env without sourcing (file has a parse-unsafe line)
+const envTxt = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
+const m = envTxt.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m);
+if (!m) { console.error('no token'); process.exit(1); }
+const TOKEN = m[1].trim();
+
+const URL = `https://${SHOP}/admin/api/${VER}/graphql.json`;
+
+async function gql(query, variables) {
+  for (let attempt = 0; attempt < 6; attempt++) {
+    const res = await fetch(URL, {
+      method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+      body: JSON.stringify({ query, variables }),
+    });
+    const j = await res.json();
+    if (j.errors) {
+      const throttled = JSON.stringify(j.errors).includes('THROTTLED');
+      if (throttled) { await sleep(2000 * (attempt + 1)); continue; }
+      throw new Error(JSON.stringify(j.errors));
+    }
+    // gentle pacing based on remaining cost budget
+    const t = j.extensions?.cost?.throttleStatus;
+    if (t && t.currentlyAvailable < 500) await sleep(1500);
+    return j.data;
+  }
+  throw new Error('exhausted retries (throttle)');
+}
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
+const QUERY = `
+query($cursor: String, $q: String!) {
+  products(first: 50, after: $cursor, query: $q) {
+    pageInfo { hasNextPage endCursor }
+    nodes {
+      id
+      handle
+      title
+      status
+      vendor
+      tags
+      totalInventory
+      mediaCount { count }
+      variants(first: 30) { nodes { sku price } }
+      widthGlobal: metafield(namespace: "global", key: "width") { value }
+      widthCustom: metafield(namespace: "custom", key: "width") { value }
+      widthDwc: metafield(namespace: "dwc", key: "width") { value }
+    }
+  }
+}`;
+
+function hasWidth(p) {
+  const v = (p.widthGlobal?.value || p.widthCustom?.value || p.widthDwc?.value || '').trim();
+  return v.length > 0;
+}
+function hasImage(p) {
+  return (p.mediaCount?.count || 0) > 0;
+}
+// derive a base SKU for sample detection: find a non-sample variant SKU
+function variantSkus(p) {
+  return (p.variants?.nodes || []).map(v => (v.sku || '').trim()).filter(Boolean);
+}
+function hasSampleVariant(p) {
+  const skus = variantSkus(p);
+  // any variant whose sku ends with -Sample (case-insensitive)
+  return skus.some(s => /-sample$/i.test(s));
+}
+function looksDiscontinued(p) {
+  const t = (p.tags || []).map(x => x.toLowerCase());
+  const blob = (t.join(' ') + ' ' + (p.title || '').toLowerCase());
+  return /discontinu|disco\b|no longer avail|obsolete/.test(blob);
+}
+
+async function pageStatus(status, out) {
+  let cursor = null, hasNext = true, n = 0;
+  const q = `status:${status}`;
+  while (hasNext) {
+    const data = await gql(QUERY, { cursor, q });
+    const page = data.products;
+    for (const p of page.nodes) {
+      out.push(p);
+      n++;
+    }
+    hasNext = page.pageInfo.hasNextPage;
+    cursor = page.pageInfo.endCursor;
+    if (n % 500 === 0) process.stderr.write(`  ${status}: ${n}\n`);
+  }
+  process.stderr.write(`  ${status}: DONE ${n}\n`);
+  return n;
+}
+
+(async () => {
+  const all = [];
+  process.stderr.write('Paging ACTIVE...\n');
+  const nActive = await pageStatus('ACTIVE', all);
+  process.stderr.write('Paging DRAFT...\n');
+  const nDraft = await pageStatus('DRAFT', all);
+
+  const buckets = {
+    draft_gate_clean: [],        // DRAFT w/ image+width -> reactivation candidate
+    active_missing_image: [],     // ACTIVE no image
+    active_missing_width: [],     // ACTIVE no width metafield
+    active_missing_sample: [],    // ACTIVE no {SKU}-Sample variant
+    draft_looks_discontinued: [], // DRAFT that should be ARCHIVED
+  };
+
+  const vc = (obj, k) => { obj[k] = (obj[k] || 0) + 1; };
+  const vendorTally = {
+    draft_gate_clean: {},
+    active_missing_image: {},
+    active_missing_width: {},
+    active_missing_sample: {},
+    draft_looks_discontinued: {},
+  };
+
+  for (const p of all) {
+    const row = {
+      id: p.id.split('/').pop(),
+      handle: p.handle,
+      title: p.title,
+      vendor: p.vendor || '(none)',
+      status: p.status,
+      images: p.mediaCount?.count || 0,
+      width: hasWidth(p),
+      hasSample: hasSampleVariant(p),
+      skus: variantSkus(p),
+      tags: p.tags,
+    };
+    if (p.status === 'DRAFT') {
+      if (hasImage(p) && hasWidth(p) && !looksDiscontinued(p)) {
+        buckets.draft_gate_clean.push(row); vc(vendorTally.draft_gate_clean, row.vendor);
+      }
+      if (looksDiscontinued(p)) {
+        buckets.draft_looks_discontinued.push(row); vc(vendorTally.draft_looks_discontinued, row.vendor);
+      }
+    } else if (p.status === 'ACTIVE') {
+      if (!hasImage(p)) { buckets.active_missing_image.push(row); vc(vendorTally.active_missing_image, row.vendor); }
+      if (!hasWidth(p)) { buckets.active_missing_width.push(row); vc(vendorTally.active_missing_width, row.vendor); }
+      if (!hasSampleVariant(p)) { buckets.active_missing_sample.push(row); vc(vendorTally.active_missing_sample, row.vendor); }
+    }
+  }
+
+  const topVendors = (obj, n = 15) =>
+    Object.entries(obj).sort((a, b) => b[1] - a[1]).slice(0, n).map(([v, c]) => ({ vendor: v, count: c }));
+
+  const result = {
+    generated_at: new Date().toISOString(),
+    scope: 'ACTIVE + DRAFT (ARCHIVED skipped per scope; April restore-prep covers archived)',
+    shop: SHOP,
+    api_version: VER,
+    read_only: true,
+    counts: {
+      scanned_active: nActive,
+      scanned_draft: nDraft,
+      total_scanned: all.length,
+      draft_gate_clean: buckets.draft_gate_clean.length,
+      active_missing_image: buckets.active_missing_image.length,
+      active_missing_width: buckets.active_missing_width.length,
+      active_missing_sample: buckets.active_missing_sample.length,
+      draft_looks_discontinued: buckets.draft_looks_discontinued.length,
+    },
+    top_vendors: {
+      draft_gate_clean: topVendors(vendorTally.draft_gate_clean),
+      active_missing_image: topVendors(vendorTally.active_missing_image),
+      active_missing_width: topVendors(vendorTally.active_missing_width),
+      active_missing_sample: topVendors(vendorTally.active_missing_sample),
+      draft_looks_discontinued: topVendors(vendorTally.draft_looks_discontinued),
+    },
+    rows: buckets,
+  };
+
+  const outPath = process.env.HOME + '/.claude/yolo-queue/gate-integrity-audit-2026-06-14.json';
+  fs.mkdirSync(process.env.HOME + '/.claude/yolo-queue', { recursive: true });
+  fs.writeFileSync(outPath, JSON.stringify(result, null, 2));
+  process.stderr.write('WROTE ' + outPath + '\n');
+  // emit summary counts to stdout
+  console.log(JSON.stringify(result.counts, null, 2));
+  console.log('TOP draft_gate_clean:', JSON.stringify(result.top_vendors.draft_gate_clean.slice(0,8)));
+})();

← d086ddd Add fail-closed pre-flight validator (rules.json + preflight  ·  back to Dw Yolo Loop  ·  brands page: consolidate Scalamandré to single /collections 451c923 →