[object Object]

← back to Designerwallcoverings

guard TK-11357 Fix D: per-variant $0 gate in the two activate-1838 activators

2fe073d26372b1f34a003ee2233dd30ce5f93be5 · 2026-09-10 09:27:04 -0700 · Steve

PRE-EXISTING GUARD STATE (verified, not assumed):
  activate-1838.mjs — had a PRODUCT-level gate only: `maxVariantPrice.amount > 5` (line 20).
    That excludes sample-only products but does NOT stop a $0 variant riding alongside a priced
    one: the loop then stamped 2026 on p.variants.nodes.map(...) i.e. EVERY variant. This is the
    archetype the brief names (TARGET/quantity 2026 with a product-level price check).
  activate-1838-showroom.mjs — NO price gate at all. It deliberately targets the complementary
    maxVariantPrice <= 5 bucket, which INCLUDES all-$0 products, and stamped 2026 on every
    variant. Its only filter is a paste-tub title regex.

FIX: both now decide PER VARIANT via the shared guard (lib/inventory-stamp-guard.mjs).
activate-1838.mjs gains `vendor tags` on the product and `title price` on the variant, which
the query did not fetch. activate-1838-showroom.mjs reads its products from /tmp/sl16.json,
which carries NO price/vendor/tags — price was structurally unavailable — so it now does a
read-only live Q_PROD fetch per product and refuses to stock blind if the product is missing.

NOTE (honesty): /tmp/sl16.json no longer exists, so activate-1838-showroom.mjs would today die
at readFileSync before reaching any write. The guard is defence-in-depth for a re-created input.

Proven by shopify/scripts/tk11357-source-fix-proof/predicate-proof.mjs (sibling repo):
4/4 required + 3/3 regression, predicate EXTRACTED from each file.
SOURCE-ONLY: nothing executed with --apply, no Shopify write. Reversible: git revert.

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

Files touched

Diff

commit 2fe073d26372b1f34a003ee2233dd30ce5f93be5
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Sep 10 09:27:04 2026 -0700

    guard TK-11357 Fix D: per-variant $0 gate in the two activate-1838 activators
    
    PRE-EXISTING GUARD STATE (verified, not assumed):
      activate-1838.mjs — had a PRODUCT-level gate only: `maxVariantPrice.amount > 5` (line 20).
        That excludes sample-only products but does NOT stop a $0 variant riding alongside a priced
        one: the loop then stamped 2026 on p.variants.nodes.map(...) i.e. EVERY variant. This is the
        archetype the brief names (TARGET/quantity 2026 with a product-level price check).
      activate-1838-showroom.mjs — NO price gate at all. It deliberately targets the complementary
        maxVariantPrice <= 5 bucket, which INCLUDES all-$0 products, and stamped 2026 on every
        variant. Its only filter is a paste-tub title regex.
    
    FIX: both now decide PER VARIANT via the shared guard (lib/inventory-stamp-guard.mjs).
    activate-1838.mjs gains `vendor tags` on the product and `title price` on the variant, which
    the query did not fetch. activate-1838-showroom.mjs reads its products from /tmp/sl16.json,
    which carries NO price/vendor/tags — price was structurally unavailable — so it now does a
    read-only live Q_PROD fetch per product and refuses to stock blind if the product is missing.
    
    NOTE (honesty): /tmp/sl16.json no longer exists, so activate-1838-showroom.mjs would today die
    at readFileSync before reaching any write. The guard is defence-in-depth for a re-created input.
    
    Proven by shopify/scripts/tk11357-source-fix-proof/predicate-proof.mjs (sibling repo):
    4/4 required + 3/3 regression, predicate EXTRACTED from each file.
    SOURCE-ONLY: nothing executed with --apply, no Shopify write. Reversible: git revert.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 scripts/activate-1838/activate-1838-showroom.mjs | 32 +++++++++++++++++++++++-
 scripts/activate-1838/activate-1838.mjs          | 29 +++++++++++++++++++--
 2 files changed, 58 insertions(+), 3 deletions(-)

diff --git a/scripts/activate-1838/activate-1838-showroom.mjs b/scripts/activate-1838/activate-1838-showroom.mjs
index c709c5a..2c93370 100644
--- a/scripts/activate-1838/activate-1838-showroom.mjs
+++ b/scripts/activate-1838/activate-1838-showroom.mjs
@@ -19,6 +19,32 @@ held.forEach(p=>console.log(`  HOLD: ${p.title.slice(0,44)}`));
 console.log(`\n${APPLY?'APPLYING':'DRY-RUN'} on ${wallcoverings.length} wallcoverings\n`);
 if(!APPLY){wallcoverings.forEach(p=>console.log(`  would SL: ${p.title.slice(0,44)} | ${p.variants.nodes.length}var`));process.exit(0);}
 
+// GUARD TK-11357: /tmp/sl16.json carries no price/vendor/tags, so the guard's inputs do not
+// exist in this script's input file — fetch them LIVE (read-only) before any inventory write.
+const Q_PROD=`query($id:ID!){product(id:$id){id vendor tags variants(first:20){nodes{id title price inventoryItem{id tracked}}}}}`;
+import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs';  // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+  return (variants || []).map(v => ({
+    inventoryItemId: v.inventoryItem.id,
+    locationId,
+    quantity: Number(v.price) > 0
+      ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+      : 0,
+  }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
 const M_TAG=`mutation($id:ID!,$tags:[String!]!){tagsAdd(id:$id,tags:$tags){userErrors{message}}}`;
 const M_MF=`mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){userErrors{field message}}}`;
 const M_STATUS=`mutation($id:ID!){productUpdate(input:{id:$id,status:ACTIVE}){product{status} userErrors{message}}}`;
