[object Object]

← back to York Reprice 2026 08

York cadence fixes: reprice valid-price-set (dup MAP rows) + bridge single-roll-aware + NO_WRITE test mode

25d9e88a409756bb87df2455e5166b1f7fedcf2d · 2026-07-28 15:52:59 -0700 · Steve Abrams

- york-cadence: collect ALL master MAP rows per SKU into a set; live matching any is not drift -> reprice-drift 3->0 (was false-positive on 39 dup-MAP SKUs)
- bridge-reprice: load roll_price_single, skip when live already at single-roll -> WOULD WRITE 279->3 (135 naturals no longer wrongly pushed to MAP)
- york-cadence: add YORK_CADENCE_NO_WRITE=1 read-only test mode (no latest.json/memo/CNCP/auto-onboard writes)

Files touched

Diff

commit 25d9e88a409756bb87df2455e5166b1f7fedcf2d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 28 15:52:59 2026 -0700

    York cadence fixes: reprice valid-price-set (dup MAP rows) + bridge single-roll-aware + NO_WRITE test mode
    
    - york-cadence: collect ALL master MAP rows per SKU into a set; live matching any is not drift -> reprice-drift 3->0 (was false-positive on 39 dup-MAP SKUs)
    - bridge-reprice: load roll_price_single, skip when live already at single-roll -> WOULD WRITE 279->3 (135 naturals no longer wrongly pushed to MAP)
    - york-cadence: add YORK_CADENCE_NO_WRITE=1 read-only test mode (no latest.json/memo/CNCP/auto-onboard writes)
---
 bridge-reprice.mjs       | 10 ++++++++++
 cadence/york-cadence.mjs | 17 +++++++++++++----
 2 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/bridge-reprice.mjs b/bridge-reprice.mjs
index ec080d5..effc712 100644
--- a/bridge-reprice.mjs
+++ b/bridge-reprice.mjs
@@ -1,4 +1,5 @@
 import fs from 'node:fs';
+import { execSync } from 'node:child_process';
 const SHOP='designer-laboratory-sandbox.myshopify.com',VER='2024-10';
 const TOKEN=(fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]?.trim();
 const URL=`https://${SHOP}/admin/api/${VER}/graphql.json`;
