[object Object]

← back to Dw Yolo Loop

Contract-scope auditor: read-only dw_unified canary for vendor-scope integrity

540e2424c9d91d85108c46a62dc990825aeec3b4 · 2026-06-15 23:59:38 -0700 · Steve Abrams

Complements dw-map-auditor (which owns Kravet MAP + Nicolette). Covers the gap:
discontinued-but-ACTIVE (found 484), photos-only-vendor-priced (Cowtan, clean),
gated-vendor active snapshot (info only, no cry-wolf). dw-map-auditor PASS tonight.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 540e2424c9d91d85108c46a62dc990825aeec3b4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 15 23:59:38 2026 -0700

    Contract-scope auditor: read-only dw_unified canary for vendor-scope integrity
    
    Complements dw-map-auditor (which owns Kravet MAP + Nicolette). Covers the gap:
    discontinued-but-ACTIVE (found 484), photos-only-vendor-priced (Cowtan, clean),
    gated-vendor active snapshot (info only, no cry-wolf). dw-map-auditor PASS tonight.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 scripts/cole-son-price-audit.js                    | 72 ----------------------
 .../contract-scope-auditor.mjs                     | 63 +++++++++++++++++++
 scripts/kravet-2026-coverage-audit.js              | 51 ---------------
 3 files changed, 63 insertions(+), 123 deletions(-)