@@ -29,7 +55,11 @@ for(const p of wallcoverings){
     let r=await gql(M_TAG,{id:p.id,tags:['Showroom Line']}); if(r.data?.tagsAdd?.userErrors?.length)throw new Error('tag:'+JSON.stringify(r.data.tagsAdd.userErrors));
     r=await gql(M_MF,{mf:[{ownerId:p.id,namespace:'custom',key:'showroom_line',value:'true',type:'boolean'}]}); if(r.data?.metafieldsSet?.userErrors?.length)throw new Error('mf:'+JSON.stringify(r.data.metafieldsSet.userErrors));
     r=await gql(M_STATUS,{id:p.id}); if(r.data?.productUpdate?.userErrors?.length)throw new Error('status:'+JSON.stringify(r.data.productUpdate.userErrors));
-    const quantities=p.variants.nodes.map(v=>({inventoryItemId:v.inventoryItem.id,locationId:LOCATION,quantity:2026}));
+    // GUARD TK-11357: this script deliberately targets the maxVariantPrice<=5 bucket, which
+    // INCLUDES all-$0 products — previously it stamped 2026 on every one of their variants.
+    const live=(await gql(Q_PROD,{id:p.id})).data?.product;
+    if(!live) throw new Error('guard: product not found live — refusing to stock blind');
+    const quantities=safeQuantities(live,live.variants.nodes,LOCATION,2026);
     r=await gql(M_INV,{input:{reason:'correction',name:'available',ignoreCompareQuantity:true,quantities}}); if(r.data?.inventorySetQuantities?.userErrors?.length)throw new Error('inv:'+JSON.stringify(r.data.inventorySetQuantities.userErrors));
     ok++; console.log(`  ✓ ${p.title.slice(0,42)} | Showroom Line + ACTIVE + inv2026 x${quantities.length}`);
   }catch(e){fail++;errs.push(`${p.handle}: ${e.message}`);console.log(`  ✗ ${p.title.slice(0,42)} — ${e.message.slice(0,90)}`);}
diff --git a/scripts/activate-1838/activate-1838.mjs b/scripts/activate-1838/activate-1838.mjs
index c2542dc..b9e759b 100644
--- a/scripts/activate-1838/activate-1838.mjs
+++ b/scripts/activate-1838/activate-1838.mjs
@@ -15,7 +15,30 @@ async function gql(q,v){for(let t=0;t<5;t++){const r=await fetch(API,{method:'PO
 
 // 1. gather all draft 1838 with variant inventoryItem ids
 let cursor=null, prods=[];
-const Q=`query($c:String){products(first:100,after:$c,query:"vendor:'1838 Wallcoverings' status:draft"){pageInfo{hasNextPage endCursor} nodes{id title handle priceRangeV2{maxVariantPrice{amount}} variants(first:10){nodes{id inventoryItem{id tracked}}}}}}`;
+const Q=`query($c:String){products(first:100,after:$c,query:"vendor:'1838 Wallcoverings' status:draft"){pageInfo{hasNextPage endCursor} nodes{id title handle vendor tags priceRangeV2{maxVariantPrice{amount}} variants(first:10){nodes{id title price inventoryItem{id tracked}}}}}}`;
+import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs';  // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+  return (variants || []).map(v => ({
+    inventoryItemId: v.inventoryItem.id,
+    locationId,
+    quantity: Number(v.price) > 0
+      ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+      : 0,
+  }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
 do{const d=await gql(Q,{c:cursor});const pg=d.data.products;prods.push(...pg.nodes);cursor=pg.pageInfo.hasNextPage?pg.pageInfo.endCursor:null;}while(cursor);
 const real=prods.filter(p=>parseFloat(p.priceRangeV2.maxVariantPrice.amount)>5);
 const sampleOnly=prods.filter(p=>parseFloat(p.priceRangeV2.maxVariantPrice.amount)<=5);
@@ -31,7 +54,9 @@ for(const p of targets){
   try{
     const s=await gql(M_STATUS,{id:p.id});
     const se=s.data?.productUpdate?.userErrors||[]; if(se.length)throw new Error('status: '+JSON.stringify(se));
-    const quantities=p.variants.nodes.map(v=>({inventoryItemId:v.inventoryItem.id,locationId:LOCATION,quantity:2026}));
+    // GUARD TK-11357: the maxVariantPrice>5 filter above is PRODUCT-level, so a $0 variant riding
+    // alongside a priced one still passed it. Decide per VARIANT.
+    const quantities=safeQuantities(p,p.variants.nodes,LOCATION,2026);
     const iv=await gql(M_INV,{input:{reason:'correction',name:'available',ignoreCompareQuantity:true,quantities}});
     const ie=iv.data?.inventorySetQuantities?.userErrors||[]; if(ie.length)throw new Error('inv: '+JSON.stringify(ie));
     ok++; console.log(`  ✓ ${p.title.slice(0,42)} | ACTIVE + inv2026 x${quantities.length}`);

← fab7c61 guard TK-11357 Fix D: never stock a $0/quote-only variant in  ·  back to Designerwallcoverings  ·  guard TK-11357 Fix D: don't stock a $0 Roll in osborne roll- 077ac09 →