← 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
D scripts/cole-son-price-audit.jsA scripts/contract-scope-auditor/contract-scope-auditor.mjsD scripts/kravet-2026-coverage-audit.js
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 →