← back to Ticket System
data/claude-run-11256/evidence/canary.mjs.pre-fix-backup
141 lines
#!/usr/bin/env node
// dw-image-identity-canary — flags GALLERY images whose filename-encoded SKU
// does NOT match the product's own mfr SKU (cross-pattern / wrong-colorway
// contamination). Vendor-generalizable; Sanderson-scoped by default.
// READ-ONLY: only GETs Shopify Admin + reads local dw_unified mirror. Writes
// only its own data/latest.json + data/baseline.json. Never mutates products.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
const DIR = path.dirname(fileURLToPath(import.meta.url));
const DATA = path.join(DIR, 'data');
const VER = '2024-10';
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const VENDORS = (process.env.IIC_VENDORS || 'Sanderson').split(',').map(s=>s.trim()).filter(Boolean);
// Per-vendor staging catalog wiring: enriches the asset-ownership (home) map with
// filename->SKU encodings that exist in staging but appear SKU-less on the live product.
// Vendor-generalizable: add a row per vendor as lines are onboarded.
const STAGING = {
Sanderson: { table:'sanderson_catalog', sku:'mfr_sku', main:'image_url', gallery:'gallery_images' },
};
const ALERT = process.argv.includes('--alert');
function token(fn){ return fn.split('?')[0].split('/').pop().split(/[_.]/)[0]; }
function fname(src){ return src.split('?')[0].split('/').pop(); }
const SKU_RE=/([A-Z]{3}\d{4})[_-](\d{2})/;
function dsku(fn){ const m=fn.match(SKU_RE); return m?`${m[1]}-${m[2]}`:null; }
function getToken(){
const env=fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8');
const m=env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m);
return m[1].trim().replace(/^["']|["']$/g,'');
}
async function api(url,tok){
const r=await fetch(url,{headers:{'X-Shopify-Access-Token':tok,'Content-Type':'application/json'}});
if(!r.ok) throw new Error('HTTP '+r.status);
return {json:await r.json(), link:r.headers.get('link')||''};
}
function skuByIdFromMirror(vendors){
// mfr sku from local dw_unified mirror metafields, keyed by numeric shopify id
const list=vendors.map(v=>`'${v.replace(/'/g,"''")}'`).join(',');
const sql=`SELECT split_part(shopify_id,'/',5)||'|'||coalesce(metafields->'dwc'->'manufacturer_sku'->>'value','') FROM shopify_products WHERE vendor IN (${list});`;
const out=execSync(`/opt/homebrew/bin/psql -h /tmp dw_unified -t -A -c "${sql}"`,{encoding:'utf8'});
const map={};
for(const line of out.split('\n')){ const [id,sku]=line.split('|'); if(id) map[id.trim()]=(sku||'').trim(); }
return map;
}
function stagingHome(vendors){
// returns {token: Set(sku)} from each vendor's staging catalog (SKU-encoded filenames)
const home={};
for(const v of vendors){ const cfg=STAGING[v]; if(!cfg) continue;
let rows='';
try{ rows=execSync(`/opt/homebrew/bin/psql -h /tmp dw_unified -t -A -F'|' -c "SELECT ${cfg.sku}, coalesce(${cfg.main},''), array_to_string(${cfg.gallery},'~~') FROM ${cfg.table};"`,{encoding:'utf8'}); }catch(e){ continue; }
for(const line of rows.split('\n')){ const p=line.split('|'); if(p.length<3) continue;
const files=[]; if(p[1]) files.push(fname(p[1])); for(const g of p[2].split('~~')) if(g.trim()) files.push(fname(g));
for(const fn of files){ const d=dsku(fn); if(d)(home[token(fn)]=home[token(fn)]||new Set()).add(d); }
}
}
return home;
}
function classify(fn, psku){
const pbase=psku.split('-')[0];
const d=dsku(fn);
if(d){ if(d===psku) return 'own'; return d.split('-')[0]!==pbase?'foreign_pattern':'foreign_colorway'; }
return 'skuless'; // undetermined by filename alone — canary does not flag (avoids FP)
}
async function run(){
let verdict='PASS', status='PASS', note='', err=null;
const perVendor={}; let totalForeignImgs=0, totalForeignProds=0, totalActive=0, totalGallery=0;
try{
const tok=getToken();
const skuMap=skuByIdFromMirror(VENDORS);
// global home map across all in-scope vendors
const home={}; const mainHome={};
const prodCache={};
for(const vendor of VENDORS){
let url=`https://${DOMAIN}/admin/api/${VER}/products.json?vendor=${encodeURIComponent(vendor)}&status=active&limit=250&fields=id,handle,title,images`;
const prods=[];
while(url){ const {json,link}=await api(url,tok); prods.push(...json.products); const m=link.split(',').find(p=>p.includes('rel="next"')); url=m?m.slice(m.indexOf('<')+1,m.indexOf('>')):''; if(url) await new Promise(r=>setTimeout(r,600)); }
prodCache[vendor]=prods;
for(const p of prods) for(const im of (p.images||[])){ const f=fname(im.src); const d=dsku(f); if(d)(home[token(f)]=home[token(f)]||new Set()).add(d); }
}
// enrich home map with staging-encoded ownership (recovers SKU-less-on-live foreigns)
const sh=stagingHome(VENDORS);
for(const t in sh){ (home[t]=home[t]||new Set()); for(const s2 of sh[t]) home[t].add(s2); }
// live-main-home: an asset used as some product's MAIN(pos1) image is 'owned' by that product
for(const v of VENDORS) for(const p of prodCache[v]){ const psk=skuMap[String(p.id)]; if(!psk) continue;
const im0=(p.images||[]).slice().sort((a,b)=>a.position-b.position)[0]; if(!im0) continue;
(mainHome[token(fname(im0.src))]=mainHome[token(fname(im0.src))]||new Set()).add(psk); }
for(const vendor of VENDORS){
const prods=prodCache[vendor];
let fImgs=0,fProds=0,gal=0;
const examples=[];
for(const p of prods){
totalActive++;
const psku=skuMap[String(p.id)]||'';
if(!psku) continue;
const pbase=psku.split('-')[0];
const imgs=(p.images||[]).slice().sort((a,b)=>a.position-b.position);
let prodFlagged=false;
for(const im of imgs.slice(1)){ // gallery = position>1
gal++; totalGallery++;
const f=fname(im.src); let c=classify(f,psku);
if(c==='skuless'){ // resolve via home map (SKU-encoded/staging ownership)
const hosts=home[token(f)];
if(hosts && !hosts.has(psku)){ c=[...hosts].some(h=>h.split('-')[0]===pbase)?'foreign_colorway':'foreign_pattern'; }
}
if(c==='skuless'){ // resolve via live-main ownership (asset is another product's main)
const mh=mainHome[token(f)];
if(mh && !mh.has(psku)){ c=[...mh].some(h=>h.split('-')[0]===pbase)?'foreign_colorway':'foreign_pattern'; }
}
if(c==='foreign_pattern'||c==='foreign_colorway'){ fImgs++; prodFlagged=true;
if(examples.length<8) examples.push({handle:p.handle,mfr_sku:psku,img:f,cls:c}); }
}
if(prodFlagged) fProds++;
}
perVendor[vendor]={active:prods.length,gallery_imgs:gal,foreign_imgs:fImgs,foreign_products:fProds,examples};
totalForeignImgs+=fImgs; totalForeignProds+=fProds;
}
}catch(e){ err=String(e); verdict='UNKNOWN'; status='WARN'; note='read failure: '+err; }
// baseline-aware: alert only on worsening
const blPath=path.join(DATA,'baseline.json');
let baseline=null; if(fs.existsSync(blPath)) baseline=JSON.parse(fs.readFileSync(blPath,'utf8'));
if(!err){
if(!baseline){ baseline={foreign_products:totalForeignProds,foreign_imgs:totalForeignImgs,ts:new Date().toISOString(),note:'first-run baseline'}; fs.writeFileSync(blPath,JSON.stringify(baseline,null,1)); verdict='PASS'; status='PASS'; note=`baseline set: ${totalForeignProds} products / ${totalForeignImgs} imgs contaminated`; }
else if(totalForeignProds>baseline.foreign_products){ verdict='FAIL'; status='FAIL'; note=`contamination GREW: ${totalForeignProds} products vs baseline ${baseline.foreign_products} — a re-import likely re-introduced foreign gallery images`; }
else { verdict='PASS'; status='PASS'; note=`no worsening (${totalForeignProds} products <= baseline ${baseline.foreign_products})`; }
}
const out={ skill:'dw-image-identity-canary', verdict, status, ts:new Date().toISOString(),
vendors:VENDORS, totals:{active:totalActive,gallery_imgs:totalGallery,foreign_imgs:totalForeignImgs,foreign_products:totalForeignProds},
baseline, perVendor, note };
fs.writeFileSync(path.join(DATA,'latest.json'),JSON.stringify(out,null,1));
console.log(JSON.stringify({verdict,status,totals:out.totals,note},null,1));
if(ALERT && (status==='FAIL')) console.log('[ALERT] would post CNCP card + George email:',note);
}
run();