← back to Dw Yolo Loop
scripts/zero-dollar-orderable-canary/zero-dollar-orderable-canary.mjs
141 lines
// zero-dollar-orderable-canary — READ-ONLY, $0. Cycle 78 (DTD-B unanimous).
// A DURABLE standing guard for the most SEVERE class this run found: orderable zero-dollar
// variants = free-checkout (P1; c67-c71 bounded 145 live). NO standing detector existed.
// Operationalizes the c75 spec's top line, hardened per c77.
//
// Scans status:active + status:draft (STATUS-AGNOSTIC except archived — archived can't be
// ordered; DRAFT zero-dollar is the latent-activation landmine the c69 officer named).
// Flags ANY variant priced 0 that is ORDERABLE (availableForSale OR inventoryPolicy=CONTINUE),
// ANY position. Groups by vendor + status. HEALTHY = 0 active orderable-zero-dollar.
//
// c77 hardening / discipline:
// - ABORT-ON-PARTIAL: if pagination errors mid-scan, a "0 found" is a FALSE-CLEAN → report
// INCONCLUSIVE, never HEALTHY, on an incomplete scan.
// - COVERAGE GATE / negative control: the known Ralph Lauren zero-dollar set (~111) MUST be
// reproduced; if the scan finds ~0 RL, coverage failed (don't trust a clean result).
// - This is an absolute gate (target count = 0), not a baseline-drift check, so no rerun-
// confirm needed (a $0 DB state isn't a transient network blip; abort-on-partial covers
// the under-count risk). exit 3 on live ACTIVE exposure, 0 if clean, 2 if inconclusive.
// EXCLUDES Phillip Jeffries. Read-only Admin GraphQL; never a mutation.
import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url';
const ENV=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8');
const TOK=((ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]||'').replace(/['"\r]/g,'').trim(); // c78-officer: guard missing token
const GQL='https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
const PJ=/phillip[- ]?jeffries/i;
const MIN_ACTIVE_SCAN=Number(process.env.MIN_ACTIVE_SCAN||50000); // completeness floor (real active ~71.7k); survives the RL reprice (decoupled from $0 content)
const BASE_DIR=path.dirname(fileURLToPath(import.meta.url)); const DATA_DIR=path.join(BASE_DIR,'data');
const ACK_FILE=path.join(BASE_DIR,'ack-baseline.json'); // acknowledged-pending-fix set (c80, c78/c79 officer design)
const ACK_SEED=process.env.ACK_SEED==='1'; // ACK_SEED=1: write current hits as the acknowledged set, exit 0
const SELFTEST=process.env.SELFTEST==='1'; // SELFTEST=1: classify a synthetic dataset (no GraphQL) to prove ack logic
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
// tuple key: handle#position#status — keying on status means a DRAFT-acknowledged $0 that ACTIVATES
// (draft→active) appears as a NEW tuple and fires (latent→live escalation), not silently acknowledged.
const tup=h=>(h.pos||[]).map(p=>`${h.h}#${p}#${h.status}`);
function loadAck(){ try{ return new Set(JSON.parse(fs.readFileSync(ACK_FILE,'utf8')).tuples||[]); }catch{ return null; } }
// SET-DIFFERENCE (NOT count-delta, per c78 officer): a NEW hit = a tuple not in the acknowledged set.
function classify(hits, ack){ const nw=[], ak=[]; for(const h of hits){ for(const t of tup(h)){ (ack&&ack.has(t)?ak:nw).push({...h,tuple:t}); } } return {nw, ak}; }
async function gql(q,v){for(let a=0;a<6;a++){try{const r=await fetch(GQL,{method:'POST',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});if(r.status===429||r.status>=500){await sleep(2000*(a+1));continue;}const j=await r.json();if(j.errors&&JSON.stringify(j.errors).includes('Throttled')){await sleep(2500*(a+1));continue;}return j;}catch(e){await sleep(1500*(a+1));}}return null;}
// c78-officer fix: fetch inventoryQuantity (DENY+stock>0 IS orderable — was a false-clean hole);
// variants(first:100) + pageInfo so a >100-variant product's truncation is OBSERVABLE, not silent.
const Q=`query($cursor:String,$q:String!){ products(first:100, after:$cursor, query:$q){ pageInfo{hasNextPage endCursor} edges{node{handle vendor status variants(first:100){pageInfo{hasNextPage} edges{node{price position inventoryPolicy availableForSale inventoryQuantity}}}}} } }`;
// orderable iff price 0 AND can actually transact: oversell allowed OR has stock OR storefront says available
const isOrderableZero=v=> parseFloat(v.price)===0 && (v.inventoryPolicy==='CONTINUE' || (Number.isFinite(v.inventoryQuantity)&&v.inventoryQuantity>0) || v.availableForSale===true);
async function scanStatus(statusQ){
let cursor=null, pages=0, scanned=0, complete=true, truncated=0; const hits=[];
while(true){
const j=await gql(Q,{cursor,q:statusQ});
if(!j||!j.data){ complete=false; break; }
const c=j.data.products;
for(const e of c.edges){ const n=e.node; if(PJ.test(n.vendor||'')) continue; scanned++;
const vs=(n.variants?.edges||[]).map(x=>x.node);
if(n.variants?.pageInfo?.hasNextPage) truncated++; // >100 variants: a $0 past 100 is unverified
const z=vs.filter(isOrderableZero);
if(z.length){ hits.push({h:n.handle,vendor:n.vendor,status:n.status,n:z.length,pos:z.map(v=>v.position)}); }
}
pages++; if(!c.pageInfo.hasNextPage) break; cursor=c.pageInfo.endCursor;
const cost=j.extensions?.cost?.throttleStatus; if(cost&&cost.currentlyAvailable<800) await sleep(1500); else await sleep(220);
}
return {scanned, pages, complete, truncated, hits};
}
console.log(`=== zero-dollar-orderable-canary (READ-ONLY Admin GraphQL, $0) ===`);
// SELFTEST: prove the ack-baseline set-difference logic without a 10-min GraphQL scan.
if(SELFTEST){
const ack=new Set(['known-a#1#ACTIVE','known-b#2#DRAFT']);
const hits=[{h:'known-a',status:'ACTIVE',pos:[1]},{h:'known-b',status:'DRAFT',pos:[2]},{h:'NEW-import',status:'ACTIVE',pos:[1]}];
const {nw,ak}=classify(hits,ack);
const pass = nw.length===1 && nw[0].h==='NEW-import' && ak.length===2;
console.log(`SELFTEST: acknowledged=${ak.length} (expect 2), NEW=${nw.length} (expect 1: ${nw.map(x=>x.h).join(',')})`);
// also prove draft->active escalation: a draft-acknowledged tuple seen ACTIVE = NEW
const esc=classify([{h:'known-b',status:'ACTIVE',pos:[2]}],ack);
const escPass = esc.nw.length===1;
console.log(`SELFTEST escalation (draft-ack tuple now ACTIVE): NEW=${esc.nw.length} (expect 1) → ${escPass?'fires':'MISS'}`);
console.log(`SELFTEST: ${pass&&escPass?'PASS':'FAIL'}`);
process.exit(pass&&escPass?0:1);
}
if(!TOK){ console.log('🔴 INCONCLUSIVE: SHOPIFY_ADMIN_TOKEN missing — cannot scan'); fs.mkdirSync(DATA_DIR,{recursive:true}); fs.writeFileSync(path.join(DATA_DIR,'latest.json'),JSON.stringify({ts:new Date().toISOString(),verdict:'INCONCLUSIVE',reason:'no token'},null,2)); process.exitCode=2; process.exit(2); }
const act=await scanStatus('status:active');
const dft=await scanStatus('status:draft');
const complete = act.complete && dft.complete;
console.log(`active: scanned ${act.scanned} (${act.pages}p, complete=${act.complete}, >100-var trunc=${act.truncated}) | orderable-$0 hits ${act.hits.length}`);
console.log(`draft: scanned ${dft.scanned} (${dft.pages}p, complete=${dft.complete}, >100-var trunc=${dft.truncated}) | orderable-$0 hits ${dft.hits.length}`);
// COVERAGE GATE decoupled from the remediated content (c78-officer): a scan-completeness FLOOR,
// not an RL fingerprint (RL=111 would brick to INCONCLUSIVE the moment Steve fixes the RL set).
// Trust the result iff the crawl completed AND the active scan saw a plausible full catalog.
const coverageOk = complete && act.scanned>=MIN_ACTIVE_SCAN;
const rlActive=act.hits.filter(h=>/ralph/i.test(h.vendor||'')).length; // informational context only, NOT a gate
console.log(`\nCOVERAGE GATE: complete=${complete} + activeScanned ${act.scanned} >= floor ${MIN_ACTIVE_SCAN} → ${coverageOk?'PASS':'FAIL (scan incomplete/blind)'} [RL $0 = ${rlActive}, info only]`);
if(act.truncated+dft.truncated>0) console.log(` ⚠️ ${act.truncated+dft.truncated} products have >100 variants → a $0 past position 100 is UNVERIFIED (observable, not silent)`);
// vendor/status breakdown
function vb(hits){ const m={}; hits.forEach(h=>{const k=`${h.vendor}|${h.status}`; m[k]=(m[k]||0)+1;}); return Object.entries(m).sort((a,b)=>b[1]-a[1]).slice(0,12); }
console.log(`\nactive orderable-$0 by vendor:`); vb(act.hits).forEach(([k,n])=>console.log(` ${k}: ${n}`));
if(dft.hits.length){ console.log(`DRAFT orderable-$0 (latent activation landmine) by vendor:`); vb(dft.hits).forEach(([k,n])=>console.log(` ${k}: ${n}`)); }
const allHits=[...act.hits,...dft.hits];
fs.mkdirSync(DATA_DIR,{recursive:true});
// ACK_SEED mode: record the CURRENT orderable-$0 set as acknowledged-pending-fix, then exit clean.
// (Only meaningful on a complete scan — never seed off a partial/blind crawl.)
if(ACK_SEED){
if(!coverageOk){ console.log(`\n🔴 refusing to seed off an incomplete scan (coverageOk=false)`); process.exit(2); }
const tuples=allHits.flatMap(tup);
fs.writeFileSync(ACK_FILE, JSON.stringify({ts:new Date().toISOString(),note:'acknowledged-pending-fix orderable-$0 set (c80); alert only on NEW tuples beyond this',count:tuples.length,activeHits:act.hits.length,draftHits:dft.hits.length,tuples},null,2));
console.log(`\nACK_SEED → wrote ${ACK_FILE} with ${tuples.length} acknowledged tuples (${act.hits.length} active + ${dft.hits.length} draft). Re-baseline to 0 after the gated reprice (delete this file + ACK_SEED again, or just delete once fixed).`);
process.exit(0);
}
const ack=loadAck(); // null if not seeded yet
const {nw,ak}=classify(allHits, ack);
const alerts=[];
if(!coverageOk){
alerts.push(`INCONCLUSIVE: scan incomplete/blind (complete=${complete}, activeScanned=${act.scanned} vs floor ${MIN_ACTIVE_SCAN}) — a "0 found" cannot be trusted`);
} else if(ack===null){
// no acknowledged baseline yet → behave as the absolute gate (every hit is "live"); prompts Steve to seed
if(act.hits.length>0) alerts.push(`LIVE FREE-CHECKOUT: ${act.hits.length} ACTIVE orderable zero-dollar (no ack-baseline yet — run once with ACK_SEED=1 to acknowledge the known set, then this alerts only on NEW)`);
if(dft.hits.length>0) alerts.push(`LATENT: ${dft.hits.length} DRAFT orderable zero-dollar`);
} else {
// SET-DIFFERENCE: loud only on NEW (a tuple not acknowledged = a regression / bad import / draft→active escalation)
const newActive=nw.filter(x=>x.status==='ACTIVE'), newDraft=nw.filter(x=>x.status!=='ACTIVE');
if(newActive.length>0) alerts.push(`NEW LIVE FREE-CHECKOUT: ${newActive.length} orderable zero-dollar NOT in the acknowledged set (regression / bad import / draft→active) — e.g. ${newActive.slice(0,5).map(x=>x.h).join(', ')}`);
if(newDraft.length>0) alerts.push(`NEW LATENT: ${newDraft.length} draft orderable zero-dollar not acknowledged`);
}
if(coverageOk && act.truncated+dft.truncated>0) alerts.push(`UNVERIFIED: ${act.truncated+dft.truncated} products >100 variants — $0 past position 100 not checked`);
let verdict;
if(!coverageOk) verdict='INCONCLUSIVE';
else if(alerts.some(a=>/^NEW|^LIVE FREE-CHECKOUT|^LATENT/.test(a))) verdict='ALERT';
else verdict='HEALTHY';
console.log(`\nacknowledged-pending-fix: ${ack?ak.length:'(none — not seeded)'} | NEW (beyond acknowledged): ${ack?nw.length:'n/a'}`);
console.log(`=== VERDICT: ${verdict} ===`);
if(alerts.length) alerts.forEach(a=>console.log(` 🔴 ${a}`)); else console.log(` 🟢 0 NEW orderable zero-dollar beyond the acknowledged set (${ack?ak.length:0} known-open, quietly tracked)`);
const result={ts:new Date().toISOString(),verdict,activeScanned:act.scanned,draftScanned:dft.scanned,complete,coverageOk,rlActive,activeHits:act.hits.length,draftHits:dft.hits.length,acknowledged:ack?ak.length:null,newBeyondAck:ack?nw.length:null,newSample:nw.slice(0,15),activeByVendor:vb(act.hits),draftByVendor:vb(dft.hits),alerts};
fs.writeFileSync('/tmp/zero-dollar-orderable-canary.json',JSON.stringify(result,null,2));
fs.writeFileSync(path.join(DATA_DIR,'latest.json'),JSON.stringify(result,null,2)); // heartbeat for meta-watchdog
console.log(`\nwrote /tmp/zero-dollar-orderable-canary.json + data/latest.json`);
process.exitCode = verdict==='HEALTHY'?0 : verdict==='INCONCLUSIVE'?2 : 3;