← back to Dw Yolo Loop

scripts/enrichment-board/build-enrichment-board.mjs

74 lines

// build-enrichment-board — LOCAL synthesis (no Shopify scan, $0). Joins the
// already-collected per-vendor enrichment audits into ONE ranked vendor×axis
// board the enrichment pipeline can act on directly:
//   price   = # ACTIVE products feeding Google at $4.25 (GMC worklist)   [c-GMC]
//   specs   = % with material+width metafields (any namespace, hardened) [c28]
//   images  = % with >=1 image                                          [c26]
//   color   = % with color_hex/color_details                            [c26]
//   alt     = % with ALL images alt-texted                              [c29]
// Ranks repped vendors by total "products needing work" across axes.
//
//   node build-enrichment-board.mjs
// READ-ONLY (reads local JSON written by prior cycles). $0.
import fs from 'node:fs';
const Q=`${process.env.HOME}/.claude/yolo-queue`;
const mf=JSON.parse(fs.readFileSync(`${Q}/metafield-coverage-2026-06-16.json`,'utf8'));
const alt=JSON.parse(fs.readFileSync(`${Q}/alt-text-coverage-2026-06-16.json`,'utf8'));
const gmc=JSON.parse(fs.readFileSync(`${Q}/gmc-feed-exclude-worklist.json`,'utf8'));

// price gap: count GMC $4.25-on-Google targets per vendor
const priceGap=new Map();
for(const t of (gmc.targets||[])) priceGap.set(t.vendor, (priceGap.get(t.vendor)||0)+1);

// alt lookup by vendor
const altByVendor=new Map(alt.vendors.map(v=>[v.vendor, v]));

// build board for REPPED vendors (from the metafield audit's repped flag)
const PJ=/jeffries/i; // display-excluded — alt/price gaps are by-design, flag but don't rank
const rows=mf.vendors.filter(v=>v.repped===true && v.active_products>=50).map(v=>{
  const a=altByVendor.get(v.vendor)||{};
  const specs=v.specs_pct??0, img=v.img1_pct??0, color=v.color_pct??0;
  const altp=a.all_alt_pct??null, withImg=a.with_image??v.active_products;
  const price=priceGap.get(v.vendor)||0;
  // products-needing-work per axis (absolute), images/color usually ~0
  const specsNeed=Math.round(v.active_products*(100-specs)/100);
  const imgNeed=Math.round(v.active_products*(100-img)/100);
  const colorNeed=Math.round(v.active_products*(100-color)/100);
  const altNeed= altp==null?null:Math.round(withImg*(100-altp)/100);
  const excluded=PJ.test(v.vendor);
  // total work excludes price (separate one-shot GMC fix) and excludes PJ (by-design)
  const totalNeed= excluded?0:(specsNeed+imgNeed+colorNeed+(altNeed||0));
  // biggest single fixable axis
  const axes=[['specs',specsNeed],['images',imgNeed],['color',colorNeed],['alt',altNeed||0]].filter(x=>!excluded);
  axes.sort((a,b)=>b[1]-a[1]);
  return { vendor:v.vendor, active:v.active_products, excluded,
    price_gap:price, specs_pct:specs, images_pct:img, color_pct:color, alt_pct:altp,
    specs_need:specsNeed, alt_need:altNeed, total_need:totalNeed,
    top_fix: excluded?'(display-excluded — by design)':(axes[0]&&axes[0][1]>0?`${axes[0][0]} (${axes[0][1]})`:'—clean—') };
}).sort((a,b)=>b.total_need-a.total_need);

const board={ generated_at:new Date().toISOString(), source:'local synthesis of c26/c28/c29/GMC (no new scan)',
  repped_vendors:rows.length, rows };
fs.writeFileSync(`${Q}/enrichment-board-2026-06-16.json`, JSON.stringify(board,null,2));

const pc=p=>p==null?'—':p+'%';
let md=`# Repped-vendor ENRICHMENT BOARD — ${new Date().toISOString().slice(0,16)}\n\n`;
md+=`**LOCAL synthesis ($0, no new scan)** of four prior-cycle audits into one ranked vendor×axis to-do. `;
md+=`Axes: price=# products feeding Google at $4.25 (GMC) · specs=material+width metafields (c28) · images=≥1 image (c26) · color=hex/details (c26) · alt=all-images-alt'd (c29). Ranked by total products needing work (specs+images+color+alt; price is a separate one-shot GMC fix; Phillip Jeffries excluded from ranking — display-excluded by rule).\n\n`;
md+=`## Ranked board — ${rows.filter(r=>!r.excluded).length} repped vendors\n`;
md+=`| # | Vendor | Active | price $4.25 | specs | images | color | alt | TOP FIX (products) |\n|--:|---|--:|--:|--:|--:|--:|--:|---|\n`;
let i=0;
for(const r of rows){ i++; md+=`| ${i} | ${r.vendor}${r.excluded?' ⚠️':''} | ${r.active} | ${r.price_gap||'—'} | ${pc(r.specs_pct)} | ${pc(r.images_pct)} | ${pc(r.color_pct)} | ${pc(r.alt_pct)} | ${r.top_fix} |\n`; }
md+=`\n## How to read it\n`;
md+=`- **TOP FIX** = the single axis with the most products needing work for that vendor — the highest-leverage backfill to task the enrichment pipeline with first.\n`;
md+=`- **price $4.25** = products whose min variant feeds Google at the sample price (the GMC exclusion set); fixed in one shot by the GMC opt-out + the sample-split remodel, not per-vendor enrichment.\n`;
md+=`- **images/color ~100%** across repped vendors (c26) — already solved; specs + alt are where the real per-vendor work is.\n`;
md+=`- ⚠️ = display-excluded (Phillip Jeffries); its blank alt/price are by-design, excluded from ranking.\n`;
md+=`- All backfills are GATED (prod writes); this board only prioritizes them.\n`;
fs.writeFileSync(`${Q}/enrichment-board-2026-06-16.md`, md);

console.log(`[enrichment-board] repped vendors ranked: ${rows.filter(r=>!r.excluded).length} (+1 excluded)`);
console.log('Top 8 by products-needing-work:');
for(const r of rows.filter(r=>!r.excluded).slice(0,8)) console.log(`  ${(""+r.total_need).padStart(5)} need  ${r.vendor.padEnd(22)} TOP=${r.top_fix}`);
console.log(`Board: ${Q}/enrichment-board-2026-06-16.md`);