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