← back to Gmc Titlefix
5x/sweep.mjs
112 lines
#!/usr/bin/env node
/** TK-11448 /5x sweep — five independent checks over what actually shipped.
* Each is designed so it CAN go red, and each carries a CONTROL that must behave the opposite way,
* so a check that silently measures nothing is caught instead of printing green. */
import fs from 'fs';
import {findRecords} from '/Users/macstudio3/Projects/filemaker-mcp/src/fm-client.js';
const tok=(fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)||[])[1];
const SHOP='designer-laboratory-sandbox';
const defects=[]; const ledger=[];
const D=(check,msg)=>{defects.push({check,msg});console.log(' DEFECT ['+check+'] '+msg);};
async function gql(q,v){const r=await fetch(`https://${SHOP}.myshopify.com/admin/api/2024-10/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':tok,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v}),signal:AbortSignal.timeout(60000)});const j=await r.json();if(j.errors)throw new Error(JSON.stringify(j.errors).slice(0,200));return j.data;}
const arch=JSON.parse(fs.readFileSync('data/tk11448-archive-plan.json','utf8')).rows.filter(r=>r.verdict==='ARCHIVE');
const fix =JSON.parse(fs.readFileSync('data/tk11448-remediation-plan.json','utf8')).rows;
// ---- S1 Shopify status: 33 archived, 17 fix-set UNTOUCHED, keeper still ACTIVE
console.log('\nS1 Shopify status (archive set archived; fix set + keeper must still be ACTIVE)');
let a=0,f=0;
for(const r of arch){ const d=await gql('query($h:String!){productByHandle(handle:$h){status}}',{h:r.handle});
const s=d.productByHandle?.status; if(s!=='ARCHIVED'){D('S1',`${r.handle} expected ARCHIVED got ${s}`);}else a++; await new Promise(z=>setTimeout(z,120)); }
for(const r of fix){ const d=await gql('query($h:String!){productByHandle(handle:$h){status}}',{h:r.handle});
const s=d.productByHandle?.status; if(s!=='ACTIVE'){D('S1',`FIX-SET ${r.handle} must stay ACTIVE, got ${s}`);}else f++; await new Promise(z=>setTimeout(z,120)); }
const keep=await gql('query($h:String!){productByHandle(handle:$h){status}}',{h:'dwrw-76636'});
if(keep.productByHandle?.status!=='ACTIVE') D('S1','keeper dwrw-76636 must stay ACTIVE, got '+keep.productByHandle?.status);
console.log(` archived ${a}/${arch.length} | fix-set ACTIVE ${f}/${fix.length} | keeper ${keep.productByHandle?.status}`);
ledger.push(`S1 shopify: ${a}/${arch.length} archived, ${f}/${fix.length} fix-set intact`);
// ---- S2 Live storefront: archived handles must NOT serve a buyable page; a control must
console.log('\nS2 live storefront (archived -> not buyable; ACTIVE control -> buyable)');
// FIXED sweep 1: redirect:'follow' made an archived product look ALIVE — Shopify 302s a dead
// product URL to the HOMEPAGE, which returns 200, so "final status 200" measured the homepage,
// not the product. Classify at the product URL itself: 200 + add-to-cart = buyable, 404 = gone,
// 3xx = gone (soft-404 redirect away). Anything else is NOT_MEASURED, never a pass.
async function storefront(h){ try{
const r=await fetch(`https://www.designerwallcoverings.com/products/${h}`,{redirect:'manual',headers:{'User-Agent':'Mozilla/5.0'},signal:AbortSignal.timeout(25000)});
if(r.status>=300&&r.status<400) return {http:r.status,buyable:false,why:'redirects away (soft-404)'};
if(r.status===404) return {http:404,buyable:false,why:'404'};
if(r.status===200){ const b=await r.text();
if(/404 Not Found|Page not found/i.test(b)) return {http:200,buyable:false,why:'200 body is the 404 page'};
return {http:200,buyable:/add to cart|addtocart|product-form/i.test(b),why:'200 product page'}; }
return {http:r.status,buyable:null,why:'unexpected status'};
}catch(e){ return {http:null,err:String(e).slice(0,60)}; } }
let notBuyable=0,unread=0;
for(const r of arch){ const s=await storefront(r.handle);
if(s.http===null||s.buyable===null){unread++;D('S2',`${r.handle} storefront NOT_MEASURED (${s.err||s.why}) — not a pass`);}
else if(s.buyable) D('S2',`${r.handle} is STILL BUYABLE after archiving (${s.why})`);
else notBuyable++; await new Promise(z=>setTimeout(z,400)); }
// CONTROL: a product that is still ACTIVE must come back BUYABLE. Without this the fixed probe
// could call everything "gone" and look perfectly green while detecting nothing at all.
const ctl=await storefront(fix[0].handle);
if(ctl.buyable!==true) D('S2',`CONTROL ${fix[0].handle} is ACTIVE but probe says buyable=${ctl.buyable} (${ctl.why||ctl.err}) — the probe is not detecting live products`);
console.log(` archived-not-buyable ${notBuyable}/${arch.length} | not-measured ${unread} | control(${fix[0].handle}) http=${ctl.http} buyable=${ctl.buyable}`);
ledger.push(`S2 storefront: ${notBuyable}/${arch.length} gone, control buyable=${ctl.buyable}`);
// ---- S2b FAULT INJECTION: prove the probe can go RED, not just green.
console.log('\nS2b fault injection (probe must disagree between a live and a dead product)');
const live=await storefront(fix[1].handle); // ACTIVE -> must be buyable
const dead=await storefront('this-handle-does-not-exist-tk11448'); // must be gone
if(live.buyable!==true) D('S2b',`injected LIVE product ${fix[1].handle} read as buyable=${live.buyable} — probe cannot see live products`);
if(dead.buyable!==false) D('S2b',`injected DEAD handle read as buyable=${dead.buyable} — probe cannot see dead products`);
console.log(` live(${fix[1].handle})=${live.buyable} dead(bogus)=${dead.buyable} -> probe discriminates: ${live.buyable===true&&dead.buyable===false}`);
ledger.push(`S2b fault-injection: discriminates=${live.buyable===true&&dead.buyable===false}`);
// ---- S3 FileMaker: 105 dated + NEGATIVE control (an untouched product must still be blank)
console.log('\nS3 FileMaker (our rows dated; an UNTOUCHED product must still be blank)');
const sv=JSON.parse(fs.readFileSync('data/tk11448-fm-survey.json','utf8'));
let rows=0,dated=0;
for(const s of sv.rows){ try{ const r=await findRecords('WALLPAPER','REPORT ON SAMPLES ORDERED',[{'combo sku':`==${s.combo}`}],{limit:200});
if(!r||!Array.isArray(r.records)) throw new Error('UNEXPECTED_SHAPE');
for(const x of r.records){rows++; if(String(x.fieldData['Date Discontinued']||'').trim())dated++;} }
catch(e){ D('S3',`${s.combo} unreadable: ${String(e).slice(0,60)}`);} await new Promise(z=>setTimeout(z,150)); }
if(rows!==dated) D('S3',`${rows-dated} of ${rows} rows still blank`);
const negCombo=fix[0].handle.replace(/-/g,'').toUpperCase();
try{ const n=await findRecords('WALLPAPER','REPORT ON SAMPLES ORDERED',[{'combo sku':`==${negCombo}`}],{limit:50});
const nd=(n.records||[]).filter(x=>String(x.fieldData['Date Discontinued']||'').trim()).length;
if(nd>0) D('S3',`NEGATIVE CONTROL ${negCombo} has ${nd} discontinued rows — I may have over-stamped`);
console.log(` our rows ${dated}/${rows} dated | negative control ${negCombo}: ${n.records.length} rows, ${nd} dated (expect 0)`);
}catch(e){ D('S3','negative control unreadable: '+String(e).slice(0,60)); }
ledger.push(`S3 filemaker: ${dated}/${rows} dated`);
// ---- S4 Ledger integrity: every write has a recoverable undo
console.log('\nS4 undo-ledger integrity');
const sl=fs.readFileSync('data/tk11448-archive-ledger.jsonl','utf8').trim().split('\n').map(JSON.parse);
const fl=fs.readFileSync('data/tk11448-fm-ledger.jsonl','utf8').trim().split('\n').map(JSON.parse);
if(sl.length!==arch.length) D('S4',`shopify ledger ${sl.length} rows vs ${arch.length} archived`);
if(fl.length!==rows) D('S4',`fm ledger ${fl.length} rows vs ${rows} FM rows`);
for(const x of sl) if(x.prior_status!=='ACTIVE'||!x.productId||!x.undo) D('S4',`shopify ledger row ${x.handle} incomplete`);
for(const x of fl) if(x.prior_value!==''||!x.recordId||!x.undo) D('S4',`fm ledger row ${x.recordId} incomplete`);
const slh=new Set(sl.map(x=>x.handle)); for(const r of arch) if(!slh.has(r.handle)) D('S4',`no undo row for ${r.handle}`);
console.log(` shopify ledger ${sl.length} rows | fm ledger ${fl.length} rows | all have prior value + undo`);
ledger.push(`S4 ledgers: ${sl.length}+${fl.length} undo rows`);
// ---- S5 Re-derive the headline measurement claims independently
console.log('\nS5 re-derive claims');
const ver=JSON.parse(fs.readFileSync('data/tk11448-screenshot-shopify-verify.json','utf8'));
if(ver.measured!==ver.population) D('S5',`631 claim: measured ${ver.measured} != population ${ver.population}`);
if((ver.tally.STILL_SCREENSHOT||0)!==0) D('S5',`631 claim: ${ver.tally.STILL_SCREENSHOT} still carry the screenshot`);
const sweep=JSON.parse(fs.readFileSync('data/tk11448-placeholder-sweep.json','utf8'));
if(sweep.not_measured!==0) D('S5',`placeholder sweep has ${sweep.not_measured} NOT_MEASURED`);
if(sweep.hits!==51) D('S5',`placeholder hits ${sweep.hits} != 51`);
const overlap=fix.filter(r=>arch.some(x=>x.handle===r.handle));
if(overlap.length) D('S5',`archive/fix overlap: ${overlap.map(r=>r.handle).join(',')}`);
if(arch.length+fix.length+1!==sweep.hits) D('S5',`accounting ${arch.length}+${fix.length}+1 != ${sweep.hits}`);
console.log(` 631: ${ver.measured}/${ver.population} measured, ${ver.tally.STILL_SCREENSHOT||0} still screenshot | placeholder ${sweep.hits} hits, ${sweep.not_measured} unmeasured | ${arch.length}+${fix.length}+1=${sweep.hits}`);
ledger.push(`S5 claims re-derived`);
console.log('\n================ SWEEP RESULT ================');
console.log(ledger.join('\n'));
console.log(`DEFECTS: ${defects.length}`);
for(const d of defects) console.log(' -',d.check,d.msg);
fs.writeFileSync('5x/last-sweep.json',JSON.stringify({ts:new Date().toISOString(),defects,ledger},null,2));
process.exit(defects.length?1:0);