← back to Designer Wallcoverings
TK-10002: PR base-collision regression fix (dry-run tooling, GATED — not executed)
f9d5538baddf864ea79d550bebd7bfa46be0b192 · 2026-07-29 21:15:03 -0700 · Steve
/yolo census found BATCH-1's 3 derived PR reassigns collided into older 'Weave' products
(pre-existing -Yard base collision the mirror hid; my -Sample write propagated it). Fix applier
mints DWPX-500116/117/118 for the named products (Weave keeps 500044/45/46), WITH the live
new-SKU uniqueness guard apply-residual.js lacked. Drafted to pending-approval; awaits Steve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A scripts/tk10002-null-sample/fix-pr-base-collision.js
Diff
commit f9d5538baddf864ea79d550bebd7bfa46be0b192
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 29 21:15:03 2026 -0700
TK-10002: PR base-collision regression fix (dry-run tooling, GATED — not executed)
/yolo census found BATCH-1's 3 derived PR reassigns collided into older 'Weave' products
(pre-existing -Yard base collision the mirror hid; my -Sample write propagated it). Fix applier
mints DWPX-500116/117/118 for the named products (Weave keeps 500044/45/46), WITH the live
new-SKU uniqueness guard apply-residual.js lacked. Drafted to pending-approval; awaits Steve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
.../tk10002-null-sample/fix-pr-base-collision.js | 87 ++++++++++++++++++++++
1 file changed, 87 insertions(+)
diff --git a/scripts/tk10002-null-sample/fix-pr-base-collision.js b/scripts/tk10002-null-sample/fix-pr-base-collision.js
new file mode 100644
index 00000000..8fc75fb0
--- /dev/null
+++ b/scripts/tk10002-null-sample/fix-pr-base-collision.js
@@ -0,0 +1,87 @@
+#!/usr/bin/env node
+/**
+ * TK-10002 REGRESSION FIX — PR base-SKU collisions (3 pairs). GATED (Steve which-keeps + live write).
+ * DRY-RUN by default; APPLY=1 + approval artifact required.
+ *
+ * Discovered 2026-07-29 during the /yolo census: Oro Veneziano/Argento Bianco/Foglia d'Oro each
+ * share their DWPX base (BOTH -Yard sellable AND -Sample) with an older "…Weave" product
+ * (created ~80s earlier in the same 2026-04-06 import). The -Yard collision was PRE-EXISTING and
+ * mirror-hidden; the BATCH-1 derived-SKU reassign propagated it to -Sample. Oldest-keeps → the
+ * Weave products keep 500044/45/46; the named products get FRESH minted bases (both variants).
+ *
+ * BUG THIS FIXES IN TOOLING: apply-residual.js had NO live new-SKU uniqueness check. This applier
+ * verifies each new base is free on all NON-ARCHIVED products before writing (the missing guard).
+ */
+const { Pool } = require('pg');
+const fs = require('fs');
+const { requireApproval } = require('../../shopify/scripts/lib/approval-gate');
+const pool = new Pool({ host:'/tmp', database:'dw_unified', user: require('os').userInfo().username });
+const TOKEN = fs.readFileSync(require('path').join(process.env.HOME,'Projects/Designer-Wallcoverings/shopify/.env'),'utf8')
+ .split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=').slice(1).join('=').replace(/['"]/g,'').trim();
+const STORE='designer-laboratory-sandbox.myshopify.com', API='2024-10';
+const APPLY = process.env.APPLY === '1';
+const sleep = ms => new Promise(r=>setTimeout(r,ms));
+
+// named product gets a fresh base; the older "Weave" twin keeps the old base
+const TARGETS = [
+ { pid:'7813312315443', label:'Oro Veneziano', oldBase:'DWPX-500044', newBase:'DWPX-500116', keeper:'Light Weave (7813310742579)' },
+ { pid:'7813312348211', label:'Argento Bianco', oldBase:'DWPX-500045', newBase:'DWPX-500117', keeper:'Umber Weave (7813310775347)' },
+ { pid:'7813312380979', label:"Foglia d'Oro", oldBase:'DWPX-500046', newBase:'DWPX-500118', keeper:'Basket Weave (7813310808115)' },
+];
+async function gql(query,variables){
+ for(let a=0;a<5;a++){
+ const r=await fetch(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query,variables})});
+ if(r.status===429){await sleep(2000);continue;}
+ const j=await r.json();
+ if((j.errors||[]).some(e=>/throttl/i.test(e.message||''))){await sleep(2000);continue;}
+ if(j.errors) throw new Error('GQL '+JSON.stringify(j.errors));
+ return j.data;
+ }
+ throw new Error('gql retries exhausted');
+}
+const MUT=`mutation($productId:ID!,$variants:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$productId,variants:$variants){ productVariants{ id inventoryItem{ sku } } userErrors{ field message } } }`;
+
+// THE MISSING GUARD: is newBase already carried by any NON-ARCHIVED product live?
+async function baseFreeLive(newBase){
+ for(const suf of ['-Yard','-Sample','-Roll','']){
+ const sku=newBase+suf; if(!sku) continue;
+ const d=await gql(`{ productVariants(first:10, query:"sku:${sku}"){ nodes{ product{ status } inventoryItem{ sku } } } }`);
+ const active=(d.productVariants.nodes||[]).filter(n=>String(n.product.status).toUpperCase()!=='ARCHIVED' && n.inventoryItem?.sku===sku);
+ if(active.length) return { free:false, conflict:sku };
+ }
+ return { free:true };
+}
+
+async function main(){
+ const gate=requireApproval({ ticket:'TK-10002', scope:'residual-collision-cleanup', apply:APPLY });
+ console.log(`MODE: ${APPLY?'APPLY (LIVE WRITE)':'DRY-RUN'} gate=${gate.mode}\n`);
+ const results={applied:[],failed:[],skipped:[]};
+ for(const t of TARGETS){
+ const gid=`gid://shopify/Product/${t.pid}`;
+ const guard=await baseFreeLive(t.newBase);
+ if(!guard.free){ results.skipped.push({...t,reason:'newBase not free: '+guard.conflict}); console.log('⚠',t.label,'newBase',t.newBase,'NOT free (',guard.conflict,') — SKIP'); continue; }
+ const d=await gql(`{ product(id:"${gid}"){ status variants(first:20){ nodes{ id title inventoryItem{ sku } } } } }`);
+ const plan=(d.product.variants.nodes||[]).filter(v=>String(v.inventoryItem?.sku||'').startsWith(t.oldBase))
+ .map(v=>({id:v.id, title:v.title, old:v.inventoryItem.sku, neu:v.inventoryItem.sku.replace(t.oldBase,t.newBase)}));
+ console.log(`${t.label} (${t.pid}) [status ${d.product.status}] keeper=${t.keeper} ${t.oldBase} -> ${t.newBase}`);
+ plan.forEach(x=>console.log(` ${APPLY?'':'[dry] '}${x.title}: '${x.old}' -> '${x.neu}'`));
+ if(!APPLY) continue;
+ // register new base
+ await pool.query(`INSERT INTO dw_sku_registry(dw_sku,vendor_prefix,vendor_name,mfr_sku,shopify_product_id,status,created_at,updated_at)
+ VALUES($1,'DWPX','Phillipe Romano',NULL,$2,'active',NOW(),NOW()) ON CONFLICT DO NOTHING`,[t.newBase,gid]);
+ // PG mirror first (rewrite the row's scalar to the new sample base)
+ await pool.query(`UPDATE shopify_products SET dw_sku=$2, variant_sku=$3, synced_at=NOW() WHERE split_part(shopify_id,'/',5)=$1`,[t.pid,t.newBase,`${t.newBase}-Sample`]);
+ try{
+ const r=await gql(MUT,{productId:gid,variants:plan.map(x=>({id:x.id,inventoryItem:{sku:x.neu}}))});
+ const ue=r.productVariantsBulkUpdate.userErrors;
+ if(ue.length){ results.failed.push({...t,ue}); console.log(' ✗',JSON.stringify(ue)); }
+ else { results.applied.push({...t,wrote:r.productVariantsBulkUpdate.productVariants.map(v=>v.inventoryItem.sku)}); console.log(' ✓ ->',results.applied.at(-1).wrote.join(', ')); }
+ }catch(e){ results.failed.push({...t,err:e.message}); console.log(' ✗',e.message); }
+ await sleep(300);
+ }
+ fs.writeFileSync('/tmp/tk10002_pr_basefix_results.json',JSON.stringify(results,null,2));
+ console.log(`\nDONE: applied=${results.applied.length} skipped=${results.skipped.length} failed=${results.failed.length}`);
+ if(!APPLY) console.log('(DRY-RUN — gated: needs Steve OK on Weave-keeps + APPLY=1)');
+ await pool.end();
+}
+main().catch(e=>{console.error(e);process.exit(1);});
← d516e317 TK-10002 MEDIUM batch: Wavy minted DWPX-500115 + Wilderness/
·
back to Designer Wallcoverings
·
auto-save: 2026-07-29T21:42:29 (1 files) — scripts/tk10002-n dd4a842f →