diff --git a/scripts/cole-son-price-audit.js b/scripts/cole-son-price-audit.js
deleted file mode 100644
index 48022e3..0000000
--- a/scripts/cole-son-price-audit.js
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env node
-/* Read-only Cole & Son pricing audit: join every live product to MAP (kravet_master_price,
-   MAP = wholesale x 1.5) via the custom.manufacturer_sku metafield. Classifies:
-   - sample-only $4.25 with a MAP available  → fixable (set yard variant to MAP)
-   - real price that differs from MAP         → off-MAP (over/under)
-   - no MAP match                             → can't auto-fix
-   Writes /tmp/cs_audit.json with the full fix plan. NO writes to Shopify. */
-const fs = require('fs');
-const STORE = 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.T;
-const sleep = ms => new Promise(r => setTimeout(r, ms));
-
-const MAP = {};
-fs.readFileSync('/tmp/cs_map.txt', 'utf8').trim().split('\n').forEach(l => {
-  const [sku, m] = l.split('|'); if (sku) MAP[sku.trim().toUpperCase()] = parseFloat(m);
-});
-
-async function api(p, tries = 5) {
-  for (let i = 0; i < tries; i++) {
-    const r = await fetch(`https://${STORE}/admin/api/2024-10${p}`, { headers: { 'X-Shopify-Access-Token': TOKEN } });
-    if (r.status === 429 || r.status >= 500) { await sleep(1500 * (i + 1)); continue; }
-    await sleep(90); return r;
-  }
-  throw new Error('api fail ' + p);
-}
-async function getAll() {
-  let url = `/products.json?vendor=Cole%20%26%20Son&limit=250&fields=id,title,status,variants`;
-  const out = [];
-  while (url) {
-    const r = await api(url); const link = r.headers.get('Link') || '';
-    out.push(...((await r.json()).products || []));
-    const m = link.split(',').find(s => s.includes('rel="next"'));
-    url = m ? m.slice(m.indexOf('<') + 1, m.indexOf('>')).replace(/^https:\/\/[^/]+\/admin\/api\/[^/]+/, '') : null;
-  }
-  return out;
-}
-const isSample = v => (v.option1 || '').toLowerCase() === 'sample' || /-sample$/i.test(v.sku || '');
-
-(async () => {
-  const prods = await getAll();
-  console.log(`auditing ${prods.length} Cole & Son products...`);
-  const r = { fixable: [], offMap: [], noMap: [], ok: [] };
-  let i = 0;
-  for (const p of prods) {
-    if (++i % 200 === 0) console.log(`  …${i}/${prods.length}`);
-    const mf = await (await api(`/products/${p.id}/metafields.json`)).json();
-    const msku = (mf.metafields || []).find(m => m.namespace === 'custom' && m.key === 'manufacturer_sku');
-    const code = msku ? String(msku.value).trim().toUpperCase() : null;
-    const map = code ? MAP[code] : null;
-    const yard = p.variants.filter(v => !isSample(v)).sort((a, b) => parseFloat(b.price) - parseFloat(a.price))[0] || p.variants[0];
-    const yardPrice = parseFloat(yard.price);
-    const rec = { id: p.id, vid: yard.id, title: p.title, status: p.status, code, map, cur: yardPrice };
-    if (!map) { r.noMap.push(rec); continue; }
-    if (yardPrice <= 4.25) r.fixable.push(rec);                         // sample-only, MAP available
-    else if (Math.abs(yardPrice - map) > 0.5) r.offMap.push(rec);       // real price ≠ MAP
-    else r.ok.push(rec);                                                 // at MAP
-  }
-  fs.writeFileSync('/tmp/cs_audit.json', JSON.stringify(r, null, 0));
-  const sum = k => r[k].length;
-  const active = a => a.filter(x => x.status === 'active').length;
-  console.log('\n=== COLE & SON PRICING AUDIT ===');
-  console.log(`at MAP (correct):        ${sum('ok')}  (active ${active(r.ok)})`);
-  console.log(`FIXABLE ($4.25 → MAP):   ${sum('fixable')}  (active+live ${active(r.fixable)})`);
-  console.log(`OFF-MAP (real ≠ MAP):    ${sum('offMap')}  (active ${active(r.offMap)})`);
-  console.log(`no MAP match:            ${sum('noMap')}  (active ${active(r.noMap)})`);
-  const overMap = r.offMap.filter(x => x.cur > x.map);
-  console.log(`  of off-MAP, OVER map:  ${overMap.length}  (total $${overMap.reduce((s,x)=>s+(x.cur-x.map),0).toFixed(0)} above MAP)`);
-  console.log('\nsample FIXABLE (now $4.25 → should be MAP):');
-  r.fixable.slice(0, 6).forEach(x => console.log(`  ${x.code}  $4.25 → $${x.map}   ${x.title.slice(0,45)}`));
-  console.log('\nsample OFF-MAP:');
-  r.offMap.slice(0, 6).forEach(x => console.log(`  ${x.code}  $${x.cur} → MAP $${x.map}  (${x.cur>x.map?'+':''}${(x.cur-x.map).toFixed(0)})  ${x.title.slice(0,40)}`));
-})().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/contract-scope-auditor/contract-scope-auditor.mjs b/scripts/contract-scope-auditor/contract-scope-auditor.mjs
new file mode 100644
index 0000000..39b3af6
--- /dev/null
+++ b/scripts/contract-scope-auditor/contract-scope-auditor.mjs
@@ -0,0 +1,63 @@
+// contract-scope-auditor — read-only dw_unified canary for VENDOR-SCOPE / contract
+// integrity, complementing dw-map-auditor (which owns Kravet MAP + Nicolette).
+//
+// This auditor covers the SCOPE gaps the MAP auditor intentionally leaves:
+//   1) FAIL  Discontinued-but-ACTIVE — products tagged 'Discontinued' still status=ACTIVE.
+//            Discontinued SKUs must be archived (vendor 404s, dead links, and they
+//            compound the GMC $4.25 risk). Bounded + actionable.
+//   2) FAIL  Photos-only vendor carrying a REAL price — Cowtan & Tout is crawl-images-only
+//            by design (0% cost-coverage intentional). A Cowtan product with price>$5 is a
+//            data error / scope violation.
+//   3) INFO  Gated-vendor ACTIVE snapshot — Koroseal/Newmor/Desima/Wolf Gordon/Innovations/
+//            Scalamandre are uncosted/push-GATED but KNOWINGLY live. Counts only, never a
+//            FAIL (flagging thousands daily = cry-wolf, per dw-map-auditor's own note).
+//
+// MAP-floor + Nicolette tripwire are NOT re-checked here — dw-map-auditor owns them.
+// READ-ONLY local psql. No writes except the local report JSON. Cost: $0.
+import { execFileSync } from 'node:child_process';
+import fs from 'node:fs';
+
+const HOME = process.env.HOME, DIR = `${HOME}/.claude/yolo-queue`;
+const PSQL = [ '/opt/homebrew/opt/postgresql@14/bin/psql', '/usr/local/opt/postgresql@14/bin/psql', 'psql' ]
+  .find(p => { try { execFileSync(p, ['--version'], { stdio: 'ignore' }); return true; } catch { return false; } }) || 'psql';
+const DBURL = process.env.DW_UNIFIED_URL || 'postgresql:///dw_unified?host=/tmp';
+
+function q(sql) {
+  const out = execFileSync(PSQL, [DBURL, '-At', '-F', '|', '-c', sql], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
+  return out.trim() ? out.trim().split('\n').map(r => r.split('|')) : [];
+}
+
+// 1) Discontinued-but-ACTIVE (sample the worst 200 for the report; count all)
+const discoCount = +(q(`select count(*) from shopify_products where status='ACTIVE' and tags ~* 'discontinu'`)[0]?.[0] || 0);
+const discoByVendor = q(`select vendor, count(*) from shopify_products where status='ACTIVE' and tags ~* 'discontinu' group by vendor order by 2 desc`);
+const discoRows = q(`select vendor, mfr_sku, price, handle from shopify_products where status='ACTIVE' and tags ~* 'discontinu' order by vendor, mfr_sku limit 200`);
+
+// 2) Photos-only vendor (Cowtan) carrying a real price
+const cowtanPriced = q(`select vendor, mfr_sku, price, handle from shopify_products where status='ACTIVE' and vendor ~* 'cowtan' and price::numeric > 5 order by price::numeric desc limit 100`);
+
+// 3) Gated-vendor ACTIVE snapshot (informational)
+const gated = q(`select vendor, count(*) from shopify_products where status='ACTIVE' and vendor ~* 'koroseal|newmor|desima|wolf *gordon|innovations|scalamandre' group by vendor order by 2 desc`);
+
+const findings = [];
+if (discoCount) findings.push({ sev: 'FAIL', check: 'discontinued_active',
+  msg: `${discoCount} ACTIVE product(s) tagged Discontinued (should be archived)`,
+  by_vendor: discoByVendor.map(r => ({ vendor: r[0], count: +r[1] })),
+  sample: discoRows.map(r => ({ vendor: r[0], sku: r[1], price: +r[2], handle: r[3] })) });
+if (cowtanPriced.length) findings.push({ sev: 'FAIL', check: 'photosonly_priced',
+  msg: `${cowtanPriced.length} Cowtan (photos-only) product(s) carry a real price >$5 (scope/data error)`,
+  rows: cowtanPriced.map(r => ({ vendor: r[0], sku: r[1], price: +r[2], handle: r[3] })) });
+findings.push({ sev: 'INFO', check: 'gated_vendor_active_snapshot',
+  msg: `gated/uncosted vendors knowingly live (counts only, not a violation)`,
+  by_vendor: gated.map(r => ({ vendor: r[0], count: +r[1] })) });
+
+const fail = findings.filter(f => f.sev === 'FAIL').length;
+const verdict = fail ? 'FAIL' : 'PASS';
+const out = { scanned_at: new Date().toISOString(), verdict, fail, findings,
+  note: 'Kravet MAP-floor + Nicolette tripwire are owned by dw-map-auditor (ran separately).' };
+
+fs.mkdirSync(DIR, { recursive: true });
+fs.writeFileSync(`${DIR}/contract-scope-audit-${new Date().toISOString().slice(0,10)}.json`, JSON.stringify(out, null, 2));
+
+console.log(`[contract-scope-auditor] ${verdict} · ${fail} FAIL`);
+for (const f of findings) console.log(`  ${f.sev === 'FAIL' ? '✗' : f.sev === 'INFO' ? 'ℹ' : '△'} ${f.check}: ${f.msg}`);
+if (verdict === 'PASS') console.log('  ✓ no discontinued-active, no photos-only mispricing');
diff --git a/scripts/kravet-2026-coverage-audit.js b/scripts/kravet-2026-coverage-audit.js
deleted file mode 100644
index 8c7587d..0000000
--- a/scripts/kravet-2026-coverage-audit.js
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/usr/bin/env node
-/* READ-ONLY: do we have a 2026 price for every live Kravet-family item?
-   For each Kravet-family vendor, pull active products + their manufacturer_sku metafield,
-   check membership in the 2026-priced SKU set (auth_pricing.new_map>0 → /tmp/auth2026_skus.txt).
-   Reports per-vendor coverage + writes /tmp/kravet_2026_gaps.csv (items with NO 2026 price). */
-const fs = require('fs');
-const STORE = 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.T;
-const sleep = ms => new Promise(r => setTimeout(r, ms));
-
-const SET = new Set(fs.readFileSync('/tmp/auth2026_skus.txt','utf8').trim().split('\n').map(s=>s.trim().toUpperCase()));
-const VENDORS = ['Kravet','Kravet Couture','Kravet Design','Lee Jofa','Lee Jofa Modern','Brunschwig & Fils',
-  'Cole & Son','GP & J Baker','Clarke And Clarke','Mulberry','Threads','Baker Lifestyle','Gaston y Daniela','Andrew Martin'];
-const norm = s => (s||'').toUpperCase().replace(/\s+/g,' ').trim();
-
-async function api(p, tries=5){
-  for(let i=0;i<tries;i++){ const r=await fetch(`https://${STORE}/admin/api/2024-10${p}`,{headers:{'X-Shopify-Access-Token':TOKEN}});
-    if(r.status===429||r.status>=500){await sleep(1500*(i+1));continue;} await sleep(80); return r; }
-  throw new Error('fail '+p);
-}
-async function getAll(vendor){
-  let url=`/products.json?vendor=${encodeURIComponent(vendor)}&status=active&limit=250&fields=id,title,handle,variants`; const o=[];
-  while(url){const r=await api(url);const link=r.headers.get('Link')||'';o.push(...((await r.json()).products||[]));const m=link.split(',').find(s=>s.includes('rel="next"'));url=m?m.slice(m.indexOf('<')+1,m.indexOf('>')).replace(/^https:\/\/[^/]+\/admin\/api\/[^/]+/,''):null;} return o;
-}
-const isSample = v => (v.option1||'').toLowerCase()==='sample' || /-sample$/i.test(v.sku||'');
-
-(async()=>{
-  const gaps=[]; const summary=[];
-  const csv=['vendor,mfr_sku,dw_sku,title,handle'];
-  for(const vendor of VENDORS){
-    let prods; try{ prods=await getAll(vendor); }catch(e){ console.log(`  ${vendor}: ERR`); continue; }
-    if(!prods.length) continue;
-    let have=0, miss=0, noSku=0, n=0;
-    for(const p of prods){
-      n++;
-      const mf=await(await api(`/products/${p.id}/metafields.json`)).json();
-      const code=norm(((mf.metafields||[]).find(x=>x.namespace==='custom'&&x.key==='manufacturer_sku')||{}).value||'');
-      const yard=p.variants.find(v=>!isSample(v))||p.variants[0];
-      if(!code){ noSku++; csv.push([vendor,'(no mfr_sku)',yard&&yard.sku||'',JSON.stringify(p.title),p.handle].join(',')); continue; }
-      if(SET.has(code)) have++;
-      else { miss++; csv.push([vendor,code,yard&&yard.sku||'',JSON.stringify(p.title),p.handle].join(',')); }
-    }
-    const pct=(100*have/prods.length).toFixed(1);
-    summary.push({vendor,total:prods.length,have,miss,noSku,pct});
-    console.log(`  ${vendor.padEnd(20)} ${prods.length} total | 2026:${have} (${pct}%) | missing:${miss} | no-mfr-sku:${noSku}`);
-  }
-  fs.writeFileSync('/tmp/kravet_2026_gaps.csv', csv.join('\n'));
-  const T=summary.reduce((a,s)=>({total:a.total+s.total,have:a.have+s.have,miss:a.miss+s.miss,noSku:a.noSku+s.noSku}),{total:0,have:0,miss:0,noSku:0});
-  console.log(`\n=== TOTAL: ${T.total} live Kravet items | have 2026 price: ${T.have} (${(100*T.have/T.total).toFixed(1)}%) | missing: ${T.miss} | no mfr_sku: ${T.noSku} ===`);
-  console.log(`gap list → /tmp/kravet_2026_gaps.csv (${T.miss+T.noSku} rows)`);
-})().catch(e=>{console.error(e);process.exit(1);});

← ceef3ac feat(staged): backup-freshness + secrets-env-backup LaunchAg  ·  back to Dw Yolo Loop  ·  refactor: add scripts/lib/shopify.mjs shared GQL client + mi d2d776f →