← 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}`);
})();