[object Object]

← back to Tk 10965 Zero Price Analysis

TK-10965: zero-price-orderable root-cause analysis + verify harness

8b81bcc8e50275f19525fb9a435e869bed96371c · 2026-08-30 09:38:25 -0700 · steve@designerwallcoverings.com

Root cause: all 1281 flagged PR variants are DENY + qty=2026 + tracked
(year-literal stamped by setInventory2026), NOT inventory_policy=CONTINUE
as previously diagnosed. True blast radius = 1743 (PR 1281 + Fentucci 462);
canary undercounts by 462 (Fentucci not tagged quote-only). READ-ONLY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 8b81bcc8e50275f19525fb9a435e869bed96371c
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Sun Aug 30 09:38:25 2026 -0700

    TK-10965: zero-price-orderable root-cause analysis + verify harness
    
    Root cause: all 1281 flagged PR variants are DENY + qty=2026 + tracked
    (year-literal stamped by setInventory2026), NOT inventory_policy=CONTINUE
    as previously diagnosed. True blast radius = 1743 (PR 1281 + Fentucci 462);
    canary undercounts by 462 (Fentucci not tagged quote-only). READ-ONLY.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .gitignore                   |  7 ++++
 FINDINGS.md                  | 72 +++++++++++++++++++++++++++++++++++++
 README.md                    | 12 +++++++
 REMEDIATION.md               | 40 +++++++++++++++++++++
 evidence/tk10965_probe.mjs   | 22 ++++++++++++
 evidence/tk10965_quant.mjs   | 34 ++++++++++++++++++
 evidence/tk10965_vendors.mjs | 19 ++++++++++
 evidence/verify-latest.json  | 28 +++++++++++++++
 verify.mjs                   | 85 ++++++++++++++++++++++++++++++++++++++++++++
 9 files changed, 319 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..08240ae
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
diff --git a/FINDINGS.md b/FINDINGS.md
new file mode 100644
index 0000000..da9a273
--- /dev/null
+++ b/FINDINGS.md
@@ -0,0 +1,72 @@
+# TK-10965 — Zero-price-orderable canary FAIL: root-cause analysis (READ-ONLY)
+
+**Ticket:** TK-10965 (analysis) · underlying alert **TK-10963** (`zero-price-orderable-canary` verdict FAIL)
+**Analyst:** codex-10965 · **Date:** 2026-08-30 · **Method:** live Shopify Admin GraphQL reads + `dw_unified` mirror reads. **No Shopify or DB writes.**
+**Store:** `designer-laboratory-sandbox.myshopify.com` (the LIVE production DW store).
+
+---
+
+## 1. The defect (what the canary caught)
+
+`zero-price-orderable-canary` (`~/.claude/skills/zero-price-orderable-canary/check.mjs`) flags any **ACTIVE, `quote-only`-tagged** product whose **non-Sample variant** has `price == 0` **and** `availableForSale == true`. Such a variant can be added to cart and **checked out for $0**, bypassing the quote-only ("contact for price") flow.
+
+Latest verdict: **FAIL — 1,281 of 1,455 quote-only-active products (88%)** have a $0 orderable non-sample variant. All flagged products are **Phillipe Romano** (the "Vienna It's Electric" contract-vinyl line and siblings). This is real, not a phantom — confirmed by independent live reads.
+
+## 2. Quantified root cause — 100% uniform
+
+Live inspection of **all 1,281** flagged variants (`evidence/tk10965_quant.mjs`):
+
+| Signal | Value across flagged variants |
+|---|---|
+| `inventoryPolicy` | **DENY** — 1281/1281 (100%) |
+| `inventoryQuantity` | **2026** — 1281/1281 (100%) |
+| `inventoryItem.tracked` | **true** — 1281/1281 (100%) |
+| `price` (non-sample "Per Yard") | **0.00** |
+
+**Mechanism.** Shopify derives `availableForSale ≈ (tracked && quantity > 0) || policy == CONTINUE`. Here the variant is purchasable **purely because `quantity = 2026 > 0`**. `inventoryPolicy = DENY` is inert — DENY only blocks purchase when `quantity ≤ 0`. So the buyable-at-$0 state is driven entirely by the stamped quantity.
+
+**`2026` is not a stock count — it is a hard-coded year-as-sentinel.** Provenance (`grep`, read-only):
+
+- `Designer-Wallcoverings/DW-Programming/command54-shopify-push.js` and `justindavid-shopify-push.js` (both Phillipe-Romano private-label push scripts) contain **`setInventory2026()`**, which sets `on_hand = 2026` at the Ventura Blvd location on the **sellable (non-Sample) SKU**, plus `inventory_quantity: 2026` on product create. Header comment: *"All-channels publish + inventory-2026 on activation (Steve 2026-06-20, memory activation-all-channels-and-2026-inventory) … Cap-free."*
+- The same `setInventory2026` / `quantity: 2026` convention appears in **~20 importer scripts**, including the canonical live importer `Designer-Wallcoverings/shopify/scripts/cadence/cadence-import.js` and the shared `.../templates/new-product-import-template.js`.
+
+**So this is a design collision, not an accident.** An activation policy deliberately stamps a large arbitrary in-stock number (the year `2026`) on the sellable variant — intended to keep products from reading "out of stock" on sales channels (Google/GMC etc.). That policy collides head-on with **quote-only $0 pricing**: a sellable variant that is (a) priced $0 and (b) given positive tracked stock is, by Shopify's rules, **purchasable for $0**. `DENY` cannot save it.
+
+Corroborating detail: on each flagged product the **Sample** variant is correctly `qty=0 → availableForSale=false`, i.e. the product's *own* Sample variant already demonstrates the non-orderable state the sellable variant should have.
+
+## 3. Corrections to the prior analysis (TK-10963 / existing memo)
+
+The earlier `vp-dw-commerce` pass and the drafted memo `pending-approval/zero-price-orderable-1281-quoteonly-REMEDIATION.md` state two things this investigation **disproves with live data**:
+
+1. **"The bug is `inventory_policy=CONTINUE`."** — **False.** 0 of 1,281 are CONTINUE; all 1,281 are **DENY**. The cause is `quantity=2026`, not the policy. Therefore the memo's proposed remediation *"set inventory policy to DENY"* is a **no-op** — it is already DENY.
+2. **"De Gournay proves the fix: identical setup but DENY+0 → not orderable."** — **Misread.** De Gournay's variants are also `DENY, qty=2026, availableForSale=true`. De Gournay is simply **not flagged because it has no $0 non-sample variant** (it is structurally sample-priced), *not* because it is guarded. It is not a valid reference for the fix.
+
+The correct fix is **`quantity → 0`** on the sellable variant (keep DENY + tracked), which is exactly the state PR's own Sample variant already has.
+
+## 4. True blast radius — the canary UNDERCOUNTS by 462
+
+The canary searches only `tag:'quote-only'`. A vendor-scoped scan (`verify.mjs`) finds the **identical `qty=2026` defect on Fentucci Naturals**, which is tagged `quotes` / `Needs-Price` and carries **zero** `quote-only` tags — so the canary never sees it.
+
+| Vendor | Active scanned | $0-orderable | qty==2026 | Seen by canary? |
+|---|---|---|---|---|
+| Phillipe Romano | 12,000 | **1,281** | 1,281 | yes (tagged quote-only) |
+| Fentucci Naturals | 504 | **462** | 462 | **NO — canary blind spot** |
+| De Gournay | 172 | 0 | 0 | n/a (no $0 variant) |
+| Atomic 50 Ceilings | 2 | 0 | 0 | n/a |
+| **TOTAL** | | **1,743** | **1,743 (100%)** | canary sees 1,281 = **73%** |
+
+**26.5% of the true defect population is invisible to the standing canary.** Remediation must be scoped to **1,743**, and the canary itself must be broadened (see REMEDIATION.md §Prevention).
+
+## 5. Impact
+
+- **Revenue leak / free goods:** any customer can check out a contract wallcovering "Per Yard" variant for **$0** on 1,743 live products.
+- **Quote-only bypass:** these lines have **no public retail price by design**; a $0 order also skips the intended "get a quote" commercial flow.
+- **GMC exposure (secondary):** a $0-priced offer with positive stock is also disapproval-bait on Google Shopping — the very channel the `setInventory2026` policy was trying to serve.
+
+## 6. Reproduction / evidence
+
+- `verify.mjs` — re-runnable READ-ONLY acceptance test. `exit 0` = clean, `exit 1` = defect present. Also emits `evidence/verify-latest.json`.
+- `evidence/tk10965_probe.mjs` — single-product variant dump (PR flagged / Fentucci / De Gournay).
+- `evidence/tk10965_quant.mjs` — full flagged-set mechanism tabulation (policy/qty/tracked).
+- `evidence/tk10965_vendors.mjs` — bounded per-vendor blast-radius scan.
+- `evidence/verify-latest.json` — machine-readable snapshot from the latest `verify.mjs` run.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..27563b8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+# TK-10965 — zero-price-orderable analysis + verification harness
+
+READ-ONLY analysis of the `zero-price-orderable-canary` FAIL (underlying alert TK-10963):
+quote-only $0 products checkout-able for $0 on the LIVE DW Shopify store.
+
+- **FINDINGS.md** — quantified root cause (100% `qty=2026` year-literal stamp), true blast
+  radius (1,743 not 1,281), and corrections to the prior analysis.
+- **REMEDIATION.md** — GATED remediation draft (data fix + importer prevention + canary widening).
+- **verify.mjs** — re-runnable READ-ONLY acceptance test (`exit 0`=clean / `exit 1`=defect).
+- **evidence/** — the probe/quant scripts + latest machine-readable snapshot.
+
+No Shopify or DB writes. All remediation is draft-only and Steve-gated.
diff --git a/REMEDIATION.md b/REMEDIATION.md
new file mode 100644
index 0000000..ce0b328
--- /dev/null
+++ b/REMEDIATION.md
@@ -0,0 +1,40 @@
+# TK-10965 — Remediation draft (GATED — Steve APPROVE/REVISE/BLOCK)
+
+> Supersedes the mechanism/fix in `pending-approval/zero-price-orderable-1281-quoteonly-REMEDIATION.md`,
+> which misdiagnosed the cause as `inventory_policy=CONTINUE` (it is DENY; see FINDINGS.md §3).
+> **This is a DRAFT. No Shopify or DB writes have been performed. Nothing auto-executes.**
+
+## Fix A — Data remediation (make the $0 variants non-orderable)
+
+**What:** For the sellable **non-Sample** variant of each affected product, set inventory **`on_hand = 0`** at the Ventura Blvd location (`gid://shopify/Location/5795643504`), keeping `inventoryPolicy = DENY` and `tracked = true`. Result: `availableForSale → false` → not purchasable. Do **not** touch the $4.25 Sample variant.
+
+This is the exact inverse of the importer's `setInventory2026()`; it lands each product in the same non-orderable state its own Sample variant already has.
+
+**Scope:** **1,743 products** (NOT 1,281) — Phillipe Romano 1,281 + **Fentucci Naturals 462**. The Fentucci 462 are the canary's blind spot and MUST be included or the fix is only 73% complete. Exact live id/variant list is enumerable via `verify.mjs` (extend it to emit the variant GIDs for the apply script).
+
+**Mechanism:** one `inventorySetQuantities` mutation per batch (`name:'on_hand', reason:'correction', quantities:[{inventoryItemId, locationId, quantity:0}]`) — the same API `setInventory2026()` uses, value `0` instead of `2026`.
+
+**Reversibility:** trivially reversible. Capture a before-map `{variantId → 2026}` (all are uniformly 2026) and restore via the identical mutation. Recommend batches of 50 with a saved restore-map per batch and a `verify.mjs` re-run after each.
+
+**Acceptance test:** `node verify.mjs` returns **exit 0 / RESULT: CLEAN** (0 $0-orderable across all scoped vendors), and a canary re-run reports **PASS**.
+
+**Gate:** canonical, customer-facing Shopify write across 1,743 products → **HARD GATE** (>500 items + customer-facing). Draft only; needs Steve's explicit APPROVE.
+
+## Fix B — Prevention (stop recurrence at the source) — RECOMMENDED, still gated for deploy
+
+The data fix alone is temporary: the next activation run re-stamps `2026`. Root-cause fix in the importer:
+
+- In **`setInventory2026()`** (and the product-create `inventory_quantity: 2026` literal) in every affected script — canonical `shopify/scripts/cadence/cadence-import.js` + `.../templates/new-product-import-template.js` + the ~18 vendor push scripts (`command54-shopify-push.js`, `justindavid-shopify-push.js`, `hollywood-shopify-push.js`, `as-creation-shopify-push.js`, Fentucci's importer, etc.) — **guard on price**: **skip the positive-stock stamp when the sellable variant's price is `0` (or the product is quote-only / `contact-for-price` / `Needs-Price`).** An unpriced sellable variant must never be given buyable stock.
+- Rationale: a $0 offer should not be advertised as in-stock anyway (GMC disapproves $0 offers), so the "keep it in stock for channels" intent does not apply to quote-only lines.
+
+## Fix C — Close the canary blind spot — SAFE, low-risk (own follow-up ticket)
+
+`~/.claude/skills/zero-price-orderable-canary/check.mjs` searches only `tag:'quote-only'`. Broaden the `SEARCH` to the full quote/price-suppressed tag family (`quote-only`, `quotes`, `contact-for-price`, `Needs-Price`) **or** switch to the vendor-scoped scan in `verify.mjs`, so Fentucci (and future untagged lines) are no longer invisible. This is a read-only skill edit (per-machine, `~/.claude/skills` is gitignored) — not a Shopify write — but is logged here so it isn't lost.
+
+## Recommended sequence
+
+1. **B** (fix the importer so re-stamping stops) →
+2. **A** (zero the 1,743 existing variants, batched + restore-map + `verify.mjs` gate) →
+3. **C** (broaden the canary) → re-run canary → expect PASS.
+
+All three are drafted; **A and B are customer-facing/prod and stay Steve-gated.**
diff --git a/evidence/tk10965_probe.mjs b/evidence/tk10965_probe.mjs
new file mode 100644
index 0000000..7a63f77
--- /dev/null
+++ b/evidence/tk10965_probe.mjs
@@ -0,0 +1,22 @@
+import fs from 'node:fs';
+const env=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8');
+const val=k=>(env.match(new RegExp('^'+k+'=(.*)$','m'))||[])[1]?.trim();
+const DOM=val('SHOPIFY_STORE_DOMAIN'), TOK=val('SHOPIFY_ADMIN_TOKEN');
+const API=`https://${DOM}/admin/api/2024-10/graphql.json`;
+async function gql(q,v){for(let a=0;a<6;a++){const r=await fetch(API,{method:'POST',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});const j=await r.json();if(j.errors){if(JSON.stringify(j.errors).includes('THROTTLED')){await new Promise(s=>setTimeout(s,1500*(a+1)));continue;}throw new Error(JSON.stringify(j.errors));}return j.data;}throw new Error('retries');}
+const Q=`query($id:ID!){product(id:$id){title vendor tags totalInventory tracksInventory variants(first:20){nodes{title price availableForSale inventoryPolicy inventoryQuantity inventoryItem{tracked}}}}}`;
+async function show(label,gid){
+  const d=await gql(Q,{id:gid});
+  const p=d.product; if(!p){console.log(`\n### ${label}: NOT FOUND ${gid}`);return;}
+  console.log(`\n### ${label}: ${p.title} [${p.vendor}]`);
+  console.log(`  tags: ${p.tags.join(', ')}`);
+  for(const v of p.variants.nodes){
+    console.log(`  - "${v.title}" price=${v.price} afs=${v.availableForSale} policy=${v.inventoryPolicy} qty=${v.inventoryQuantity} tracked=${v.inventoryItem?.tracked}`);
+  }
+}
+const g=id=>`gid://shopify/Product/${id}`;
+await show('PR-FLAGGED', g('7896453414963'));      // Vienna It's Electric Twist & Shout
+await show('FENTUCCI-$0ROLL', g('7896848072755'));  // Fasano
+// live-fetch a De Gournay quote-only reference
+const s=await gql(`query{products(first:1,query:"status:active AND vendor:'De Gournay'"){nodes{id title}}}`,{});
+if(s.products.nodes[0]) await show('DEGOURNAY-REF', s.products.nodes[0].id);
diff --git a/evidence/tk10965_quant.mjs b/evidence/tk10965_quant.mjs
new file mode 100644
index 0000000..5569886
--- /dev/null
+++ b/evidence/tk10965_quant.mjs
@@ -0,0 +1,34 @@
+import fs from 'node:fs';
+const env=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8');
+const val=k=>(env.match(new RegExp('^'+k+'=(.*)$','m'))||[])[1]?.trim();
+const DOM=val('SHOPIFY_STORE_DOMAIN'), TOK=val('SHOPIFY_ADMIN_TOKEN');
+const API=`https://${DOM}/admin/api/2024-10/graphql.json`;
+async function gql(q,v){for(let a=0;a<8;a++){const r=await fetch(API,{method:'POST',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});const j=await r.json();if(j.errors){if(JSON.stringify(j.errors).includes('THROTTLED')){await new Promise(s=>setTimeout(s,1800*(a+1)));continue;}throw new Error(JSON.stringify(j.errors));}return j.data;}throw new Error('retries');}
+const SEARCH=`status:active AND (tag:'quote-only' OR tag:'Quote Only' OR tag:'quote_only' OR tag:'Quote-Only')`;
+let after=null,pages=0,all=[];
+do{ const d=await gql(`query($q:String!,$after:String){products(first:100,query:$q,after:$after){pageInfo{hasNextPage endCursor} nodes{id vendor variants(first:20){nodes{title price availableForSale inventoryPolicy inventoryQuantity inventoryItem{tracked}}}}}}`,{q:SEARCH,after});
+  all.push(...d.products.nodes); after=d.products.pageInfo.hasNextPage?d.products.pageInfo.endCursor:null; pages++;
+}while(after&&pages<40);
+// bad = non-sample $0 variant that is availableForSale
+const badVars=[];
+for(const p of all){
+  for(const v of p.variants.nodes){
+    if(!/sample/i.test(v.title||'') && Number(v.price)===0 && v.availableForSale===true){
+      badVars.push({vendor:p.vendor, policy:v.inventoryPolicy, qty:v.inventoryQuantity, tracked:v.inventoryItem?.tracked});
+    }
+  }
+}
+const byVendor={}, byPolicy={}, byQty={}, byTracked={};
+for(const b of badVars){
+  byVendor[b.vendor]=(byVendor[b.vendor]||0)+1;
+  byPolicy[b.policy]=(byPolicy[b.policy]||0)+1;
+  const qk = b.qty>0? (b.qty===2026?'2026':'other_positive') : (b.qty===0?'0':'neg/null');
+  byQty[qk]=(byQty[qk]||0)+1;
+  byTracked[String(b.tracked)]=(byTracked[String(b.tracked)]||0)+1;
+}
+console.log('quote_only_active_products:', all.length);
+console.log('bad_zero_price_orderable_variants:', badVars.length);
+console.log('by_vendor:', JSON.stringify(byVendor));
+console.log('by_inventoryPolicy:', JSON.stringify(byPolicy));
+console.log('by_inventoryQty:', JSON.stringify(byQty));
+console.log('by_tracked:', JSON.stringify(byTracked));
diff --git a/evidence/tk10965_vendors.mjs b/evidence/tk10965_vendors.mjs
new file mode 100644
index 0000000..25857a1
--- /dev/null
+++ b/evidence/tk10965_vendors.mjs
@@ -0,0 +1,19 @@
+import fs from 'node:fs';
+const env=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8');
+const val=k=>(env.match(new RegExp('^'+k+'=(.*)$','m'))||[])[1]?.trim();
+const DOM=val('SHOPIFY_STORE_DOMAIN'), TOK=val('SHOPIFY_ADMIN_TOKEN');
+const API=`https://${DOM}/admin/api/2024-10/graphql.json`;
+async function gql(q,v){for(let a=0;a<8;a++){const r=await fetch(API,{method:'POST',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});const j=await r.json();if(j.errors){if(JSON.stringify(j.errors).includes('THROTTLED')){await new Promise(s=>setTimeout(s,1800*(a+1)));continue;}throw new Error(JSON.stringify(j.errors));}return j.data;}throw new Error('retries');}
+async function scanVendor(vendor){
+  let after=null,pages=0,n=0,bad=0,q2026=0,hasQO=0;
+  do{ const d=await gql(`query($q:String!,$after:String){products(first:100,query:$q,after:$after){pageInfo{hasNextPage endCursor} nodes{id tags variants(first:20){nodes{title price availableForSale inventoryQuantity}}}}}`,{q:`status:active AND vendor:'${vendor}'`,after});
+    for(const p of d.products.nodes){ n++;
+      const qo=p.tags.some(t=>/^quote[-_ ]?only$/i.test(t));
+      for(const v of p.variants.nodes){ if(!/sample/i.test(v.title||'')&&Number(v.price)===0&&v.availableForSale===true){bad++; if(v.inventoryQuantity===2026)q2026++; if(qo)hasQO++; break;} }
+    }
+    after=d.products.pageInfo.hasNextPage?d.products.pageInfo.endCursor:null; pages++;
+  }while(after&&pages<80);
+  console.log(`${vendor}: active=${n} $0-orderable=${bad} qty2026=${q2026} taggedQuoteOnly=${hasQO}`);
+  return {vendor,active:n,bad,q2026,hasQO};
+}
+for(const v of ['Fentucci Naturals','Phillipe Romano','De Gournay','Justin David','Command54']) await scanVendor(v);
diff --git a/evidence/verify-latest.json b/evidence/verify-latest.json
new file mode 100644
index 0000000..108c62b
--- /dev/null
+++ b/evidence/verify-latest.json
@@ -0,0 +1,28 @@
+{
+  "ts": "2026-08-30T16:37:03.265Z",
+  "total": 1743,
+  "qty2026": 1743,
+  "canary_blind": 462,
+  "per_vendor": [
+    {
+      "vendor": "Phillipe Romano",
+      "active": 12000,
+      "bad": 1281
+    },
+    {
+      "vendor": "Fentucci Naturals",
+      "active": 504,
+      "bad": 462
+    },
+    {
+      "vendor": "De Gournay",
+      "active": 172,
+      "bad": 0
+    },
+    {
+      "vendor": "Atomic 50 Ceilings",
+      "active": 2,
+      "bad": 0
+    }
+  ]
+}
\ No newline at end of file
diff --git a/verify.mjs b/verify.mjs
new file mode 100644
index 0000000..a4fed58
--- /dev/null
+++ b/verify.mjs
@@ -0,0 +1,85 @@
+#!/usr/bin/env node
+// TK-10965 — zero-price-orderable verification / test harness (READ-ONLY).
+//
+// Purpose: reproduce and quantify the "quote-only $0 product is checkout-orderable"
+// defect, AND serve as the post-remediation acceptance test. Re-run after any fix:
+//   exit 0  => CLEAN  (no vendor in scope has a $0 orderable non-sample variant)
+//   exit 1  => DIRTY  (defect still present)  <-- fails a test/CI gate
+//
+// It is a SUPERSET of the shipped canary (skills/zero-price-orderable-canary):
+// the canary searches only `tag:'quote-only'` (catches Phillipe Romano) and MISSES
+// the identical bug on Fentucci Naturals (tagged `quotes`/`Needs-Price`, 0 quote-only).
+// This harness scans by VENDOR so it sees the true blast radius.
+//
+// READ-ONLY: issues only Shopify Admin GraphQL *queries*. No mutations, no DB writes.
+// $0 cost (live Shopify reads are unmetered).
+//
+// Usage:  node verify.mjs            # scan the known-affected vendor set
+//         node verify.mjs --all      # also scan every quote/price-suppressed vendor (slower)
+import fs from 'node:fs';
+
+const ENV = '/Users/macstudio3/Projects/secrets-manager/.env';
+const env = fs.readFileSync(ENV, 'utf8');
+const val = k => (env.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.trim();
+const DOM = val('SHOPIFY_STORE_DOMAIN'), TOK = val('SHOPIFY_ADMIN_TOKEN');
+const API = `https://${DOM}/admin/api/2024-10/graphql.json`;
+
+async function gql(q, v) {
+  for (let a = 0; a < 8; a++) {
+    const r = await fetch(API, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOK, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) });
+    const j = await r.json();
+    if (j.errors) {
+      if (JSON.stringify(j.errors).includes('THROTTLED')) { await new Promise(s => setTimeout(s, 1800 * (a + 1))); continue; }
+      throw new Error(JSON.stringify(j.errors));
+    }
+    return j.data;
+  }
+  throw new Error('gql retries exhausted');
+}
+
+// A variant is the defect iff: non-Sample AND price==0 AND availableForSale==true.
+// (availableForSale is Shopify's authoritative "purchasable" signal — same test the canary uses.)
+const isBadVariant = v => !/sample/i.test(v.title || '') && Number(v.price) === 0 && v.availableForSale === true;
+
+async function scanVendor(vendor) {
+  let after = null, pages = 0, active = 0;
+  const bad = [];
+  do {
+    const d = await gql(
+      `query($q:String!,$after:String){products(first:100,query:$q,after:$after){pageInfo{hasNextPage endCursor} nodes{id title tags variants(first:20){nodes{title price availableForSale inventoryPolicy inventoryQuantity inventoryItem{tracked}}}}}}`,
+      { q: `status:active AND vendor:'${vendor}'`, after });
+    for (const p of d.products.nodes) {
+      active++;
+      const v = p.variants.nodes.find(isBadVariant);
+      if (v) bad.push({ id: p.id.split('/').pop(), title: p.title, policy: v.inventoryPolicy, qty: v.inventoryQuantity, tracked: v.inventoryItem?.tracked, quoteOnly: p.tags.some(t => /^quote[-_ ]?only$/i.test(t)) });
+    }
+    after = d.products.pageInfo.hasNextPage ? d.products.pageInfo.endCursor : null;
+    pages++;
+  } while (after && pages < 120);
+  return { vendor, active, bad };
+}
+
+// Known-affected + guard-reference vendors. Extend as new lines are onboarded with setInventory2026().
+const VENDORS = ['Phillipe Romano', 'Fentucci Naturals', 'De Gournay', 'Atomic 50 Ceilings'];
+
+const results = [];
+for (const v of VENDORS) results.push(await scanVendor(v));
+
+let total = 0, q2026 = 0, missedByCanary = 0;
+console.log('# TK-10965 zero-price-orderable verification —', new Date().toISOString());
+for (const r of results) {
+  total += r.bad.length;
+  q2026 += r.bad.filter(b => b.qty === 2026).length;
+  missedByCanary += r.bad.filter(b => !b.quoteOnly).length;
+  console.log(`  ${r.vendor}: active=${r.active}  $0-orderable=${r.bad.length}  (qty2026=${r.bad.filter(b => b.qty === 2026).length}, canary-blind=${r.bad.filter(b => !b.quoteOnly).length})`);
+}
+console.log(`\nTOTAL $0-orderable non-sample variants: ${total}`);
+console.log(`  of which qty==2026 (year-literal stamp): ${q2026}`);
+console.log(`  of which UNTAGGED quote-only (canary blind spot): ${missedByCanary}`);
+console.log(total === 0 ? '\nRESULT: CLEAN ✅' : '\nRESULT: DIRTY ❌ (defect present)');
+
+// Emit a machine-readable snapshot next to the harness for the ticket record.
+fs.writeFileSync(new URL('./evidence/verify-latest.json', import.meta.url),
+  JSON.stringify({ ts: new Date().toISOString(), total, qty2026: q2026, canary_blind: missedByCanary, per_vendor: results.map(r => ({ vendor: r.vendor, active: r.active, bad: r.bad.length })) }, null, 2));
+
+process.exit(total === 0 ? 0 : 1);

(oldest)  ·  back to Tk 10965 Zero Price Analysis  ·  Add multi-location caveat (Kimi second-model confirm) to rem 31761c3 →