← back to Dw Yolo Loop
metafield-coverage audit: repped-vendor specs/images/color coverage (read-only GraphQL)
acaeef7191185910315f4151989947f961793b7d · 2026-06-16 08:10:41 -0700 · Steve Abrams
Cycle-26 read-only canary. Scans ACTIVE Shopify products, counts per-vendor
coverage of image (>=1/>=3), specs (custom.material+width), color-enrichment
(custom.color_hex/color_details), pattern_name metafields; tags repped vendors
from vendor_registry (active + non-private-label + discount_confirmed). Result:
images 100% + color 100% (solved), specs 51.9% weighted = the real gap, 13
repped vendors <60%. Price coverage tracked separately via GMC scan.
Files touched
A scripts/metafield-coverage/metafield-coverage-audit.mjs
Diff
commit acaeef7191185910315f4151989947f961793b7d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jun 16 08:10:41 2026 -0700
metafield-coverage audit: repped-vendor specs/images/color coverage (read-only GraphQL)
Cycle-26 read-only canary. Scans ACTIVE Shopify products, counts per-vendor
coverage of image (>=1/>=3), specs (custom.material+width), color-enrichment
(custom.color_hex/color_details), pattern_name metafields; tags repped vendors
from vendor_registry (active + non-private-label + discount_confirmed). Result:
images 100% + color 100% (solved), specs 51.9% weighted = the real gap, 13
repped vendors <60%. Price coverage tracked separately via GMC scan.
---
.../metafield-coverage-audit.mjs | 156 +++++++++++++++++++++
1 file changed, 156 insertions(+)
diff --git a/scripts/metafield-coverage/metafield-coverage-audit.mjs b/scripts/metafield-coverage/metafield-coverage-audit.mjs
new file mode 100644
index 0000000..2e450f7
--- /dev/null
+++ b/scripts/metafield-coverage/metafield-coverage-audit.mjs
@@ -0,0 +1,156 @@
+// metafield-coverage-audit — read-only Shopify GraphQL audit of enrichment
+// metafield coverage per vendor, for the standing rule "discover price+specs+
+// images per SKU and ALWAYS write to Shopify metafields" (repped vendors).
+//
+// For every ACTIVE product it records, per vendor:
+// • images: has >=1 image, has >=3 images (the "ALL images" half of the rule)
+// • specs: has custom.material AND custom.width (the core spec metafields)
+// • color: has custom.color_hex OR custom.color_details (the AI color-enrichment)
+// • pattern: has custom.pattern_name
+// Price coverage is intentionally NOT re-pulled here — the GMC dry-run already
+// maps which products are sample-only ($4.25) vs have a real roll; pulling 60
+// variants/product would blow the GraphQL cost cap. This audit is specs+images.
+//
+// Vendors are tagged from dw_unified.vendor_registry: repped = is_active AND
+// NOT is_private_label AND discount_confirmed (the confirmed trade lines DW reps).
+//
+// node metafield-coverage-audit.mjs [--min 60] [--page 50]
+// READ-ONLY (GraphQL queries + metafield reads only, no mutations). $0.
+import fs from 'node:fs';
+import { execFileSync } from 'node:child_process';
+
+const args = process.argv.slice(2);
+const MIN = parseInt(args.find((_,i,a)=>a[i-1]==='--min')||'60',10)||60; // WARN threshold % per axis
+const PAGE = parseInt(args.find((_,i,a)=>a[i-1]==='--page')||'50',10)||50;
+const SHOP='designer-laboratory-sandbox.myshopify.com', VERSION='2024-10';
+const ENV=fs.readFileSync('/Users/stevestudio2/Projects/secrets-manager/.env','utf8');
+const TOKEN=(ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1];
+if(!TOKEN){console.error('no SHOPIFY_ADMIN_TOKEN');process.exit(1);}
+const URL=`https://${SHOP}/admin/api/${VERSION}/graphql.json`;
+const OUT=`${process.env.HOME}/.claude/yolo-queue/metafield-coverage-2026-06-16.json`;
+const MD =`${process.env.HOME}/.claude/yolo-queue/metafield-coverage-2026-06-16.md`;
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+
+// --- repped tags from vendor_registry (case-insensitive vendor_name map) ---
+function reppedTags(){
+ const PSQL='/opt/homebrew/opt/postgresql@14/bin/psql';
+ const DB='postgresql:///dw_unified?host=/tmp';
+ let rows=[];
+ try{
+ const out=execFileSync(PSQL,[DB,'-At','-F','\t','-c',
+ "select lower(vendor_name), is_active, is_private_label, coalesce(discount_confirmed,false) from vendor_registry;"],{encoding:'utf8'});
+ rows=out.trim().split('\n').filter(Boolean).map(l=>l.split('\t'));
+ }catch(e){ console.error('warn: vendor_registry read failed ('+e.message.slice(0,40)+') — tags will be blank'); }
+ const m=new Map();
+ for(const [name,act,pl,disc] of rows){
+ const repped = act==='t' && pl!=='t' && disc==='t';
+ m.set(name,{is_active:act==='t',private_label:pl==='t',discount_confirmed:disc==='t',repped});
+ }
+ return m;
+}
+
+async function gql(q,v){
+ for(let a=0;a<8;a++){
+ let res;
+ try{ res=await fetch(URL,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})}); }
+ catch(e){ await sleep(2000*(a+1)); continue; }
+ if(res.status===429){ await sleep(2000*(a+1)); continue; }
+ const j=await res.json();
+ if(j.errors){ if(JSON.stringify(j.errors).match(/THROTTLED|exceeded/i)){ await sleep(2500*(a+1)); continue;} throw new Error(JSON.stringify(j.errors).slice(0,200)); }
+ const avail=j.extensions?.cost?.throttleStatus?.currentlyAvailable??2000;
+ if(avail<300) await sleep(1500);
+ return j.data;
+ }
+ throw new Error('exhausted retries');
+}
+
+const QUERY=`query($cursor:String){
+ products(first:${PAGE}, after:$cursor, query:"status:active"){
+ pageInfo{ hasNextPage endCursor }
+ nodes{
+ vendor
+ images(first:3){ nodes{ id } }
+ material: metafield(namespace:"custom", key:"material"){ value }
+ width: metafield(namespace:"custom", key:"width"){ value }
+ colorHex: metafield(namespace:"custom", key:"color_hex"){ value }
+ colorDetails: metafield(namespace:"custom", key:"color_details"){ value }
+ patternName: metafield(namespace:"custom", key:"pattern_name"){ value }
+ }
+ }
+}`;
+
+function blank(){return {n:0, img1:0, img3:0, specs:0, color:0, pattern:0};}
+
+(async()=>{
+ const tags=reppedTags();
+ const byVendor=new Map();
+ let cursor=null, total=0, pages=0; const t0=Date.now();
+ while(true){
+ const d=await gql(QUERY,{cursor});
+ const c=d.products;
+ for(const p of c.nodes){
+ total++;
+ const v=p.vendor||'(none)';
+ if(!byVendor.has(v)) byVendor.set(v,blank());
+ const s=byVendor.get(v); s.n++;
+ const imgs=p.images.nodes.length;
+ if(imgs>=1) s.img1++;
+ if(imgs>=3) s.img3++;
+ if(p.material?.value && p.width?.value) s.specs++;
+ if(p.colorHex?.value || p.colorDetails?.value) s.color++;
+ if(p.patternName?.value) s.pattern++;
+ }
+ pages++;
+ if(pages%50===0) process.stderr.write(` ...${pages} pages, ${total} products, ${byVendor.size} vendors\n`);
+ if(!c.pageInfo.hasNextPage) break;
+ cursor=c.pageInfo.endCursor;
+ }
+ const elapsed=((Date.now()-t0)/1000).toFixed(1);
+ const pct=(x,n)=>n?Math.round(1000*x/n)/10:0;
+
+ const vendors=[...byVendor.entries()].map(([vendor,s])=>{
+ const t=tags.get(vendor.toLowerCase())||null;
+ return { vendor, repped: t?.repped??null, private_label: t?.private_label??null,
+ active_products:s.n,
+ img1_pct:pct(s.img1,s.n), img3_pct:pct(s.img3,s.n),
+ specs_pct:pct(s.specs,s.n), color_pct:pct(s.color,s.n), pattern_pct:pct(s.pattern,s.n) };
+ }).sort((a,b)=>b.active_products-a.active_products);
+
+ // repped vendors below MIN on any axis = the gap list
+ const reppedV = vendors.filter(v=>v.repped===true);
+ const gaps = reppedV.filter(v=>v.active_products>=50 &&
+ (v.img1_pct<MIN || v.specs_pct<MIN || v.color_pct<MIN));
+
+ const report={ generated_at:new Date().toISOString(), scanned_active:total, pages, elapsed_s:+elapsed,
+ warn_below_pct:MIN, vendor_count:vendors.length,
+ repped_vendor_count:reppedV.length, repped_gap_count:gaps.length,
+ repped_gaps:gaps, vendors };
+ fs.writeFileSync(OUT, JSON.stringify(report,null,2));
+
+ // overall coverage (repped only, weighted by product count)
+ const sum=(arr,k)=>arr.reduce((a,v)=>a+v[k]*v.active_products,0);
+ const totR=reppedV.reduce((a,v)=>a+v.active_products,0)||1;
+ const wimg1=Math.round(10*sum(reppedV,'img1_pct')/totR)/10;
+ const wspecs=Math.round(10*sum(reppedV,'specs_pct')/totR)/10;
+ const wcolor=Math.round(10*sum(reppedV,'color_pct')/totR)/10;
+
+ let md=`# Shopify metafield-coverage audit (repped vendors) — ${new Date().toISOString().slice(0,16)}\n\n`;
+ md+=`**READ-ONLY (GraphQL metafield reads, no writes), \$0.** Scanned **${total}** ACTIVE products in ${elapsed}s (${pages} pages). `;
+ md+=`Standing rule: repped vendors must have price+specs+images discovered → written to Shopify metafields. Price coverage tracked separately (GMC \$4.25 scan); this audit = **specs + images + color-enrichment** metafields.\n\n`;
+ md+=`Repped = vendor_registry is_active AND NOT private_label AND discount_confirmed. Repped vendors: **${reppedV.length}** (${totR} active products).\n\n`;
+ md+=`## Weighted repped coverage\n`;
+ md+=`| Axis | Coverage |\n|---|---:|\n| ≥1 image | ${wimg1}% |\n| specs (custom.material+width) | ${wspecs}% |\n| color-enrichment (hex/details) | ${wcolor}% |\n\n`;
+ md+=`## 🔴 Repped vendors below ${MIN}% on image / specs / color (≥50 active products) — ${gaps.length}\n`;
+ md+=`| Vendor | Active | ≥1img% | ≥3img% | specs% | color% | pattern% |\n|---|---:|---:|---:|---:|---:|---:|\n`;
+ for(const v of gaps) md+=`| ${v.vendor} | ${v.active_products} | ${v.img1_pct} | ${v.img3_pct} | ${v.specs_pct} | ${v.color_pct} | ${v.pattern_pct} |\n`;
+ md+=`\n## All vendors (top 50 by active count)\n`;
+ md+=`| Vendor | Repped | Active | ≥1img% | ≥3img% | specs% | color% | pattern% |\n|---|:--:|---:|---:|---:|---:|---:|---:|\n`;
+ for(const v of vendors.slice(0,50)) md+=`| ${v.vendor} | ${v.repped===true?'✓':v.repped===false?'·':'?'} | ${v.active_products} | ${v.img1_pct} | ${v.img3_pct} | ${v.specs_pct} | ${v.color_pct} | ${v.pattern_pct} |\n`;
+ md+=`\n_Repped ✓ = confirmed trade line · · = active non-repped · ? = not matched in vendor_registry. \"specs\" = both custom.material AND custom.width present._\n`;
+ fs.writeFileSync(MD,md);
+
+ console.log(`[metafield-coverage] scanned=${total} vendors=${vendors.length} repped=${reppedV.length} gaps=${gaps.length}`);
+ console.log(` weighted repped: img>=1 ${wimg1}% · specs ${wspecs}% · color ${wcolor}%`);
+ console.log(`Report: ${MD}`);
+ process.exit(0);
+})().catch(e=>{console.error('FATAL',e.message);process.exit(1);});
← 3343eea harden 3 read-only canaries (DTD-A 3/3): dynamic-dated repor
·
back to Dw Yolo Loop
·
robots/sitemap fleet canary: crawl-directive + sitemap healt 30b9b80 →