← back to Gmc 425 Supplemental Feed
add bounded leak sampler/classifier (N=150): true leak rate 14.7% (~7150 of 48748), 38% supp-fixable, 47% already clean; v0.1.1
60c265d3a5ea6a45f31fb024850e506ec111750f · 2026-08-10 09:08:17 -0700 · Steve Abrams
Files touched
M package.jsonA sample-and-classify.mjs
Diff
commit 60c265d3a5ea6a45f31fb024850e506ec111750f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 10 09:08:17 2026 -0700
add bounded leak sampler/classifier (N=150): true leak rate 14.7% (~7150 of 48748), 38% supp-fixable, 47% already clean; v0.1.1
---
package.json | 6 +++--
sample-and-classify.mjs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 69 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index a91e118..f113b34 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,10 @@
{
"name": "gmc-425-supplemental-feed",
- "version": "0.1.0",
+ "version": "0.1.1",
"private": true,
"type": "module",
"description": "DRY-RUN classifier for the DW $4.25 GMC leak: determines per-offer whether a supplemental feed can fix it, or whether the Roll is absent from the primary feed (needs a Shopify feed fix / unpublish). Never uploads.",
- "bin": { "classify-leakers": "classify-leakers.mjs" }
+ "bin": {
+ "classify-leakers": "classify-leakers.mjs"
+ }
}
diff --git a/sample-and-classify.mjs b/sample-and-classify.mjs
new file mode 100644
index 0000000..29702df
--- /dev/null
+++ b/sample-and-classify.mjs
@@ -0,0 +1,65 @@
+#!/usr/bin/env node
+/**
+ * sample-and-classify.mjs — DRY-RUN, READ-ONLY, bounded. Touches NO live feed.
+ * 1) Pull a BOUNDED sample (N) of the real "leaks" population from Shopify:
+ * active, published-on-Google, min-variant<=4.25 AND max-variant>4.25 (has $4.25 sample + real roll).
+ * 2) For each, resolve the Roll variant (max price) + Sample variant ($4.25), check live MC per-offer.
+ * 3) Classify SUPP_FIXABLE / SUPP_CANT_HELP / CLEAN and report the distribution → true leak RATE.
+ * Extrapolates to the full leaks population count passed as --pop (default 48748).
+ * Usage: node sample-and-classify.mjs [N=150] [--pop 48748]
+ */
+import fs from 'node:fs'; import crypto from 'node:crypto';
+const HOME=process.env.HOME, MERCHANT='146735262', SHOP='designer-laboratory-sandbox';
+const API=`https://${SHOP}.myshopify.com/admin/api/2024-10/graphql.json`;
+const SA_PATH=HOME+'/Projects/secrets-manager/gmc-sa-146735262.json';
+const T=(fs.readFileSync(HOME+'/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1].replace(/['"]/g,'').trim();
+const OUT=new URL('./out/',import.meta.url); fs.mkdirSync(OUT,{recursive:true});
+const N=parseInt(process.argv[2]||'150',10);
+const POP=parseInt((process.argv.find(a=>a.startsWith('--pop='))||'--pop=48748').split('=')[1],10);
+const b64=b=>Buffer.from(b).toString('base64').replace(/=/g,'').replace(/\+/g,'-').replace(/\//g,'_');
+async function gql(q,v){const r=await fetch(API,{method:'POST',headers:{'X-Shopify-Access-Token':T,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v||{}})});return r.json();}
+async function mcToken(){const SA=JSON.parse(fs.readFileSync(SA_PATH,'utf8'));const now=Math.floor(Date.now()/1000);
+ const si=b64(JSON.stringify({alg:'RS256',typ:'JWT'}))+'.'+b64(JSON.stringify({iss:SA.client_email,scope:'https://www.googleapis.com/auth/content',aud:'https://oauth2.googleapis.com/token',iat:now,exp:now+3600}));
+ const s=crypto.createSign('RSA-SHA256');s.update(si);const jwt=si+'.'+b64(s.sign(SA.private_key));
+ const r=await fetch('https://oauth2.googleapis.com/token',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({grant_type:'urn:ietf:params:oauth:grant-type:jwt-bearer',assertion:jwt})});
+ const j=await r.json();if(!j.access_token)throw new Error('mc token fail');return j.access_token;}
+const GOOG_PUB=await (async()=>{const j=await gql(`{publications(first:25){nodes{id name}}}`);const g=j.data.publications.nodes.find(n=>/google/i.test(n.name));return g.id;})();
+const H=await mcToken().then(t=>({Authorization:`Bearer ${t}`}));
+async function inFeed(pid,vid){const oid=`online:en:US:shopify_US_${pid}_${vid}`;
+ const r=await fetch(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/productstatuses/${encodeURIComponent(oid)}`,{headers:H});return {oid,present:r.status!==404};}
+// 1) pull bounded leak sample
+const picks=[]; let cur=null, scanned=0;
+while(picks.length<N){
+ const j=await gql(`query($c:String){products(first:100,after:$c,query:"status:active"){pageInfo{hasNextPage endCursor} nodes{id onG:publishedOnPublication(publicationId:"${GOOG_PUB}") featuredImage{url} variants(first:60){nodes{id title price}}}}}`,{c:cur});
+ const pg=j.data.products; for(const p of pg.nodes){ scanned++;
+ if(!p.onG||!p.featuredImage) continue;
+ const vs=p.variants.nodes.map(v=>({...v,p:parseFloat(v.price)}));
+ const mn=Math.min(...vs.map(v=>v.p)), mx=Math.max(...vs.map(v=>v.p));
+ if(mn<=4.25 && mx>4.25){ // leak signature
+ const sample=vs.find(v=>v.p<=4.25), roll=vs.reduce((a,b)=>b.p>a.p?b:a,vs[0]);
+ picks.push({pid:p.id.split('/').pop(),rollVid:roll.id.split('/').pop(),sampleVid:sample.id.split('/').pop(),rollPrice:roll.p});
+ if(picks.length>=N) break;
+ }
+ }
+ if(!pg.pageInfo.hasNextPage) break; cur=pg.pageInfo.endCursor;
+ if(scanned>20000) break; // hard bound
+}
+// 2+3) classify
+const counts={SUPP_FIXABLE:0,SUPP_CANT_HELP:0,CLEAN:0}; const rows=[];
+for(const L of picks){
+ const roll=await inFeed(L.pid,L.rollVid), samp=await inFeed(L.pid,L.sampleVid);
+ let cls = !samp.present ? 'CLEAN' : (roll.present ? 'SUPP_FIXABLE' : 'SUPP_CANT_HELP');
+ counts[cls]++; rows.push({...L,rollInFeed:roll.present,sampleInFeed:samp.present,classification:cls});
+}
+fs.writeFileSync(new URL('sample-classification.json',OUT),JSON.stringify({n:picks.length,scanned,counts,rows},null,2));
+const n=picks.length||1;
+const leakRate=counts.SUPP_CANT_HELP/n;
+console.log(`\nScanned ${scanned} active products to collect ${picks.length} leak-signature products (on Google + $4.25 sample + real roll).`);
+console.log(`CLASS DISTRIBUTION (of the sample):`);
+for(const k of Object.keys(counts)) console.log(` ${k.padEnd(15)} ${counts[k]} (${(100*counts[k]/n).toFixed(1)}%)`);
+console.log(`\nTRUE-LEAK interpretation:`);
+console.log(` SUPP_CANT_HELP = Google feeds ONLY the $4.25 Sample (Roll absent) = the ACTUAL advertised-price leak.`);
+console.log(` SUPP_FIXABLE = both offers in feed (supplemental can suppress sample).`);
+console.log(` CLEAN = Google already feeds the Roll (NOT leaking, despite having a $4.25 sample).`);
+console.log(`\n Measured true-leak rate: ${(100*leakRate).toFixed(1)}% → extrapolated over ${POP} leak-bucket products ≈ ${Math.round(leakRate*POP)} genuinely leaking.`);
+console.log(` (vs the Shopify-side upper bound of ${POP}.) Wrote out/sample-classification.json.`);
← 8e6c434 gmc-425 dry-run leaker classifier v0.1.0: proves supplementa
·
back to Gmc 425 Supplemental Feed
·
supplemental-feed dry-run generator + primary-feed spike: RO 05b55a3 →