@@ -13,6 +14,14 @@ const words=s=>new Set(String(s||'').toLowerCase().replace(/[^a-z0-9 ]/g,' ').sp
 const share=(a,b)=>{const A=words(a),B=words(b);for(const w of A)if(B.has(w))return true;return false;};
 const M=new Map();
 for(const line of fs.readFileSync('data/master-priced-by-sku.csv','utf8').trim().split('\n').slice(1)){const c=line.split(',');M.set(norm(c[0]),{newp:parseFloat(c[1]),fb:c[2]==='1',status:c[3],pattern:c[4]});}
+// SINGLE-ROLL awareness (parity with york-cadence): naturals (grasscloth/sisal/weave/paperweave/cork)
+// sell at brewster_york_master.roll_price_single, NOT MAP. Without this, master-priced-by-sku.csv (MAP-only)
+// false-flagged every correctly-priced natural as a reprice and would push it UP to MAP (135 of 279 on 2026-07-28).
+const RPS=new Map();
+try{ const out=execSync(`PGPASSWORD=DW2024! /opt/homebrew/opt/postgresql@14/bin/psql -h 127.0.0.1 -U dw_admin -d dw_unified -tA -c "select upper(trim(mfr_sku))||chr(31)||roll_price_single from brewster_york_master where roll_price_single>0"`,{encoding:'utf8'});
+  for(const line of out.trim().split('\n').filter(Boolean)){const [s,p]=line.split(String.fromCharCode(31)); if(!RPS.has(s))RPS.set(s,parseFloat(p));}
+  console.log(`  single-roll targets loaded: ${RPS.size}`);
+}catch(e){ console.log('  ⚠ single-roll load failed (falling back to MAP-only):',String(e.message||e).slice(0,80)); }
 let cur=null, live=[];
 for(let pg=0;pg<90;pg++){
   const Q=`query($c:String){products(first:60,after:$c,query:"vendor:'Jeffrey Stevens'"){pageInfo{hasNextPage endCursor} nodes{id status title dwc:metafield(namespace:"dwc",key:"manufacturer_sku"){value} cust:metafield(namespace:"custom",key:"manufacturer_sku"){value} variants(first:20){nodes{id sku title price}}}}}`;
@@ -29,6 +38,7 @@ for(const p of live){
   const m=M.get(p.mfr); if(!m){b.notmaster++;continue;}
   if(!['Active','New'].includes(m.status)||p.status!=='ACTIVE'){b.notsell++;continue;}
   const map=m.newp,cur=p.cur; if(Math.abs(cur-map)<0.01){b.already++;continue;}
+  const rps=RPS.get(p.mfr); if(rps&&Math.abs(cur-rps)<0.01){b.already++;continue;} // already at valid single-roll price (naturals) — not a reprice
   if(m.pattern&&!share(p.title,m.pattern)){b.mismatch.push({mfr:p.mfr,master:m.pattern,live:p.title});continue;}
   const rec={pid:p.pid,vid:p.vid,mfr:p.mfr,cur,map,fb:m.fb,title:p.title};
   if(cur<=10)b.fix.push(rec); else if(map/cur>2.5||map/cur<0.4)b.anomaly.push(rec);
diff --git a/cadence/york-cadence.mjs b/cadence/york-cadence.mjs
index 7efd101..ce2dba5 100644
--- a/cadence/york-cadence.mjs
+++ b/cadence/york-cadence.mjs
@@ -12,6 +12,7 @@
 import fs from 'node:fs'; import { execSync } from 'node:child_process';
 import { autoOnboard } from './york-auto-onboard.mjs';  // the ONE Shopify write: New→DRAFT, cap 100/run
 const ROOT=process.env.HOME+'/Projects/york-reprice-2026-08';
+const NO_WRITE=process.env.YORK_CADENCE_NO_WRITE==='1'; // read-only test mode: no latest.json/memo/CNCP/auto-onboard writes
 const SHOP='designer-laboratory-sandbox.myshopify.com',VER='2024-10';
 const TOKEN=(fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]?.trim();
 const URL=`https://${SHOP}/admin/api/${VER}/graphql.json`;
@@ -30,9 +31,16 @@ const cents=x=>Math.round(Number(x)*100);
 const priceOff=(a,b)=>Math.abs(cents(a)-cents(b))>1; // "off" = more than a 1-cent rounding gap
 
 // 1. master: mfr_sku -> {price,status,pattern,color}
+// york_master_aug2026 has MULTIPLE rows per export_sku (two roll sizes → conflicting us_map, e.g.
+// NW3500 carries both $102 and $123.25; 39 SKUs fleet-wide). First-wins on price false-flagged a
+// correctly-priced live roll as "drift" whenever it matched the OTHER row's MAP. Collect ALL valid
+// MAP values per SKU into a set; a live price matching ANY of them is not drift (mirrors the
+// single-roll RPS carve-out below). status/pattern/color stay first-wins.
 const M=new Map();
 for(const line of q(`select concat_ws(chr(31), upper(trim(export_sku)), coalesce(us_map,round(us_msrp*0.75,2)), sku_status, pattern_name, colorway) from york_master_aug2026 where export_sku is not null`).split('\n').filter(Boolean)){
-  const [s,p,st,pat,col]=line.split(String.fromCharCode(31)); if(!M.has(s)) M.set(s,{price:parseFloat(p),status:st,pattern:pat,color:col});
+  const [s,p,st,pat,col]=line.split(String.fromCharCode(31)); const price=parseFloat(p);
+  if(!M.has(s)) M.set(s,{price,prices:new Set([price]),status:st,pattern:pat,color:col});
+  else M.get(s).prices.add(price);
 }
 // naturals sell at a SINGLE-ROLL price (brewster_york_master.roll_price_single), NOT MAP — a second
 // valid price target. Without this the MAP-only drift check false-flags every correctly-priced
@@ -67,7 +75,7 @@ for(let pg=0;pg<90;pg++){
       if(['Dead','Phasing'].includes(m.status)) disco.push({mfr,title:p.title,status:m.status});
       else if(['Active','New'].includes(m.status) && m.price>0 && share(p.title,m.pattern)){
         const rps=RPS.get(mfr);                                  // single-roll target (naturals)
-        const offMap=priceOff(price,m.price);                    // off from MAP
+        const offMap=![...m.prices].some(vp=>!priceOff(price,vp)); // off from EVERY valid master MAP row (dup rows = 2 roll sizes)
         const offRps=rps?priceOff(price,rps):true;               // off from single-roll (or none)
         if(offMap && offRps) reprice.push({mfr,title:p.title,cur:price,map:m.price,rps:rps||null});
       }
@@ -84,10 +92,11 @@ const onboardReady=onboardMfrs.length;
 const out={ts:new Date().toISOString(),live_js:liveMfr.size,
   reprice_drift:reprice.length, onboard_ready:onboardReady, disco:disco.length,
   reprice_sample:reprice.slice(0,10), onboard_sample:onboardMfrs.slice(0,12), disco_sample:disco.slice(0,10)};
+const actionable=reprice.length+onboardReady+disco.length;
+console.log(`york-cadence ${out.ts} | live JS ${liveMfr.size} | reprice-drift ${reprice.length} | onboard-ready ${onboardReady} | disco ${disco.length}${NO_WRITE?' | NO_WRITE (read-only test)':''}`);
+if(NO_WRITE){ console.log('  reprice_sample:',JSON.stringify(out.reprice_sample)); console.log('  onboard_sample:',JSON.stringify(out.onboard_sample.slice(0,6))); process.exit(0); }
 fs.mkdirSync(ROOT+'/cadence/data',{recursive:true});
 fs.writeFileSync(ROOT+'/cadence/data/latest.json',JSON.stringify(out,null,2));
-const actionable=reprice.length+onboardReady+disco.length;
-console.log(`york-cadence ${out.ts} | live JS ${liveMfr.size} | reprice-drift ${reprice.length} | onboard-ready ${onboardReady} | disco ${disco.length}`);
 if(actionable>0){
   const memo=`# York cadence — ${actionable} actionable (${out.ts.slice(0,10)})\n\n- **Reprice drift**: ${reprice.length} live ACTIVE below/above MAP → run york-pl-reprice / bridge-reprice (gated).\n- **Onboard-ready**: ${onboardReady} master 'New' with image + settlement-clear not yet live → run york-new-create (gated).\n- **Disco**: ${disco.length} live ACTIVE now Dead/Phasing → archive (gated).\n\nData: cadence/data/latest.json. All customer-facing writes stay gated — this is a surface, not an auto-apply.\n\n- [ ] APPROVE which action to run\n`;
   fs.writeFileSync(process.env.HOME+'/.claude/yolo-queue/pending-approval/york-cadence-actions.md',memo);

← 9ee892f York cadence 7/28: verify+stage — NO-GO all 3 (reprice+onboa  ·  back to York Reprice 2026 08  ·  York cadence/bridge: fix scan-truncation cap (90x60=5400 -> 4b91284 →