← back to Dw Yolo Loop

scripts/live-price-canary/live-price-canary.mjs

40 lines

// live-price-canary (c45) — hardened, promotable version of the c38 pilot.
// READ-ONLY storefront crawl: the Mac2 mirror price col is unreliable (c37), so
// measure TRUE customer-visible price live. Adds retry/backoff (the c38 pilot had
// ~20% UNKNOWN from throttling) + writes data/latest.json (c34 heartbeat shape).
// Classes per product: HAS_REAL (variant > $4.25) / SAMPLE_ONLY (only $4.25) /
// NO_PRICE (blank|all 0). FAIL if NO_PRICE rate spikes (a real blank-price
// regression the mirror can't catch). $0.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
const N=parseInt(process.argv[2]||'250',10);
const NOPRICE_WARN=parseFloat(process.argv[3]||'3');   // % NO_PRICE → WARN
const NOPRICE_FAIL=parseFloat(process.argv[4]||'8');   // % NO_PRICE → FAIL
const PSQL='/opt/homebrew/opt/postgresql@14/bin/psql', DB='postgresql:///dw_unified?host=/tmp';
const DIR=`${process.env.HOME}/Projects/designerwallcoverings/scripts/live-price-canary/data`;
const Q=`${process.env.HOME}/.claude/yolo-queue`;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const rows=execFileSync(PSQL,[DB,'-At','-F','\t','-c',
  `select handle, vendor from shopify_products where status='ACTIVE' and handle is not null and handle<>'' order by random() limit ${N}`],{encoding:'utf8'}).trim().split('\n').filter(Boolean).map(l=>l.split('\t'));
async function getj(h){ for(let a=0;a<5;a++){ try{ const r=await fetch(`https://www.designerwallcoverings.com/products/${encodeURIComponent(h)}.json`,{signal:AbortSignal.timeout(12000)}); if(r.status===200) return await r.json(); if(r.status===404) return {__404:true}; if(r.status===429){await sleep(1500*(a+1));continue;} }catch(e){ await sleep(1200*(a+1)); } } return null; }
const b={HAS_REAL:0,SAMPLE_ONLY:0,NO_PRICE:0,PDP_404:0,UNKNOWN:0}; const noprice=[]; let decided=0;
for(const [h,v] of rows){
  const j=await getj(h);
  if(j===null){b.UNKNOWN++; await sleep(140); continue;}
  if(j.__404){b.PDP_404++; await sleep(140); continue;}
  const prices=(j.product?.variants||[]).map(x=>parseFloat(x.price)).filter(n=>!isNaN(n));
  let cls; if(!prices.length||prices.every(p=>p===0)){cls='NO_PRICE'; if(noprice.length<10)noprice.push(`${h} [${v}]`);}
  else if(prices.every(p=>p<=4.25)) cls='SAMPLE_ONLY'; else cls='HAS_REAL';
  b[cls]++; decided++; await sleep(140);
}
const pct=x=>decided?Math.round(1000*x/decided)/10:0;
const noPricePct=pct(b.NO_PRICE);
const verdict = noPricePct>=NOPRICE_FAIL?'FAIL':noPricePct>=NOPRICE_WARN?'WARN':'PASS';
const out={generated_at:new Date().toISOString(), status:verdict, headline:`NO_PRICE ${noPricePct}% · SAMPLE_ONLY ${pct(b.SAMPLE_ONLY)}% · HAS_REAL ${pct(b.HAS_REAL)}% (n=${decided})`,
  sampled:rows.length, decided, buckets:b, pct:{HAS_REAL:pct(b.HAS_REAL),SAMPLE_ONLY:pct(b.SAMPLE_ONLY),NO_PRICE:noPricePct}, noprice_examples:noprice, thresholds:{warn:NOPRICE_WARN,fail:NOPRICE_FAIL}};
fs.writeFileSync(`${DIR}/latest.json`, JSON.stringify(out,null,2));
fs.writeFileSync(`${Q}/live-price-canary-2026-06-16.json`, JSON.stringify(out,null,2));
console.log(`[live-price-canary] ${verdict} · sampled=${rows.length} decided=${decided} UNKNOWN=${b.UNKNOWN} | ${out.headline}`);
console.log(`  latest.json: ${DIR}/latest.json`);
process.exit(verdict==='FAIL'?2:0);