← back to Designer Wallcoverings
Add scan-active-dups.js: reusable read-only both-active duplicate scanner (generalizes the Kravet-55 detector across vendor prefixes)
f486e7854059663fb241d9d8cb19cf1bcf698359 · 2026-06-12 09:18:56 -0700 · SteveStudio2
Files touched
A shopify/scripts/scan-active-dups.js
Diff
commit f486e7854059663fb241d9d8cb19cf1bcf698359
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Fri Jun 12 09:18:56 2026 -0700
Add scan-active-dups.js: reusable read-only both-active duplicate scanner (generalizes the Kravet-55 detector across vendor prefixes)
---
shopify/scripts/scan-active-dups.js | 37 +++++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/shopify/scripts/scan-active-dups.js b/shopify/scripts/scan-active-dups.js
new file mode 100644
index 00000000..634cd1f4
--- /dev/null
+++ b/shopify/scripts/scan-active-dups.js
@@ -0,0 +1,37 @@
+#!/usr/bin/env node
+/**
+ * Read-only: scan a DW SKU prefix on Shopify for base-SKUs that have 2+ ACTIVE products
+ * (live duplicates), the Kravet-55 debt class. Reports, never writes.
+ * node scan-active-dups.js DWKK # one vendor prefix
+ * node scan-active-dups.js DWLK DWBY DWYK # several
+ * Writes /tmp/active-dups-<PREFIX>.json per prefix for a follow-on cleanup planner.
+ */
+const https = require('https'); const fs = require('fs'); const os = require('os');
+const TOKEN = (fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function gql(q, v) { return new Promise(res => { const d = JSON.stringify({ query: q, variables: v }); const rq = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) } }, r => { let b = ''; r.on('data', c => b += c); r.on('end', () => { try { res(JSON.parse(b)); } catch { res({}); } }); }); rq.on('error', () => res({})); rq.write(d); rq.end(); }); }
+async function gr(q, v) { for (let a = 0; a < 7; a++) { const r = await gql(q, v); if (r.errors && JSON.stringify(r.errors).includes('THROTTLED')) { await sleep(2500 * (a + 1)); continue; } await sleep(250); return r; } return {}; }
+const base = s => (s || '').replace(/-(sample|roll|yard)$/i, '').trim().toUpperCase();
+const SAMPLE = 4.25;
+
+async function scan(pfx) {
+ const Q = `query($q:String,$after:String){products(first:100,query:$q,after:$after){pageInfo{hasNextPage endCursor} edges{node{id title status createdAt featuredImage{url} variants(first:6){edges{node{sku price}}}}}}}`;
+ const map = {}; let after = null, n = 0;
+ while (true) {
+ const r = await gr(Q, { q: `sku:${pfx}-*`, after }); const p = r?.data?.products; if (!p) break;
+ for (const e of p.edges) { const x = e.node; const vs = x.variants.edges.map(v => v.node); const skus = vs.map(v => v.sku).filter(Boolean); let b = null; for (const s of skus) { if (!/-sample$/i.test(s)) { b = base(s); break; } } if (!b && skus.length) b = base(skus[0]); if (!b) continue; const roll = vs.find(v => !/-sample$/i.test(v.sku || '')); (map[b] = map[b] || []).push({ id: x.id.replace(/.*\//, ''), status: x.status, c: x.createdAt.slice(0, 10), img: !!x.featuredImage, nv: vs.length, roll: roll ? parseFloat(roll.price) : null }); n++; }
+ if (!p.pageInfo.hasNextPage) break; after = p.pageInfo.endCursor;
+ }
+ const bothActive = Object.entries(map).filter(([, v]) => v.filter(x => x.status === 'ACTIVE').length >= 2);
+ fs.writeFileSync(`/tmp/active-dups-${pfx}.json`, JSON.stringify(bothActive.map(([b, v]) => ({ base: b, copies: v })), null, 1));
+ console.log(`${pfx}: ${n} products, ${Object.keys(map).length} distinct base-SKUs, ${bothActive.length} with >=2 ACTIVE copies`);
+ bothActive.slice(0, 6).forEach(([b, v]) => console.log(` DUP ${b}: ` + v.map(x => `${x.id}[${x.status},${x.c},$${x.roll},nv=${x.nv}]`).join(' ')));
+ return { pfx, products: n, dups: bothActive.length };
+}
+(async () => {
+ if (!TOKEN) { console.error('no token'); process.exit(1); }
+ const prefixes = process.argv.slice(2); if (!prefixes.length) { console.error('usage: scan-active-dups.js <PREFIX> [PREFIX...]'); process.exit(1); }
+ const out = []; for (const p of prefixes) out.push(await scan(p));
+ console.log('\n=== SUMMARY ==='); out.forEach(o => console.log(`${o.pfx}: ${o.dups} both-active dup base-SKUs (${o.products} products)`));
+})();
← 3a8a8504 worker-guard: verify backfill table correct (launch-pad gate
·
back to Designer Wallcoverings
·
cadence: vendor colOverrides (Newwall length→roll_length, re 1f1b3a64 →