← 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
M scripts/audit_pr_dupes.jsA scripts/audit_pr_rescope.js
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 →