[object Object]

← back to Imageless Rescue

width-from-sibling: safe additive width inheritance from same-pattern siblings (no guessing); dry-run shows ~4% yield — rest need scrape-or-demote

98e02b6ea863a76d074f96773cf4bc3c5a7a04ac · 2026-06-21 13:35:00 -0700 · Steve

Files touched

Diff

commit 98e02b6ea863a76d074f96773cf4bc3c5a7a04ac
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jun 21 13:35:00 2026 -0700

    width-from-sibling: safe additive width inheritance from same-pattern siblings (no guessing); dry-run shows ~4% yield — rest need scrape-or-demote
---
 scripts/width-from-sibling.js | 38 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 38 insertions(+)

diff --git a/scripts/width-from-sibling.js b/scripts/width-from-sibling.js
new file mode 100644
index 0000000..95ae8ab
--- /dev/null
+++ b/scripts/width-from-sibling.js
@@ -0,0 +1,38 @@
+'use strict';
+// Width inheritance: for each width-violator, copy global.width from a SAME-pattern
+// sibling colorway that already has one (exact pattern_name match ⇒ same width).
+// Additive, idempotent, resumable, dry-run. No guessing.
+const fs=require('fs'),os=require('os'),path=require('path'),https=require('https');
+const DRY=process.argv.includes('--dry-run');
+const LIMIT=process.argv.includes('--limit')?parseInt(process.argv[process.argv.indexOf('--limit')+1],10):Infinity;
+function tok(){if(process.env.SHOPIFY_ADMIN_TOKEN)return process.env.SHOPIFY_ADMIN_TOKEN.replace(/['"]/g,'').trim();for(const p of['Projects/secrets-manager/.env']){try{const m=fs.readFileSync(path.join(os.homedir(),p),'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m);if(m)return m[1].replace(/['"]/g,'').trim()}catch{}}return null}
+const T=tok(),STORE='designer-laboratory-sandbox.myshopify.com',API='2024-10';
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+function gql(q,v){return new Promise(res=>{const b=JSON.stringify({query:q,variables:v});const rq=https.request({hostname:STORE,path:`/admin/api/${API}/graphql.json`,method:'POST',headers:{'X-Shopify-Access-Token':T,'Content-Type':'application/json','Content-Length':Buffer.byteLength(b)}},r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>{try{res(JSON.parse(d))}catch{res({})}})});rq.on('error',()=>res({}));rq.write(b);rq.end()})}
+async function G(q,v){for(let a=0;a<6;a++){const r=await gql(q,v);if(r.errors&&JSON.stringify(r.errors).toUpperCase().includes('THROTTL')){await sleep(2000*(a+1));continue}return r}return{}}
+const PROD=`query($id:ID!){product(id:$id){id vendor title pat:metafield(namespace:"custom",key:"pattern_name"){value}}}`;
+const SIB=`query($q:String!){products(first:25,query:$q){nodes{title wG:metafield(namespace:"global",key:"width"){value} wC:metafield(namespace:"custom",key:"width"){value} pat:metafield(namespace:"custom",key:"pattern_name"){value}}}}`;
+const SET=`mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){userErrors{message}}}`;
+(async()=>{
+  const d=require('../data/active-gate-violations-2026-06-21.json');
+  const vio=(d.violations||[]).filter(x=>(x.types||[]).includes('specs'));
+  const DATADIR=path.join(__dirname,'..','data');const LED=path.join(DATADIR,'width-sibling-ledger.jsonl');
+  const done=new Set();if(fs.existsSync(LED))for(const l of fs.readFileSync(LED,'utf8').split('\n')){if(l.trim())try{done.add(JSON.parse(l).id)}catch{}}
+  const queue=vio.filter(x=>!done.has(x.id)).slice(0,LIMIT===Infinity?undefined:LIMIT);
+  console.log(`width-from-sibling ${DRY?'[DRY] ':''}violators=${vio.length} done=${done.size} thisRun=${queue.length}`);
+  const st={filled:0,noPattern:0,noSibWidth:0,err:0};
+  for(const x of queue){
+    const pr=await G(PROD,{id:x.id});const p=pr.data&&pr.data.product;if(!p){st.err++;continue}
+    const pattern=(p.pat&&p.pat.value||'').trim();
+    if(!pattern){st.noPattern++;if(!DRY)fs.appendFileSync(LED,JSON.stringify({id:x.id,result:'no-pattern'})+'\n');continue}
+    const sr=await G(SIB,{q:`vendor:"${p.vendor}" status:active`});
+    const sibs=(sr.data&&sr.data.products&&sr.data.products.nodes||[]).filter(n=>(n.pat&&n.pat.value||'').trim().toLowerCase()===pattern.toLowerCase());
+    const w=sibs.map(n=>(n.wG&&n.wG.value)||(n.wC&&n.wC.value)||'').map(s=>s.trim()).find(Boolean);
+    if(!w){st.noSibWidth++;if(!DRY)fs.appendFileSync(LED,JSON.stringify({id:x.id,result:'no-sibling-width',pattern})+'\n');continue}
+    if(DRY){st.filled++;if(st.filled<=12)console.log(`  • ${p.title.slice(0,46)} → width "${w}" (from sibling, pattern="${pattern}")`);continue}
+    const mr=await G(SET,{mf:[{ownerId:x.id,namespace:'global',key:'width',type:'single_line_text_field',value:w}]});
+    if(mr.data&&mr.data.metafieldsSet&&(!mr.data.metafieldsSet.userErrors.length)){st.filled++;fs.appendFileSync(LED,JSON.stringify({id:x.id,width:w,pattern,result:'filled'})+'\n');if(st.filled%25===0)console.log(`  ✓ ${st.filled} widths set`)}
+    else{st.err++}
+  }
+  console.log(`\n=== ${DRY?'DRY ':''}SUMMARY ===`,JSON.stringify(st));
+})().catch(e=>{console.error('FATAL',e);process.exit(1)});

← 1c6e8a6 Live ACTIVE-catalog gate audit + SAFE remediation of 2 image  ·  back to Imageless Rescue  ·  width-from-catalog: DB-lookup width fill from per-vendor *_c 5722e55 →