← back to Dw Settlement Audit
scan-catalog.js
60 lines
#!/usr/bin/env node
/* DW settlement audit — stage 1: cheap text pre-filter.
*
* Paginates all ~122k products and flags any whose title / tags / product_type
* mention banana-leaf / palm / tropical motifs — the settlement-protected
* pattern family. Output (candidates.jsonl) feeds stage 2 (vision verify).
*
* SHOPIFY_ADMIN_TOKEN=... node scan-catalog.js
*/
'use strict';
const fs = require('fs');
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
if (!TOKEN) { console.error('SHOPIFY_ADMIN_TOKEN not set'); process.exit(1); }
// Targeted — banana-leaf / palm / tropical only. Deliberately NOT "leaf/leaves/
// foliage" (too broad — most florals have leaves). Candidates get vision-verified.
const KW = /\b(banana|palm|palms|frond|fronds|tropical|tropics|monstera|jungle|palmette|martinique|plantain|bird of paradise|areca|rainforest|palm leaf|banana leaf)\b/i;
const sleep = ms => new Promise(r => setTimeout(r, ms));
(async () => {
let url = `https://${SHOP}/admin/api/2024-01/products.json?limit=250&fields=id,handle,title,tags,product_type,status,published_at`;
const out = fs.createWriteStream('candidates.jsonl');
let scanned = 0, flagged = 0, page = 0;
while (url) {
let res;
for (let attempt = 0; attempt < 5; attempt++) {
res = await fetch(url, { headers: { 'X-Shopify-Access-Token': TOKEN } });
if (res.status === 429) { await sleep(2000); continue; }
break;
}
const data = await res.json();
for (const p of (data.products || [])) {
scanned++;
const hay = [p.title, p.tags, p.product_type].filter(Boolean).join(' | ');
const m = hay.match(KW);
if (m) {
flagged++;
out.write(JSON.stringify({
id: p.id, handle: p.handle, title: p.title, status: p.status,
published: !!p.published_at, match: m[0].toLowerCase(),
product_type: p.product_type, tags: p.tags,
}) + '\n');
}
}
page++;
fs.writeFileSync('progress.txt', `page ${page} | scanned ${scanned} | flagged ${flagged} | ${new Date().toISOString()}\n`);
const link = res.headers.get('link') || '';
url = null;
for (const part of link.split(',')) {
if (part.includes('rel="next"')) url = part.split(';')[0].trim().replace(/^<|>$/g, '');
}
await sleep(550); // ~2 req/s — within Shopify REST leaky bucket
}
out.end();
fs.writeFileSync('progress.txt', `DONE | scanned ${scanned} | flagged ${flagged} | ${new Date().toISOString()}\n`);
console.log(`scan complete — scanned ${scanned}, flagged ${flagged}`);
})();