[object Object]

← back to Tk 10965 Zero Price Analysis

add hardened zero-price orderable canary guard

3b4e9b819689e77569e114e896741832eca75be3 · 2026-08-30 11:27:26 -0700 · Steve Abrams

Files touched

Diff

commit 3b4e9b819689e77569e114e896741832eca75be3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Aug 30 11:27:26 2026 -0700

    add hardened zero-price orderable canary guard
---
 tk10964-canary-guard/README.md                   | 10 +++
 tk10964-canary-guard/check.mjs                   | 97 ++++++++++++++++++++++++
 tk10964-canary-guard/fixtures/mixed.json         |  8 ++
 tk10964-canary-guard/test.mjs                    | 17 +++++
 tk10964-canary-guard/verification/e2e-proof.json | 48 ++++++++++++
 5 files changed, 180 insertions(+)

diff --git a/tk10964-canary-guard/README.md b/tk10964-canary-guard/README.md
new file mode 100644
index 0000000..3bb528f
--- /dev/null
+++ b/tk10964-canary-guard/README.md
@@ -0,0 +1,10 @@
+# TK-10964 canary guard
+
+Read-only replacement for the zero-price-orderable canary. It closes the
+Fentucci blind spot by scanning the complete quote/price-suppressed tag family
+plus a known-vendor fallback, de-duplicates overlapping results by product GID,
+and reports the `qty=2026 + DENY + tracked` mechanism without treating DENY as
+the cause.
+
+Use `--fixture fixtures/mixed.json` for a network-free end-to-end output test.
+Live mode performs GraphQL queries only and writes one local JSON status file.
diff --git a/tk10964-canary-guard/check.mjs b/tk10964-canary-guard/check.mjs
new file mode 100644
index 0000000..a1b9e39
--- /dev/null
+++ b/tk10964-canary-guard/check.mjs
@@ -0,0 +1,97 @@
+#!/usr/bin/env node
+// TK-10964 — read-only zero-price-orderable guard.
+// Queries cover the full quote-tag family and a vendor fallback for the known
+// untagged Fentucci cohort. Results are de-duplicated by Shopify product GID.
+import fs from 'node:fs';
+import path from 'node:path';
+
+const HERE = path.dirname(new URL(import.meta.url).pathname);
+const OUT = process.env.ZERO_PRICE_CANARY_OUT || path.join(HERE, 'data', 'latest.json');
+export const SEARCHES = [
+  `status:active AND (tag:'quote-only' OR tag:'Quote Only' OR tag:'quote_only' OR tag:'Quote-Only')`,
+  `status:active AND (tag:'quotes' OR tag:'contact-for-price' OR tag:'Needs-Price')`,
+  `status:active AND vendor:'Fentucci Naturals'`
+];
+
+const QUOTE_TAGS = new Set(['quote-only', 'quote only', 'quote_only', 'quotes', 'contact-for-price', 'needs-price']);
+export function inScope(product) {
+  return product.vendor === 'Fentucci Naturals' || (product.tags || []).some(tag => QUOTE_TAGS.has(String(tag).trim().toLowerCase()));
+}
+
+export function badVariant(product) {
+  return product.variants.nodes.find(variant =>
+    !/sample/i.test(variant.title || '') &&
+    Number(variant.price) === 0 &&
+    variant.availableForSale === true
+  );
+}
+
+export function summarize(products, ts = new Date().toISOString()) {
+  // Shopify's search grammar can over-return on nested OR expressions. Enforce
+  // the intended scope locally as a second boundary before classifying defects.
+  const unique = [...new Map(products.map(product => [product.id, product])).values()].filter(inScope);
+  const bad = unique.flatMap(product => {
+    const variant = badVariant(product);
+    return variant ? [{ product, variant }] : [];
+  });
+  const verdict = bad.length === 0 ? 'PASS' : bad.length <= 5 ? 'WARN' : 'FAIL';
+  return {
+    skill: 'zero-price-orderable-canary', verdict, status: verdict, ts,
+    searched_active_unique: unique.length,
+    zero_price_orderable: bad.length,
+    by_vendor: Object.fromEntries([...new Set(bad.map(row => row.product.vendor || 'UNKNOWN'))].sort().map(vendor => [vendor, bad.filter(row => (row.product.vendor || 'UNKNOWN') === vendor).length])),
+    mechanism: {
+      qty_2026: bad.filter(row => row.variant.inventoryQuantity === 2026).length,
+      deny: bad.filter(row => row.variant.inventoryPolicy === 'DENY').length,
+      tracked: bad.filter(row => row.variant.inventoryItem?.tracked === true).length
+    },
+    detail: verdict === 'PASS' ? 'no scoped active product has a $0 orderable non-sample variant' : `${bad.length} products have a $0 ORDERABLE non-sample variant`,
+    sample_ids: bad.slice(0, 10).map(({ product, variant }) => ({ id: product.id.split('/').pop(), title: product.title, vendor: product.vendor, qty: variant.inventoryQuantity, policy: variant.inventoryPolicy }))
+  };
+}
+
+function writeResult(result) {
+  fs.mkdirSync(path.dirname(OUT), { recursive: true });
+  fs.writeFileSync(OUT, JSON.stringify(result, null, 2) + '\n');
+  console.log(`${result.verdict}: ${result.zero_price_orderable} zero-price-orderable of ${result.searched_active_unique} uniquely searched active`);
+}
+
+async function liveProducts() {
+  const env = fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
+  const val = key => (env.match(new RegExp(`^${key}=(.*)$`, 'm')) || [])[1]?.trim();
+  const api = `https://${val('SHOPIFY_STORE_DOMAIN')}/admin/api/2024-10/graphql.json`;
+  const token = val('SHOPIFY_ADMIN_TOKEN');
+  async function gql(query, variables) {
+    for (let attempt = 0; attempt < 6; attempt++) {
+      const response = await fetch(api, { method: 'POST', headers: { 'X-Shopify-Access-Token': token, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }) });
+      const json = await response.json();
+      if (!json.errors) return json.data;
+      if (!JSON.stringify(json.errors).includes('THROTTLED')) throw new Error(JSON.stringify(json.errors));
+      await new Promise(resolve => setTimeout(resolve, 1800 * (attempt + 1)));
+    }
+    throw new Error('GraphQL retries exhausted');
+  }
+  const products = [];
+  for (const q of SEARCHES) {
+    let after = null;
+    do {
+      const data = await gql(`query($q:String!,$after:String){products(first:100,query:$q,after:$after){pageInfo{hasNextPage endCursor} nodes{id title vendor tags variants(first:20){nodes{title price availableForSale inventoryPolicy inventoryQuantity inventoryItem{tracked}}}}}}`, { q, after });
+      products.push(...data.products.nodes);
+      after = data.products.pageInfo.hasNextPage ? data.products.pageInfo.endCursor : null;
+    } while (after);
+  }
+  return products;
+}
+
+async function main() {
+  const fixtureArg = process.argv.indexOf('--fixture');
+  const products = fixtureArg >= 0 ? JSON.parse(fs.readFileSync(process.argv[fixtureArg + 1], 'utf8')) : await liveProducts();
+  writeResult(summarize(products));
+}
+
+if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
+  main().catch(error => {
+    writeResult({ skill: 'zero-price-orderable-canary', verdict: 'WARN', status: 'WARN', ts: new Date().toISOString(), zero_price_orderable: null, searched_active_unique: 0, detail: `canary error: ${error.message}` });
+    process.exitCode = 1;
+  });
+}
diff --git a/tk10964-canary-guard/fixtures/mixed.json b/tk10964-canary-guard/fixtures/mixed.json
new file mode 100644
index 0000000..de394ed
--- /dev/null
+++ b/tk10964-canary-guard/fixtures/mixed.json
@@ -0,0 +1,8 @@
+[
+  {"id":"gid://shopify/Product/1","title":"PR bad","vendor":"Phillipe Romano","tags":["quote-only"],"variants":{"nodes":[{"title":"Per Yard","price":"0.00","availableForSale":true,"inventoryPolicy":"DENY","inventoryQuantity":2026,"inventoryItem":{"tracked":true}},{"title":"Sample","price":"4.25","availableForSale":false,"inventoryPolicy":"DENY","inventoryQuantity":0,"inventoryItem":{"tracked":true}}]}},
+  {"id":"gid://shopify/Product/2","title":"Fentucci blind spot","vendor":"Fentucci Naturals","tags":["Needs-Price"],"variants":{"nodes":[{"title":"Default Title","price":"0","availableForSale":true,"inventoryPolicy":"DENY","inventoryQuantity":2026,"inventoryItem":{"tracked":true}}]}},
+  {"id":"gid://shopify/Product/2","title":"Fentucci duplicate search hit","vendor":"Fentucci Naturals","tags":["quotes"],"variants":{"nodes":[{"title":"Default Title","price":"0","availableForSale":true,"inventoryPolicy":"DENY","inventoryQuantity":2026,"inventoryItem":{"tracked":true}}]}},
+  {"id":"gid://shopify/Product/3","title":"Unavailable zero control","vendor":"Control","tags":["contact-for-price"],"variants":{"nodes":[{"title":"Per Roll","price":"0","availableForSale":false,"inventoryPolicy":"DENY","inventoryQuantity":0,"inventoryItem":{"tracked":true}}]}},
+  {"id":"gid://shopify/Product/4","title":"Sample only control","vendor":"Control","tags":["quotes"],"variants":{"nodes":[{"title":"Sample","price":"0","availableForSale":true,"inventoryPolicy":"CONTINUE","inventoryQuantity":0,"inventoryItem":{"tracked":false}}]}},
+  {"id":"gid://shopify/Product/5","title":"Search parser over-return","vendor":"Unrelated","tags":["modern"],"variants":{"nodes":[{"title":"Per Roll","price":"0","availableForSale":true,"inventoryPolicy":"CONTINUE","inventoryQuantity":10,"inventoryItem":{"tracked":true}}]}}
+]
diff --git a/tk10964-canary-guard/test.mjs b/tk10964-canary-guard/test.mjs
new file mode 100644
index 0000000..0b6b353
--- /dev/null
+++ b/tk10964-canary-guard/test.mjs
@@ -0,0 +1,17 @@
+#!/usr/bin/env node
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import { SEARCHES, inScope, summarize } from './check.mjs';
+
+const products = JSON.parse(fs.readFileSync(new URL('./fixtures/mixed.json', import.meta.url)));
+const result = summarize(products, '2026-08-30T00:00:00.000Z');
+assert.equal(SEARCHES.length, 3);
+assert.ok(SEARCHES.some(search => search.includes("tag:'quotes'") && search.includes("tag:'Needs-Price'")));
+assert.ok(SEARCHES.some(search => search.includes("vendor:'Fentucci Naturals'")));
+assert.equal(inScope(products.at(-1)), false, 'search-parser over-return must be rejected locally');
+assert.equal(result.searched_active_unique, 4, 'duplicate query hits must collapse by product GID');
+assert.equal(result.zero_price_orderable, 2);
+assert.deepEqual(result.by_vendor, { 'Fentucci Naturals': 1, 'Phillipe Romano': 1 });
+assert.deepEqual(result.mechanism, { qty_2026: 2, deny: 2, tracked: 2 });
+assert.equal(result.verdict, 'WARN');
+console.log('PASS: broad searches, Fentucci blind spot, dedupe, defect predicate, and controls');
diff --git a/tk10964-canary-guard/verification/e2e-proof.json b/tk10964-canary-guard/verification/e2e-proof.json
new file mode 100644
index 0000000..a093a67
--- /dev/null
+++ b/tk10964-canary-guard/verification/e2e-proof.json
@@ -0,0 +1,48 @@
+{
+  "ticket": "TK-10964",
+  "intent": "Close the standing canary's Fentucci/quote-tag blind spot without changing Shopify state",
+  "risk_tier": "R1",
+  "environment": "local Node.js guard plus read-only Shopify Admin GraphQL",
+  "timestamp": "2026-08-30T18:27:00Z",
+  "baseline": "Existing canary searches quote-only tags and detects 1,281 Phillipe Romano products but misses 462 Fentucci Naturals products",
+  "checks": [
+    {
+      "name": "Syntax",
+      "command": "node --check tk10964-canary-guard/check.mjs",
+      "verdict": "PASS"
+    },
+    {
+      "name": "Fixture unit and boundary assertions",
+      "command": "node tk10964-canary-guard/test.mjs",
+      "assertions": "full tag family, Fentucci fallback, product-GID dedupe, sample exclusion, unavailable-$0 exclusion, search-overreturn rejection",
+      "verdict": "PASS"
+    },
+    {
+      "name": "Fixture end-to-end status artifact",
+      "command": "ZERO_PRICE_CANARY_OUT=/tmp/tk10964-fixture.json node tk10964-canary-guard/check.mjs --fixture tk10964-canary-guard/fixtures/mixed.json",
+      "assertions": "WARN, 4 unique in-scope products, 2 defects, one per affected vendor, both qty2026/DENY/tracked",
+      "verdict": "PASS"
+    },
+    {
+      "name": "Transport failure behavior",
+      "command": "network-restricted live invocation",
+      "assertions": "writes WARN artifact with zero_price_orderable=null and exits nonzero; never emits false PASS",
+      "verdict": "PASS"
+    },
+    {
+      "name": "Live Shopify read-only journey",
+      "command": "ZERO_PRICE_CANARY_OUT=/tmp/tk10964-live-filtered.json node tk10964-canary-guard/check.mjs",
+      "assertions": "FAIL; exact 1,743 defects: Phillipe Romano 1,281 plus Fentucci Naturals 462; all 1,743 qty2026, DENY, tracked",
+      "verdict": "PASS"
+    }
+  ],
+  "boundaries": {
+    "shopify": "GraphQL query operation only; source contains no mutation",
+    "database": "not contacted",
+    "filesystem": "temporary JSON evidence and project-local fixtures only",
+    "production_writes": 0
+  },
+  "cleanup": "Only /tmp evidence retained; no remote state created",
+  "verdict": "PASS",
+  "residual_gate": "The guard is committed as a reviewed local artifact but is not installed/deployed over the standing scheduled canary in this tranche"
+}

← 7eb582b Add apply-fix.mjs (restore-map-first, canary-batched, rollba  ·  back to Tk 10965 Zero Price Analysis  ·  snapshot live zero-price evidence before canary install db7f993 →