← back to Dw Yolo Loop
scripts/inverse-mislabel-disambig/inverse-mislabel-disambig.mjs
76 lines
// inverse-mislabel-disambig — READ-ONLY, $0. Cycle 85. Disambiguates the c84 238 inverse-mislabels
// (single-variant, price<=THRESH, title NOT /sample/i, NOT showroom-tagged) the c84 officer REQUIRED
// before any fix: are these an UNTAGGED Showroom-Lines batch (cheap fix = add the tag/metafield) or
// GENUINE mislabels (fix = SPLIT, unexecutable for PR with no price sheet)?
//
// Read-only signals captured PER product (the officer's exact asks):
// 1. showroom-lines COLLECTION membership — in the collection but missing the tag? => untagged-Showroom
// 2. custom.showroom_line METAFIELD present — has the metafield but missing the tag? => untagged-Showroom
// 3. variant title "Default Title" vs other — separates the 233 core from the 5 outliers
// 4. images count + body_html length — swatch-vs-full-product presentation proxy (a real swatch
// PDP tends to be sparse; a full-product PDP has rich copy/imagery)
// Decision per product: untagged-Showroom (collection OR metafield present) => TAG fix (cheap, safe);
// else genuine-mislabel => SPLIT (gated on real cost; unexecutable for PR). EXCLUDES PJ.
import fs from 'fs';
const ENV=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8');
const TOK=((ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]||'').replace(/['"\r]/g,'').trim();
const GQL='https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
const PJ=/phillip[- ]?jeffries/i;
const THRESH=Number(process.env.THRESH||10);
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(q,v){for(let a=0;a<6;a++){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;}return null;}
const isSample=t=>/sample/i.test(t||'');
// Capture the disambiguation signals. collections(first:30) handles, the showroom_line metafield,
// image count via images(first:11) (cap+1 to observe truncation), body_html length.
const Q=`query($cursor:String){ products(first:60, after:$cursor, query:"status:active"){ pageInfo{hasNextPage endCursor}
edges{node{ id title vendor tags productType
descriptionHtml
metafield(namespace:"custom", key:"showroom_line"){ value }
collections(first:30){ edges{ node{ handle } } }
images(first:11){ edges{ node{ id } } }
variants(first:3){ edges{ node{ title price } } } } } } }`;
let cursor=null,pages=0,scanned=0,complete=true;
let matched=0;
// disposition buckets
let bColl=0, bMeta=0, bCollOrMeta=0, bNeither=0;
const byVendor={}, byVariantTitle={}, sample=[];
const SHOWROOM_COLL=/showroom/i;
while(true){ const j=await gql(Q,{cursor}); if(!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(vs.length!==1)continue; const v=vs[0]; const p=parseFloat(v.price);
if(isNaN(p)||p>THRESH) continue;
if(isSample(n.title)||isSample(v.title)) continue;
if((n.tags||[]).some(t=>/showroom/i.test(t))) continue; // tagged showroom already excluded (the 2,768)
// === this node is one of the c84 238 ===
matched++;
const inShowroomColl=(n.collections?.edges||[]).some(x=>SHOWROOM_COLL.test(x.node.handle||''));
const hasMeta=!!(n.metafield&&n.metafield.value);
const imgN=(n.images?.edges||[]).length; // 11 means >=11 (truncated)
const bodyLen=(n.descriptionHtml||'').replace(/<[^>]+>/g,'').trim().length;
if(inShowroomColl) bColl++;
if(hasMeta) bMeta++;
if(inShowroomColl||hasMeta) bCollOrMeta++; else bNeither++;
byVendor[n.vendor]=byVendor[n.vendor]||{n:0,collOrMeta:0}; byVendor[n.vendor].n++; if(inShowroomColl||hasMeta) byVendor[n.vendor].collOrMeta++;
byVariantTitle[v.title]=(byVariantTitle[v.title]||0)+1;
if(sample.length<40 || !inShowroomColl) { if(sample.length<60) sample.push({t:n.title.slice(0,42),vendor:n.vendor,p,vt:v.title,coll:inShowroomColl,meta:hasMeta,img:imgN,body:bodyLen}); }
}
pages++; if(!c.pageInfo.hasNextPage)break; cursor=c.pageInfo.endCursor;
const cost=j.extensions?.cost?.throttleStatus; if(cost&&cost.currentlyAvailable<400) await sleep(1800); else await sleep(300);
if(pages%100===0) console.log(` ${pages}p scanned=${scanned} matched=${matched}`);
}
console.log(`\n=== inverse-mislabel-disambig (READ-ONLY, $0) ===`);
console.log(`active scanned: ${scanned} (complete=${complete}, ${pages}p)`);
console.log(`c84 inverse-mislabels re-matched: ${matched}`);
console.log(`\nDISPOSITION SIGNAL (the officer's tag-vs-SPLIT question):`);
console.log(` in a showroom COLLECTION (untagged-showroom signal): ${bColl}`);
console.log(` has custom.showroom_line METAFIELD (untagged-showroom signal): ${bMeta}`);
console.log(` collection OR metafield present => UNTAGGED-SHOWROOM (cheap TAG fix): ${bCollOrMeta}`);
console.log(` NEITHER => genuine-mislabel candidate (SPLIT, gated on cost): ${bNeither}`);
console.log(`\nby vendor (top 15) [n / of-which-untagged-showroom]:`);
Object.entries(byVendor).sort((a,b)=>b[1].n-a[1].n).slice(0,15).forEach(([v,o])=>console.log(` ${v}: ${o.n} / ${o.collOrMeta} untagged-showroom`));
console.log(`\nby variant-title:`); Object.entries(byVariantTitle).sort((a,b)=>b[1]-a[1]).slice(0,8).forEach(([t,n])=>console.log(` "${t}": ${n}`));
console.log(`\nsamples (coll=in-showroom-collection meta=has-metafield img=#images body=plaintext-chars):`);
sample.forEach(x=>console.log(` "${x.t}" [${x.vendor}] $${x.p} v="${x.vt}" coll=${x.coll} meta=${x.meta} img=${x.img} body=${x.body}`));
fs.writeFileSync('/tmp/inverse-mislabel-disambig.json',JSON.stringify({ts:new Date().toISOString(),scanned,complete,thresh:THRESH,matched,bColl,bMeta,bCollOrMeta,bNeither,byVendor,byVariantTitle,sample},null,2));
console.log(`\nwrote /tmp/inverse-mislabel-disambig.json`);