[object Object]

← back to Dw Pairs Well

WS-1: junk-root denylist (SANCAR/INNOVATIONS/TRUE/placeholders → title fallback) + bulk mfr_sku pull tool

12d667726f82103b2ae0aeb526910752dac414f3 · 2026-06-26 09:12:47 -0700 · Steve

Files touched

Diff

commit 12d667726f82103b2ae0aeb526910752dac414f3
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jun 26 09:12:47 2026 -0700

    WS-1: junk-root denylist (SANCAR/INNOVATIONS/TRUE/placeholders → title fallback) + bulk mfr_sku pull tool
---
 tools/pattern-id-backfill.js | 15 +++++++++++++--
 tools/pull-mfr-sku.js        | 33 +++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+), 2 deletions(-)

diff --git a/tools/pattern-id-backfill.js b/tools/pattern-id-backfill.js
index 697c9a9..90a7691 100644
--- a/tools/pattern-id-backfill.js
+++ b/tools/pattern-id-backfill.js
@@ -98,21 +98,32 @@ function stripColors(normalized) {
 //   W7008-01     -> W7008
 //   AT9604       -> AT9604        (no colorway → unchanged)
 //   5004561      -> 5004561       (single pure-numeric token → keep, never empty out)
+// Junk roots: a few vendors stored a SUPPLIER NAME or boolean in the manufacturer_sku
+// metafield instead of a pattern code. These must NEVER become a pattern_id — demote to
+// the T2 title fallback. (DTD verdict A, 2026-06-26.)
+const DENY_ROOTS = new Set([
+  'SANCAR','INNOVATIONS','TRUE','FALSE','NULL','NONE','NA','UNKNOWN','TBD','TEST',
+  'SAMPLE','DEFAULT','N','Y','YES','NO','0','1','PENDING','TBA','VARIOUS','ASSORTED'
+]);
 function mfrRoot(raw) {
   if (!raw) return null;
   let s = String(raw).trim().toUpperCase();
   if (!s) return null;
   // split on the common SKU separators
   const tokens = s.split(/[-/_.\s]+/).filter(Boolean);
+  let root;
   if (tokens.length >= 2) {
     const last = tokens[tokens.length - 1];
     // colorway suffix = 1-3 digits, optionally one trailing letter (01, 001, 01A)
     if (/^\d{1,3}[A-Z]?$/.test(last)) {
-      return tokens.slice(0, -1).join('-');
+      root = tokens.slice(0, -1).join('-');
     }
   }
   // no separable colorway → root is the whole thing (separators normalized to '-')
-  return tokens.join('-');
+  if (!root) root = tokens.join('-');
+  // junk-root guard: supplier names / booleans / placeholders → not a real pattern key
+  if (DENY_ROOTS.has(root)) return null;
+  return root;
 }
 
 function isTexture(tagsLower, titleLower) {
diff --git a/tools/pull-mfr-sku.js b/tools/pull-mfr-sku.js
new file mode 100644
index 0000000..2a4c38b
--- /dev/null
+++ b/tools/pull-mfr-sku.js
@@ -0,0 +1,33 @@
+// READ-ONLY: bulk-pull custom.manufacturer_sku for all ACTIVE products → tools/mfr-map.jsonl
+// Uses the full-access token (write_products implies read). No writes. $0 (Shopify API).
+const https=require('https');
+const STORE='designer-laboratory-sandbox.myshopify.com', API='2024-10';
+const fs=require('fs'), cp=require('child_process');
+const TOK=cp.execSync(`grep -rhoE 'shpat_[a-f0-9]{32}' ~/Projects/secrets-manager/.env ~/Projects/Designer-Wallcoverings/DW-Agents ~/Projects/Designer-Wallcoverings/shopify 2>/dev/null | sort -u`).toString().split('\n').find(t=>t.startsWith('shpat_307')&&t.endsWith('a43b'));
+if(!TOK){console.error('no full-access token');process.exit(1);}
+function gql(q){return new Promise((res,rej)=>{const body=JSON.stringify({query:q});const r=https.request({host:STORE,path:`/admin/api/${API}/graphql.json`,method:'POST',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)}},resp=>{let d='';resp.on('data',c=>d+=c);resp.on('end',()=>{try{res(JSON.parse(d))}catch(e){rej(d.slice(0,300))}})});r.on('error',rej);r.write(body);r.end();});}
+(async()=>{
+  const start=await gql(`mutation{bulkOperationRunQuery(query:"""
+    { products(query:"status:active") { edges { node { id handle vendor
+        metafield(namespace:"custom",key:"manufacturer_sku"){ value }
+        variants(first:1){ edges { node { sku } } } } } } }
+  """){ bulkOperation{ id status } userErrors{ field message } } }`);
+  const ue=start.data?.bulkOperationRunQuery?.userErrors;
+  if(ue&&ue.length){console.error('userErrors',JSON.stringify(ue));process.exit(1);}
+  console.log('bulk op started:',start.data.bulkOperationRunQuery.bulkOperation.id);
+  // poll
+  let url=null, tries=0;
+  while(tries++<120){
+    await new Promise(r=>setTimeout(r,5000));
+    const cur=await gql(`{ currentBulkOperation{ status objectCount url errorCode } }`);
+    const o=cur.data.currentBulkOperation;
+    process.stdout.write(`\r  ${o.status} objects=${o.objectCount||0}   `);
+    if(o.status==='COMPLETED'){url=o.url;console.log('\n COMPLETED objects=',o.objectCount);break;}
+    if(o.status==='FAILED'){console.error('\n FAILED',o.errorCode);process.exit(1);}
+  }
+  if(!url){console.error('\n timed out');process.exit(1);}
+  // download JSONL
+  await new Promise((res,rej)=>{const f=fs.createWriteStream('tools/mfr-map.jsonl');https.get(url,r=>{r.pipe(f);f.on('finish',()=>{f.close();res();});}).on('error',rej);});
+  const n=fs.readFileSync('tools/mfr-map.jsonl','utf8').trim().split('\n').filter(Boolean).length;
+  console.log('saved tools/mfr-map.jsonl lines=',n);
+})().catch(e=>{console.error('ERR',e);process.exit(1);});

← de4b0bc auto-save: 2026-06-26T08:13:39 (3 files) — server.js tools/g  ·  back to Dw Pairs Well  ·  auto-save: 2026-06-26T09:14:14 (4 files) — tools/pattern-id- 9454dbf →