← back to Imageless Rescue

scripts/parse-active-audit.js

74 lines

#!/usr/bin/env node
'use strict';
// Parse an already-downloaded bulk JSONL of ACTIVE products and run the
// activation gate. READ-ONLY. Usage: node parse-active-audit.js <jsonl-path>
const fs = require('fs'); const os = require('os'); const path = require('path'); const readline = require('readline');
const { validateBeforeActivate } = require(path.join(os.homedir(),
  'Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js'));
const JSONL = process.argv[2] || path.join(__dirname,'..','data','active-bulk-20260620.jsonl');
const TODAY = new Date().toISOString().slice(0,10);
const OUT = path.join(__dirname,'..','data',`active-gate-violations-${TODAY}.json`);

(async () => {
  const products = new Map();
  await new Promise(resolve => {
    const rl = readline.createInterface({ input: fs.createReadStream(JSONL), crlfDelay: Infinity });
    rl.on('line', line => {
      if (!line.trim()) return;
      let o; try { o = JSON.parse(line); } catch { return; }
      const id = o.id || '';
      if (id.includes('/Product/') && o.title !== undefined) {
        products.set(id, { id, title:o.title, vendor:o.vendor||'(none)', descriptionHtml:o.descriptionHtml||'', images:[], variants:[], metafields:[] });
      } else if (o.__parentId && products.has(o.__parentId)) {
        const p = products.get(o.__parentId);
        if (o.url) p.images.push(o.url);
        else if (o.sku !== undefined || o.price !== undefined) p.variants.push({ sku:o.sku, price:o.price });
        else if (o.namespace !== undefined) p.metafields.push({ namespace:o.namespace, key:o.key, value:o.value });
      }
    });
    rl.on('close', resolve);
  });

  const mfVal = (mfs, ns, key) => { const m = mfs.find(x => x.namespace===ns && x.key===key); return m ? m.value : ''; };
  const violations = []; let activeTotal = 0;
  for (const p of products.values()) {
    activeTotal++;
    const mfs = p.metafields;
    const g = validateBeforeActivate({
      title: p.title, dwSku: '', descriptionHtml: p.descriptionHtml,
      specs: {
        width:  mfVal(mfs,'global','width') || mfVal(mfs,'custom','width'),
        length: mfVal(mfs,'global','length'),
        repeat: mfVal(mfs,'global','repeat'),
        material: mfVal(mfs,'global','material') || mfVal(mfs,'custom','material'),
        unitOfMeasure: mfVal(mfs,'global','unit_of_measure'),
      },
      vendorSpecs: {}, images: p.images, vendorImages: p.images,
      variants: p.variants.length ? p.variants : [{ sku: 'x-Sample' }],
    });
    const types = [];
    if (g.tags.includes('Needs-Specs') || g.tags.includes('Needs-Width')) types.push('specs');
    if (g.tags.includes('Needs-Description')) types.push('description');
    if (g.tags.includes('Needs-Image')) types.push('image');
    if (types.length) violations.push({ id:p.id, title:p.title, vendor:p.vendor, types, reasons:g.reasons.filter(r=>!/sample variant/i.test(r)), images:p.images.length });
  }

  const byVendorType = {}; const byType = { specs:0, description:0, image:0 };
  for (const v of violations) {
    byVendorType[v.vendor] = byVendorType[v.vendor] || { specs:0, description:0, image:0, total:0 };
    for (const t of v.types) { byVendorType[v.vendor][t]++; byType[t]++; }
    byVendorType[v.vendor].total++;
  }
  fs.writeFileSync(OUT, JSON.stringify({ date:TODAY, activeTotal, violationProducts:violations.length, byType, byVendorType, violations }, null, 2));

  console.log(`ACTIVE scanned: ${activeTotal}`);
  console.log(`ACTIVE with >=1 gate violation: ${violations.length}`);
  console.log(`  specs(width)-missing: ${byType.specs} · description-missing/placeholder: ${byType.description} · image-missing: ${byType.image}`);
  console.log('\nVendor x violation (top 30 by total):');
  console.log('vendor'.padEnd(34),'specs'.padStart(6),'desc'.padStart(6),'img'.padStart(6),'total'.padStart(7));
  Object.entries(byVendorType).sort((a,b)=>b[1].total-a[1].total).slice(0,30).forEach(([v,c])=>{
    console.log(v.slice(0,33).padEnd(34),String(c.specs).padStart(6),String(c.description).padStart(6),String(c.image).padStart(6),String(c.total).padStart(7));
  });
  console.log(`\nwrote ${OUT}`);
})();