← 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
M scripts/activate-1838/activate-1838-showroom.mjsM scripts/activate-1838/activate-1838.mjs
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 →