← back to Sanderson Onboard
TK-11357 Fix D: route create_sdg's inventory stamp through the shared $0 guard (defence-in-depth)
edc210b268d1d6028ed6feb9ae4e5ee878996f61 · 2026-09-10 09:30:29 -0700 · Steve
NOT a live-risk closure — stated plainly. create_sdg.mjs is ALREADY effectively guarded:
gate() pushes 'no-price' when !(parseFloat(it.retail_usd) > 0) and the caller `continue`s on
SKIP-HELD before any product create or inventory mutation; and it is CREATE-ONLY and
manifest-bounded (manifest-{harlequin,morris,sanderson,zoffany}.json = 5,655 items, ZERO
Phillipe Romano, no productByHandle / no products(first: sweep), so it only writes to pids it
created and cannot re-inflate a pre-existing cohort). Verified, not assumed.
What it lacked was the guarantee AT THE WRITE SITE. goLive() re-reads the product and stamped
TARGET_QTY on every variant, and the finish-pending path calls goLive() on drafts created by an
EARLIER run whose live prices it never re-checks. The stamp now goes through the shared guard
(lib/inventory-stamp-guard.mjs, vendored) so the invariant holds at the mutation itself rather
than only in an upstream manifest field.
Q_V now also selects variant title+price and product vendor+tags. Confirmed empirically safe for
this cadence: all 4 manifests carry ZERO quote/needs-price/contact-for-price tags and no vendor
is a price-suppressed line, so the guard's quote-only branch cannot fire and this is a strict
no-op for today's population — it only changes behaviour if a $0 variant ever reaches the stamp.
This matters because com.steve.sdg-publish-cadence is LOADED and runs cadence_resume.sh daily
at 04:30 PDT, so the write site is unattended.
Proven by Designer-Wallcoverings/shopify/scripts/tk11357-source-fix-proof/predicate-proof.mjs:
4/4 required + 3/3 regression on the predicate EXTRACTED from this file.
SOURCE-ONLY: nothing run with --apply, no Shopify write. Reversible: git revert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
M scripts/create_sdg.mjsA scripts/lib/inventory-stamp-guard.mjs
Diff
commit edc210b268d1d6028ed6feb9ae4e5ee878996f61
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 10 09:30:29 2026 -0700
TK-11357 Fix D: route create_sdg's inventory stamp through the shared $0 guard (defence-in-depth)
NOT a live-risk closure — stated plainly. create_sdg.mjs is ALREADY effectively guarded:
gate() pushes 'no-price' when !(parseFloat(it.retail_usd) > 0) and the caller `continue`s on
SKIP-HELD before any product create or inventory mutation; and it is CREATE-ONLY and
manifest-bounded (manifest-{harlequin,morris,sanderson,zoffany}.json = 5,655 items, ZERO
Phillipe Romano, no productByHandle / no products(first: sweep), so it only writes to pids it
created and cannot re-inflate a pre-existing cohort). Verified, not assumed.
What it lacked was the guarantee AT THE WRITE SITE. goLive() re-reads the product and stamped
TARGET_QTY on every variant, and the finish-pending path calls goLive() on drafts created by an
EARLIER run whose live prices it never re-checks. The stamp now goes through the shared guard
(lib/inventory-stamp-guard.mjs, vendored) so the invariant holds at the mutation itself rather
than only in an upstream manifest field.
Q_V now also selects variant title+price and product vendor+tags. Confirmed empirically safe for
this cadence: all 4 manifests carry ZERO quote/needs-price/contact-for-price tags and no vendor
is a price-suppressed line, so the guard's quote-only branch cannot fire and this is a strict
no-op for today's population — it only changes behaviour if a $0 variant ever reaches the stamp.
This matters because com.steve.sdg-publish-cadence is LOADED and runs cadence_resume.sh daily
at 04:30 PDT, so the write site is unattended.
Proven by Designer-Wallcoverings/shopify/scripts/tk11357-source-fix-proof/predicate-proof.mjs:
4/4 required + 3/3 regression on the predicate EXTRACTED from this file.
SOURCE-ONLY: nothing run with --apply, no Shopify write. Reversible: git revert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
scripts/create_sdg.mjs | 40 ++++++++++++++++--
scripts/lib/inventory-stamp-guard.mjs | 80 +++++++++++++++++++++++++++++++++++
2 files changed, 117 insertions(+), 3 deletions(-)
diff --git a/scripts/create_sdg.mjs b/scripts/create_sdg.mjs
index ac20acf..b6bd725 100644
--- a/scripts/create_sdg.mjs
+++ b/scripts/create_sdg.mjs
@@ -82,7 +82,30 @@ async function gql(query, variables) {
return j.data;
}
}
-const Q_V = `query($id:ID!){product(id:$id){status variants(first:10){edges{node{id sku inventoryItem{id}}}}}}`;
+const Q_V = `query($id:ID!){product(id:$id){status vendor tags variants(first:10){edges{node{id sku title price inventoryItem{id}}}}}}`;
+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_TRACK = `mutation($id:ID!){inventoryItemUpdate(id:$id,input:{tracked:true}){userErrors{message}}}`;
const M_ACT = `mutation($iid:ID!,$loc:ID!){inventoryActivate(inventoryItemId:$iid,locationId:$loc){userErrors{message}}}`;
const M_QTY = `mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{message}}}`;
@@ -92,9 +115,20 @@ const M_ACTIVE = `mutation($id:ID!){productUpdate(input:{id:$id,status:ACTIVE}){
async function goLive(pid) {
const gid = `gid://shopify/Product/${pid}`;
const d = await gql(Q_V, { id: gid });
- const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ const vnodes = d.product.variants.edges.map(e => e.node);
+ const items = vnodes.map(v => v.inventoryItem.id);
+ // GUARD TK-11357 (DEFENCE-IN-DEPTH, not a live-risk closure). This script is already
+ // effectively guarded upstream: gate() pushes 'no-price' when !(parseFloat(retail_usd) > 0)
+ // and the caller `continue`s on SKIP-HELD BEFORE any create or inventory mutation, and it is
+ // CREATE-ONLY + manifest-bounded (harlequin/morris/sanderson/zoffany — zero Phillipe Romano),
+ // so it cannot re-inflate a pre-existing cohort. What it lacked was the guarantee AT THE WRITE
+ // SITE: goLive() re-reads the product and stamped TARGET_QTY on every variant, and the
+ // finish-pending path calls goLive() on drafts created by an EARLIER run whose live prices it
+ // never re-checks. Routing the stamp through the shared guard makes the invariant hold at the
+ // mutation itself rather than only in an upstream manifest field.
+ const quantities = safeQuantities(d.product, vnodes, LOCATION_ID, TARGET_QTY);
for (const iid of items) { await gql(M_TRACK, { id: iid }); await gql(M_ACT, { iid, loc: LOCATION_ID }); }
- await gql(M_QTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true, quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ await gql(M_QTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true, quantities } });
await gql(M_PUB, { id: gid, pubs: PUBLICATIONS.map(p => ({ publicationId: p })) });
const r = await gql(M_ACTIVE, { id: gid });
return { status: r.productUpdate?.product?.status, varCount: items.length };
diff --git a/scripts/lib/inventory-stamp-guard.mjs b/scripts/lib/inventory-stamp-guard.mjs
new file mode 100644
index 0000000..29d1810
--- /dev/null
+++ b/scripts/lib/inventory-stamp-guard.mjs
@@ -0,0 +1,80 @@
+// VENDORED COPY — canonical source: Designer-Wallcoverings/shopify/scripts/lib/inventory-stamp-guard.mjs
+// Vendored (not cross-repo-imported) on purpose: this repo ships and runs independently, and a
+// cross-repo relative import would hard-crash the scheduled cadence if either tree moved.
+// KEEP IN SYNC — the TK-11357 fixture harness hashes both copies and FAILS on drift.
+// TK-11357 (lineage TK-10825/10965/11140/11299/11301/11357).
+// TK-10965 — Fix B (prevention): the inventory-stamp invariant, as a pure guard.
+//
+// ROOT CAUSE (see ../FINDINGS.md): importers stamp a positive "cap-free" stock
+// number (the year literal 2026) on the SELLABLE non-Sample variant of every
+// activated product — both in the product-create payload (`inventory_quantity: 2026`)
+// and on reconcile (`setInventory2026()`). When that sellable variant is ALSO
+// priced $0 (quote-only / contact-for-price lines like Phillipe Romano, Fentucci
+// Naturals), positive stock makes it `availableForSale` → checkout-orderable for $0.
+//
+// THE INVARIANT this module enforces (one place, both call sites):
+// A sellable variant that is priced $0 OR belongs to a quote-only / price-
+// suppressed line must NEVER receive positive inventory. It gets 0 → not orderable.
+// (The $4.25 Sample variant is unaffected — it is not the sellable variant and is
+// already qty=0/non-orderable by design.)
+//
+// PURE + dependency-free on purpose: no network, no env, no Shopify client, so it
+// unit-tests offline and drops into any importer runtime unchanged. $0 (local).
+
+// Tag family that means "this line has no public retail price" — a superset of the
+// single `quote-only` tag the standing canary keyed on (which is why Fentucci, tagged
+// `quotes`/`Needs-Price`, was the canary's 462-product blind spot).
+export const PRICE_SUPPRESSED_TAGS = new Set([
+ 'quote-only', 'quote only', 'quote_only',
+ 'quotes', 'contact-for-price', 'contact for price', 'needs-price', 'needs price',
+]);
+
+const norm = t => String(t).trim().toLowerCase();
+
+/**
+ * Is this product a quote-only / price-suppressed line?
+ * @param {{tags?: string[]|string, vendor?: string}} product
+ */
+export function isPriceSuppressed(product = {}) {
+ const tags = Array.isArray(product.tags)
+ ? product.tags
+ : String(product.tags || '').split(',');
+ if (tags.some(t => PRICE_SUPPRESSED_TAGS.has(norm(t)))) return true;
+ // Vendor fallback for untagged cohorts (Fentucci Naturals ships quote-only with
+ // zero quote-only tags). Extend as new price-on-request lines are onboarded.
+ return norm(product.vendor) === 'fentucci naturals';
+}
+
+/**
+ * A variant is the "sellable" one iff it is NOT the Sample variant.
+ * (Importers create exactly two variants: `Sample` @ $4.25 and the real unit @ price.)
+ * @param {{title?: string, option1?: string}} variant
+ */
+export function isSellableVariant(variant = {}) {
+ const label = variant.title ?? variant.option1 ?? '';
+ return !/sample/i.test(label);
+}
+
+/**
+ * Would giving this sellable variant positive stock make it a $0-orderable defect?
+ * True iff it's the sellable variant AND (price is 0 OR the line is price-suppressed).
+ * @param {object} variant the variant about to be stamped
+ * @param {object} product its parent (for tags/vendor)
+ */
+export function isZeroPriceOrderableRisk(variant = {}, product = {}) {
+ if (!isSellableVariant(variant)) return false;
+ const price = Number(variant.price);
+ return price === 0 || Number.isNaN(price) || isPriceSuppressed(product);
+}
+
+/**
+ * THE GUARD. Return the inventory quantity that is SAFE to stamp on this variant.
+ * Drop-in replacement for the literal `2026` at both call sites:
+ * - create payload: inventory_quantity: safeStampQuantity(variant, product)
+ * - setInventory2026: quantity: safeStampQuantity(variant, product)
+ * Returns `desired` (2026) for normal priced variants; 0 for the defect class.
+ * @returns {number} 0 for a zero-price-orderable risk, else `desired`
+ */
+export function safeStampQuantity(variant, product, desired = 2026) {
+ return isZeroPriceOrderableRisk(variant, product) ? 0 : desired;
+}
← f7d2426 auto-data-snapshot: 2026-09-10T05:55:20 (3 data files) — pil
·
back to Sanderson Onboard
·
auto-data-snapshot: 2026-09-11T01:40:13 (1 data files) — dat 5c8dbdf →