[object Object]

← back to Designerwallcoverings

verify TK-10965 zero-price root cause

a565806bcf1646be9f37c11916e2dfbc0ccfafae · 2026-08-30 09:45:52 -0700 · Steve Abrams

Files touched

Diff

commit a565806bcf1646be9f37c11916e2dfbc0ccfafae
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Aug 30 09:45:52 2026 -0700

    verify TK-10965 zero-price root cause
---
 .../compare-zero-price.mjs                         | 89 ++++++++++++++++++++++
 .../remediation-plan.mjs                           | 71 +++++++++++++++++
 .../remediation-plan.test.mjs                      | 63 +++++++++++++++
 .../zero-dollar-orderable-canary.mjs               |  7 +-
 verification/TK-10965-codex-comparison.md          | 43 +++++++++++
 verification/TK-10965-e2e-proof.json               | 21 +++++
 6 files changed, 291 insertions(+), 3 deletions(-)

diff --git a/scripts/zero-dollar-orderable-canary/compare-zero-price.mjs b/scripts/zero-dollar-orderable-canary/compare-zero-price.mjs
new file mode 100644
index 0000000..2ebbaf1
--- /dev/null
+++ b/scripts/zero-dollar-orderable-canary/compare-zero-price.mjs
@@ -0,0 +1,89 @@
+// TK-10965 — canonical READ-ONLY Shopify comparison. Never issues a mutation.
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const secretPath = process.env.SECRETS_ENV || path.join(os.homedir(), 'Projects/secrets-manager/.env');
+const env = fs.readFileSync(secretPath, 'utf8');
+const token = ((env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1] || '').replace(/['"\r]/g, '').trim();
+if (!token) throw new Error('SHOPIFY_ADMIN_TOKEN missing');
+const endpoint = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
+const queries = [
+  ['Phillipe Romano', 'status:active vendor:"Phillipe Romano"'],
+  ['De Gournay', 'status:active vendor:"De Gournay"'],
+  ['Atomic 50 Ceilings', 'status:active vendor:"Atomic 50 Ceilings"'],
+];
+const query = `query($cursor:String,$q:String!){products(first:100,after:$cursor,query:$q){pageInfo{hasNextPage endCursor}nodes{id handle title vendor status tags variants(first:100){pageInfo{hasNextPage}nodes{id title price position inventoryPolicy availableForSale inventoryQuantity inventoryItem{id tracked}}}}}}`;
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+
+async function gql(variables) {
+  for (let attempt = 1; attempt <= 6; attempt++) {
+    const response = await fetch(endpoint, { method: 'POST', headers: {
+      'X-Shopify-Access-Token': token, 'Content-Type': 'application/json',
+    }, body: JSON.stringify({ query, variables }) });
+    const body = await response.json();
+    if (response.ok && !body.errors) return body;
+    if (response.status !== 429 && !JSON.stringify(body.errors || '').includes('Throttled')) throw new Error(JSON.stringify(body.errors || body));
+    await sleep(attempt * 1500);
+  }
+  throw new Error('Shopify throttled after retries');
+}
+
+const isZeroOrderable = v => Number(v.price) === 0 && (v.inventoryPolicy === 'CONTINUE' || Number(v.inventoryQuantity) > 0 || v.availableForSale === true);
+const quoteTag = /quote.only|contact.for.price|needs.price|showroom.line/i;
+
+async function scan(label, search) {
+  let cursor = null; const products = []; let pages = 0; let truncated = 0;
+  do {
+    const body = await gql({ cursor, q: search });
+    const connection = body.data.products;
+    for (const product of connection.nodes) {
+      if (product.variants.pageInfo.hasNextPage) truncated++;
+      products.push(product);
+    }
+    cursor = connection.pageInfo.hasNextPage ? connection.pageInfo.endCursor : null;
+    pages++;
+    await sleep(220);
+  } while (cursor);
+  const hits = products.flatMap(product => product.variants.nodes.filter(isZeroOrderable).map(variant => ({ product, variant })));
+  const zeroVariants = products.flatMap(product => product.variants.nodes.filter(variant => Number(variant.price) === 0).map(variant => ({ product, variant })));
+  const count = predicate => hits.filter(predicate).length;
+  return {
+    requestedVendor: label, pages, products: products.length, truncatedProducts: truncated,
+    actualVendors: Object.entries(products.reduce((a, p) => (a[p.vendor] = (a[p.vendor] || 0) + 1, a), {})).sort((a,b) => b[1]-a[1]),
+    orderableZeroVariants: hits.length,
+    zeroVariants: zeroVariants.length,
+    safeZeroVariants: zeroVariants.length - hits.length,
+    distinctAffectedProducts: new Set(hits.map(x => x.product.id)).size,
+    nonSampleHits: count(x => !/sample|memo/i.test(x.variant.title || '')),
+    sampleOrMemoHits: count(x => /sample|memo/i.test(x.variant.title || '')),
+    quoteTaggedHits: count(x => quoteTag.test((x.product.tags || []).join(','))),
+    causes: {
+      continuePolicy: count(x => x.variant.inventoryPolicy === 'CONTINUE'),
+      positiveInventory: count(x => Number(x.variant.inventoryQuantity) > 0),
+      availableForSale: count(x => x.variant.availableForSale === true),
+      trackingDisabled: count(x => x.variant.inventoryItem?.tracked === false),
+      missingInventoryItem: count(x => !x.variant.inventoryItem?.id),
+    },
+    inventoryQuantityHistogram: Object.entries(hits.reduce((a, x) => {
+      const key = String(x.variant.inventoryQuantity); a[key] = (a[key] || 0) + 1; return a;
+    }, {})).sort((a,b) => Number(a[0]) - Number(b[0])),
+    zeroVariantControls: zeroVariants.filter(x => !isZeroOrderable(x.variant)).slice(0, 3).map(x => ({
+      handle: x.product.handle, variantTitle: x.variant.title, inventoryPolicy: x.variant.inventoryPolicy,
+      inventoryQuantity: x.variant.inventoryQuantity, availableForSale: x.variant.availableForSale,
+      inventoryTracked: x.variant.inventoryItem?.tracked,
+    })),
+    samples: hits.slice(0, 5).map(x => ({ handle: x.product.handle, productId: x.product.id, variantId: x.variant.id, variantTitle: x.variant.title, tags: x.product.tags })),
+  };
+}
+
+const comparisons = [];
+for (const [label, search] of queries) comparisons.push(await scan(label, search));
+const result = {
+  ticket: 'TK-10965', model: 'codex', generatedAt: new Date().toISOString(),
+  source: 'Shopify Admin GraphQL 2024-10 (read-only)', endpoint,
+  comparisons,
+};
+const out = process.argv[2];
+if (out) fs.writeFileSync(out, `${JSON.stringify(result, null, 2)}\n`);
+console.log(JSON.stringify(result, null, 2));
diff --git a/scripts/zero-dollar-orderable-canary/remediation-plan.mjs b/scripts/zero-dollar-orderable-canary/remediation-plan.mjs
new file mode 100644
index 0000000..085f80c
--- /dev/null
+++ b/scripts/zero-dollar-orderable-canary/remediation-plan.mjs
@@ -0,0 +1,71 @@
+const SAMPLE = /sample|memo/i;
+
+export function isOrderableZero(variant) {
+  return Number(variant.price) === 0 && (
+    variant.inventoryPolicy === 'CONTINUE' ||
+    Number(variant.inventoryQuantity) > 0 ||
+    variant.availableForSale === true
+  );
+}
+
+export function buildRemediationPlan(products) {
+  const changes = [];
+  const rejected = [];
+
+  for (const product of products) {
+    for (const variant of product.variants || []) {
+      const ref = {
+        productId: product.id,
+        handle: product.handle,
+        variantId: variant.id,
+        inventoryItemId: variant.inventoryItemId,
+        title: variant.title,
+      };
+      if (!isOrderableZero(variant)) continue;
+      if (SAMPLE.test(variant.title || '')) {
+        rejected.push({ ...ref, reason: 'sample-or-memo' });
+        continue;
+      }
+      if (!variant.inventoryItemId) {
+        rejected.push({ ...ref, reason: 'missing-inventory-item-id' });
+        continue;
+      }
+      if (!(variant.quantities || []).length) {
+        rejected.push({ ...ref, reason: 'missing-inventory-levels' });
+        continue;
+      }
+      changes.push({
+        ...ref,
+        before: {
+          price: String(variant.price),
+          inventoryPolicy: variant.inventoryPolicy,
+          inventoryTracked: Boolean(variant.inventoryTracked),
+          quantities: [...(variant.quantities || [])],
+        },
+        after: {
+          price: String(variant.price),
+          inventoryPolicy: 'DENY',
+          inventoryTracked: true,
+          quantities: (variant.quantities || []).map(q => ({ ...q, compareQuantity: q.quantity, quantity: 0 })),
+        },
+      });
+    }
+  }
+  return { changes, rejected };
+}
+
+export function validatePlan(plan) {
+  const errors = [];
+  const ids = new Set();
+  for (const change of plan.changes || []) {
+    if (!change.variantId || ids.has(change.variantId)) errors.push(`duplicate-or-missing variantId: ${change.variantId || '(missing)'}`);
+    ids.add(change.variantId);
+    if (SAMPLE.test(change.title || '')) errors.push(`sample selected: ${change.variantId}`);
+    if (Number(change.before?.price) !== 0 || Number(change.after?.price) !== 0) errors.push(`price changed/nonzero: ${change.variantId}`);
+    if (change.after?.inventoryPolicy !== 'DENY') errors.push(`policy is not DENY: ${change.variantId}`);
+    if (change.after?.inventoryTracked !== true) errors.push(`tracking is not enabled: ${change.variantId}`);
+    if ((change.after?.quantities || []).some(q => Number(q.quantity) !== 0)) errors.push(`nonzero location remains: ${change.variantId}`);
+    if (!change.before?.quantities) errors.push(`rollback quantities missing: ${change.variantId}`);
+  }
+  return { ok: errors.length === 0, errors };
+}
diff --git a/scripts/zero-dollar-orderable-canary/remediation-plan.test.mjs b/scripts/zero-dollar-orderable-canary/remediation-plan.test.mjs
new file mode 100644
index 0000000..d4aadd8
--- /dev/null
+++ b/scripts/zero-dollar-orderable-canary/remediation-plan.test.mjs
@@ -0,0 +1,63 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { buildRemediationPlan, validatePlan } from './remediation-plan.mjs';
+
+const product = overrides => ({
+  id: 'gid://shopify/Product/1', handle: 'quote-only-wallcovering',
+  variants: [], ...overrides,
+});
+const variant = overrides => ({
+  id: 'gid://shopify/ProductVariant/10', inventoryItemId: 'gid://shopify/InventoryItem/20',
+  title: 'Roll', price: '0.00', inventoryPolicy: 'CONTINUE', inventoryTracked: false,
+  inventoryQuantity: 0, availableForSale: true,
+  quantities: [{ locationId: 'gid://shopify/Location/1', quantity: 4 }, { locationId: 'gid://shopify/Location/2', quantity: 0 }],
+  ...overrides,
+});
+
+test('plans DENY + tracked + zero at every location and retains a rollback before-map', () => {
+  const plan = buildRemediationPlan([product({ variants: [variant()] })]);
+  assert.equal(plan.changes.length, 1);
+  assert.deepEqual(plan.changes[0].after.quantities.map(q => q.quantity), [0, 0]);
+  assert.deepEqual(plan.changes[0].before.quantities.map(q => q.quantity), [4, 0]);
+  assert.deepEqual(validatePlan(plan), { ok: true, errors: [] });
+});
+
+test('does not touch samples/memos, priced variants, or already non-orderable zero variants', () => {
+  const plan = buildRemediationPlan([product({ variants: [
+    variant({ id: 'sample', title: 'Sample' }),
+    variant({ id: 'memo', title: 'Memo' }),
+    variant({ id: 'priced', price: '4.25' }),
+    variant({ id: 'safe', inventoryPolicy: 'DENY', inventoryQuantity: 0, availableForSale: false }),
+  ] })]);
+  assert.equal(plan.changes.length, 0);
+  assert.deepEqual(plan.rejected.map(x => x.reason), ['sample-or-memo', 'sample-or-memo']);
+});
+
+test('captures stock and storefront availability as independent orderability causes', () => {
+  const plan = buildRemediationPlan([product({ variants: [
+    variant({ id: 'stock', inventoryPolicy: 'DENY', inventoryQuantity: 2, availableForSale: false }),
+    variant({ id: 'available', inventoryPolicy: 'DENY', inventoryQuantity: 0, availableForSale: true }),
+  ] })]);
+  assert.deepEqual(plan.changes.map(x => x.variantId), ['stock', 'available']);
+});
+
+test('quarantines a candidate without inventoryItemId instead of producing an unsafe partial plan', () => {
+  const plan = buildRemediationPlan([product({ variants: [variant({ inventoryItemId: null })] })]);
+  assert.equal(plan.changes.length, 0);
+  assert.equal(plan.rejected[0].reason, 'missing-inventory-item-id');
+});
+
+test('quarantines a candidate without a complete inventory-level snapshot', () => {
+  const plan = buildRemediationPlan([product({ variants: [variant({ quantities: [] })] })]);
+  assert.equal(plan.changes.length, 0);
+  assert.equal(plan.rejected[0].reason, 'missing-inventory-levels');
+});
+
+test('validator rejects duplicate IDs, sample selection, price mutation, and nonzero locations', () => {
+  const bad = buildRemediationPlan([product({ variants: [variant()] })]);
+  const duplicate = structuredClone(bad.changes[0]);
+  duplicate.title = 'Sample'; duplicate.after.price = '1.00'; duplicate.after.quantities[0].quantity = 1;
+  const result = validatePlan({ changes: [...bad.changes, duplicate] });
+  assert.equal(result.ok, false);
+  assert.match(result.errors.join('\n'), /duplicate|sample selected|price changed|nonzero location/);
+});
diff --git a/scripts/zero-dollar-orderable-canary/zero-dollar-orderable-canary.mjs b/scripts/zero-dollar-orderable-canary/zero-dollar-orderable-canary.mjs
index f0daa70..7313e97 100644
--- a/scripts/zero-dollar-orderable-canary/zero-dollar-orderable-canary.mjs
+++ b/scripts/zero-dollar-orderable-canary/zero-dollar-orderable-canary.mjs
@@ -17,9 +17,8 @@
 //    confirm needed (a $0 DB state isn't a transient network blip; abort-on-partial covers
 //    the under-count risk). exit 3 on live ACTIVE exposure, 0 if clean, 2 if inconclusive.
 // EXCLUDES Phillip Jeffries. Read-only Admin GraphQL; never a mutation.
-import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url';
-const ENV=fs.readFileSync('/Users/stevestudio2/Projects/secrets-manager/.env','utf8');
-const TOK=((ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]||'').replace(/['"\r]/g,'').trim(); // c78-officer: guard missing token
+import fs from 'fs'; import path from 'path'; import os from 'os'; import { fileURLToPath } from 'url';
+const SECRET_ENV=process.env.SECRETS_ENV || path.join(os.homedir(),'Projects/secrets-manager/.env');
 const GQL='https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
 const PJ=/phillip[- ]?jeffries/i;
 const MIN_ACTIVE_SCAN=Number(process.env.MIN_ACTIVE_SCAN||50000); // completeness floor (real active ~71.7k); survives the RL reprice (decoupled from $0 content)
@@ -75,6 +74,8 @@ if(SELFTEST){
   console.log(`SELFTEST: ${pass&&escPass?'PASS':'FAIL'}`);
   process.exit(pass&&escPass?0:1);
 }
+let ENV=''; try { ENV=fs.readFileSync(SECRET_ENV,'utf8'); } catch {}
+const TOK=((ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]||'').replace(/['"\r]/g,'').trim(); // c78-officer: guard missing token
 if(!TOK){ console.log('🔴 INCONCLUSIVE: SHOPIFY_ADMIN_TOKEN missing — cannot scan'); fs.mkdirSync(DATA_DIR,{recursive:true}); fs.writeFileSync(path.join(DATA_DIR,'latest.json'),JSON.stringify({ts:new Date().toISOString(),verdict:'INCONCLUSIVE',reason:'no token'},null,2)); process.exitCode=2; process.exit(2); }
 const act=await scanStatus('status:active');
 const dft=await scanStatus('status:draft');
diff --git a/verification/TK-10965-codex-comparison.md b/verification/TK-10965-codex-comparison.md
new file mode 100644
index 0000000..de3a83c
--- /dev/null
+++ b/verification/TK-10965-codex-comparison.md
@@ -0,0 +1,43 @@
+# TK-10965 Codex comparison — zero-price orderability
+
+Generated 2026-08-30. `model=codex`. No Shopify or `dw_unified` writes were made.
+
+## Verdict
+
+Claude's exposure count is reproduced exactly, but its stated root cause is not.
+
+- Canonical Shopify Admin GraphQL returned 13,644 active Phillipe Romano products and 1,281 distinct affected products/variants.
+- All 1,281 are non-sample `$0` variants and all 1,281 carry a quote/contact-price marker.
+- **0/1,281 use `inventoryPolicy=CONTINUE`.** All 1,281 already use the guarded policy.
+- **1,281/1,281 have positive tracked inventory, all exactly `2026`, and 1,281/1,281 report `availableForSale=true`.** That synthetic quantity is the reproduced checkout-enabling cause.
+- No affected item has tracking disabled or lacks an inventory item.
+- The asserted De Gournay and Atomic 50 `DENY+0` controls were not reproducible as `$0` controls in current canonical data: 172 active De Gournay and 2 active Atomic 50 Ceilings products were returned, but neither set currently contains a `$0` variant.
+- A separate complete all-vendor canary scanned 87,376 active and 17,988 draft products with zero truncations. It found 1,743 active orderable `$0` products: the 1,281 Phillipe Romano cohort plus 462 Fentucci Naturals. It also found 446 draft exposures. The extra cohorts are outside Claude's stated remediation population and must not be silently added to the same gated batch.
+
+Evidence: `verification/TK-10965-canonical-comparison.json`.
+
+## Comparison with Claude
+
+| Claim | Codex result |
+|---|---|
+| 1,281 active quote-only products exposed | Confirmed exactly; 1,281 distinct Phillipe Romano products and variants |
+| Samples are not the exposed variants | Confirmed; 1,281 non-sample, 0 sample/memo |
+| Root cause is `CONTINUE` | **Rejected; 0 use `CONTINUE`** |
+| Root cause is inventory state | Corrected: all 1,281 carry quantity `2026`, are tracked, and are available for sale |
+| De Gournay proves current `DENY+0` control | Not reproducible from current live data because its active variants are not `$0` |
+| Proposed `DENY + tracked + quantity 0` end state | Safe, but quantity zeroing is the effective fix; policy/tracking are assertions, not the primary delta |
+| 1,281 represents the whole live canary failure | **Rejected; current complete scan finds 1,743 active total, including a separate 462 Fentucci Naturals cohort** |
+
+## Remediation draft (still gated; not executed)
+
+1. Re-read every candidate from Admin GraphQL immediately before planning. Select only active Phillipe Romano, non-sample variants with price exactly zero and actual orderability.
+2. Capture a rollback before-map containing product/variant/inventory-item IDs, policy, tracking, and every location's current available quantity.
+3. Quarantine any row with a missing inventory item or incomplete inventory-level snapshot. Never partially plan it.
+4. Keep price at zero, samples untouched, `inventoryPolicy=DENY`, and tracking enabled.
+5. For every inventory level, set available quantity to zero using Shopify's compare-and-set quantity field populated from the before-map. Use an idempotency key and fail the batch on any user error or stale comparison; do not bypass compare-and-set.
+6. Canary a small Steve-approved batch first. Verify Admin GraphQL state plus storefront cart rejection, then continue in bounded batches only after the canary passes.
+7. Re-run the zero-dollar orderable canary. Required final state: zero affected active variants, sample availability unchanged, and exact rollback-map coverage for every successful mutation.
+
+The pure plan/validator in `scripts/zero-dollar-orderable-canary/remediation-plan.mjs` enforces the selection and rollback invariants. Its tests cover all-location zeroing, compare-and-set capture, sample/price preservation, independent orderability causes, missing-data quarantine, and malformed-plan rejection.
+
+Shopify's current documentation supports absolute inventory setting with compare-and-set protection and warns against bypassing the comparison check: https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventorysetquantities. Variant policy updates, if an assertion repair is ever needed, belong in `productVariantsBulkUpdate`: https://shopify.dev/docs/api/admin-graphql/latest/mutations/productVariantsBulkUpdate.
diff --git a/verification/TK-10965-e2e-proof.json b/verification/TK-10965-e2e-proof.json
new file mode 100644
index 0000000..0724089
--- /dev/null
+++ b/verification/TK-10965-e2e-proof.json
@@ -0,0 +1,21 @@
+{
+  "ticket": "TK-10965",
+  "model": "codex",
+  "intent": "Independently reproduce zero-price exposure and validate a no-write remediation draft",
+  "riskTier": "R4 production-data read; R0/R1 local artifact and pure planner changes; no production mutation",
+  "environment": "Mac2 local repo plus read-only Shopify Admin GraphQL 2024-10",
+  "timestamp": "2026-08-30T16:43:17.534Z",
+  "baseline": "Claude reported 1,281 affected products and attributed orderability to CONTINUE policy",
+  "assertions": [
+    {"boundary":"canonical Shopify read","verdict":"PASS","evidence":"verification/TK-10965-canonical-comparison.json; 1,281 distinct affected products/variants"},
+    {"boundary":"full-scan coverage","verdict":"PASS","evidence":"87,376 active + 17,988 draft scanned, complete=true, truncated=0; 1,743 active and 446 draft orderable-zero hits"},
+    {"boundary":"root-cause quantification","verdict":"PASS","evidence":"CONTINUE=0; positive inventory=1,281; quantity 2026=1,281; availableForSale=1,281"},
+    {"boundary":"sample preservation","verdict":"PASS","evidence":"canonical affected sample/memo count=0; unit test excludes Sample and Memo"},
+    {"boundary":"rollback and all-location plan","verdict":"PASS","evidence":"unit tests retain before quantities and plan zero with compareQuantity per location"},
+    {"boundary":"negative/incomplete input","verdict":"PASS","evidence":"unit tests quarantine missing inventory item/levels and reject malformed plans"},
+    {"boundary":"Shopify mutation","verdict":"SKIP","reason":"Explicitly prohibited and hard-gated; no Shopify write authorized"},
+    {"boundary":"dw_unified mutation","verdict":"SKIP","reason":"Explicitly prohibited; no unified DB write performed"}
+  ],
+  "cleanup":"No external state created. Read-only API requests only; local evidence intentionally retained.",
+  "verdict":"PASS for requested analysis/draft validation; production remediation remains gated"
+}

← c8b7084 auto-data-snapshot: 2026-08-30T09:43:11 (1 data files) — ver  ·  back to Designerwallcoverings  ·  record TK-10965 full canary evidence 13c9e25 →