← back to Ticket System

data/claude-run-11256/evidence/canary.mjs.post-fix

170 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; }
// Shopify appends a GUID suffix on re-upload of the same asset (e.g. "..._2df9_6aed8978-...-c7bc33a3d68d.jpg");
// stripping it lets us compare the underlying DAM asset identity, not the re-upload artifact.
const GUID_RE=/_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(\.[a-zA-Z]+)$/;
function stripGuid(fn){ return fn.replace(GUID_RE,'$1'); }

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 ownStagingFiles(vendors){
  // Per-SKU precision check (fixes a confirmed false-positive class, TK-11256 2026-09-05:
  // review12_verdicts.jsonl vision-adjudicated 7 "KEEP" cases where the flagged gallery
  // image WAS the SKU's own staging main/gallery asset, but the coarse token() bucket
  // let a DIFFERENT colorway's similarly-prefixed asset claim it in the global home map).
  // Returns {mfr_sku: Set(stripGuid(filename))} — own-file identity, no cross-SKU bucketing.
  const own={};
  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 sku=p[0].trim(); if(!sku) continue;
      const set=own[sku]=own[sku]||new Set();
      if(p[1]) set.add(stripGuid(fname(p[1])));
      for(const g of p[2].split('~~')) if(g.trim()) set.add(stripGuid(fname(g)));
    }
  }
  return own;
}
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); }
    // per-SKU own-file precision map (checked BEFORE the coarse token-bucket fallback below)
    const ownFiles=ownStagingFiles(VENDORS);
    // 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'){ // highest-precision check: is this literally the SKU's own staging file?
            const own=ownFiles[psku];
            if(own && own.has(stripGuid(f))) c='own';
          }
          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();