[object Object]

← back to Designer Wallcoverings

PR re-scope audit (prefix→line) + sellable/sample breakdown in dedup audit

921c8ce34bcb5eac4d2b4af6ba3ff200142578c3 · 2026-07-06 14:07:59 -0700 · Steve

Files touched

Diff

commit 921c8ce34bcb5eac4d2b4af6ba3ff200142578c3
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 6 14:07:59 2026 -0700

    PR re-scope audit (prefix→line) + sellable/sample breakdown in dedup audit
---
 scripts/audit_pr_dupes.js   | 10 ++++++++
 scripts/audit_pr_rescope.js | 56 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 66 insertions(+)

diff --git a/scripts/audit_pr_dupes.js b/scripts/audit_pr_dupes.js
index 9f58d929..4a230037 100644
--- a/scripts/audit_pr_dupes.js
+++ b/scripts/audit_pr_dupes.js
@@ -59,6 +59,13 @@ async function getPage(url) {
   }
   dupGroups.sort((a, b) => b.count - a.count);
 
+  const isSample = (p) => /memo sample/i.test(p.title) || /-sample$/i.test(primarySku(p));
+  const activeSellable = all.filter(p => p.status === 'active' && !isSample(p));
+  const skuPrefix = (sku) => (sku.match(/^[A-Za-z]+/) || ['?'])[0].toUpperCase();
+  const prefixHist = {};
+  for (const p of activeSellable) { const k = skuPrefix(primarySku(p)); prefixHist[k] = (prefixHist[k] || 0) + 1; }
+  const topPrefixes = Object.entries(prefixHist).sort((a, b) => b[1] - a[1]).slice(0, 25);
+
   const summary = {
     total_products: all.length,
     distinct_skus: bySku.size,
@@ -66,6 +73,9 @@ async function getPage(url) {
     sku_dup_groups: dupGroups.length,
     deletable_excess: deletable,
     status_breakdown: all.reduce((m, p) => (m[p.status] = (m[p.status] || 0) + 1, m), {}),
+    memo_sample_products: all.filter(isSample).length,
+    active_sellable_nonsample: activeSellable.length,
+    active_sellable_by_sku_prefix: Object.fromEntries(topPrefixes),
     top20: dupGroups.slice(0, 20).map(g => ({ sku: g.sku, count: g.count, title: g.keep_title })),
   };
   const outDir = path.join(__dirname, '..', 'output');
diff --git a/scripts/audit_pr_rescope.js b/scripts/audit_pr_rescope.js
new file mode 100644
index 00000000..506c1007
--- /dev/null
+++ b/scripts/audit_pr_rescope.js
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+/**
+ * READ-ONLY re-scope audit for vendor=Phillipe Romano.
+ * Groups ACTIVE, SELLABLE (non-sample) products by SKU prefix (~= book/line),
+ * showing count + representative titles + product types so Steve can flag which
+ * lines do NOT belong under the Phillipe Romano vendor. Makes NO writes.
+ * Output: output/pr-rescope.json + output/pr-rescope.csv
+ */
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const fs = require('fs'); const path = require('path');
+const SHOP = process.env.SHOPIFY_STORE_DOMAIN;
+const TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN;
+const VER = process.env.SHOPIFY_ADMIN_API_VERSION || '2024-01';
+const BASE = `https://${SHOP}/admin/api/${VER}`;
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
+async function getPage(url) {
+  const res = await fetch(url, { headers: { 'X-Shopify-Access-Token': TOKEN } });
+  if (!res.ok) throw new Error(`${res.status}: ${(await res.text()).slice(0, 200)}`);
+  const link = res.headers.get('Link') || ''; let next = null;
+  if (link.includes('rel="next"')) for (const part of link.split(','))
+    if (part.includes('rel="next"')) next = part.split(';')[0].trim().replace(/[<>]/g, '');
+  return { products: (await res.json()).products, next };
+}
+const primarySku = (p) => { const vs = p.variants || []; const m = vs.find(v => v.sku && !/-sample$/i.test(v.sku)) || vs[0]; return (m && m.sku || '').trim(); };
+const isSample = (p) => /memo sample/i.test(p.title) || /-sample$/i.test(primarySku(p));
+const prefix = (sku) => (sku.match(/^[A-Za-z]+/) || ['?'])[0].toUpperCase();
+
+(async () => {
+  let url = `${BASE}/products.json?vendor=Phillipe%20Romano&limit=250&fields=id,title,product_type,tags,status,variants`;
+  const all = [];
+  while (url) { const { products, next } = await getPage(url); all.push(...products); url = next; await sleep(120); }
+  const sellable = all.filter(p => p.status === 'active' && !isSample(p));
+
+  const groups = {};
+  for (const p of sellable) {
+    const k = prefix(primarySku(p));
+    (groups[k] ||= { prefix: k, count: 0, titles: [], types: {} });
+    const g = groups[k]; g.count++;
+    if (g.titles.length < 3) g.titles.push(p.title.replace(/ \| Phillipe Romano$/, ''));
+    const t = p.product_type || '(none)'; g.types[t] = (g.types[t] || 0) + 1;
+  }
+  const rows = Object.values(groups).sort((a, b) => b.count - a.count)
+    .map(g => ({ ...g, top_type: Object.entries(g.types).sort((a, b) => b[1] - a[1])[0][0] }));
+
+  const outDir = path.join(__dirname, '..', 'output'); fs.mkdirSync(outDir, { recursive: true });
+  fs.writeFileSync(path.join(outDir, 'pr-rescope.json'), JSON.stringify({ active_sellable: sellable.length, prefixes: rows }, null, 2));
+  fs.writeFileSync(path.join(outDir, 'pr-rescope.csv'),
+    ['prefix,count,top_product_type,sample_titles',
+      ...rows.map(g => `${g.prefix},${g.count},"${g.top_type}","${g.titles.join(' | ').replace(/"/g, "'")}"`)].join('\n'));
+
+  console.log(`ACTIVE SELLABLE (non-sample): ${sellable.length}  across ${rows.length} SKU-prefix lines\n`);
+  console.log('PREFIX      COUNT  TOP TYPE                     SAMPLE TITLES');
+  for (const g of rows) console.log(
+    `${g.prefix.padEnd(10)} ${String(g.count).padStart(5)}  ${(g.top_type || '').slice(0, 26).padEnd(26)}  ${g.titles.join(' · ').slice(0, 70)}`);
+})().catch(e => { console.error('FATAL', e); process.exit(1); });

← 6cad6ab6 Add DWTT double-roll min/step + dims metafield writer (dry-r  ·  back to Designer Wallcoverings  ·  Carl Robinson: enforce 8-yard increments on all 67 live SKUs 3d5ae387 →