← back to Tk 10965 Zero Price Analysis
chore: syntax-validate + VERSION 1.0.0 (session close)
7f8dbb29b629779abde78cdfc605d28500595e07 · 2026-08-30 15:02:03 -0700 · steve@designerwallcoverings.com
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A VERSIONM tk10964-canary-guard/data/latest.jsonA tk10964-canary-guard/rollback/TK-10974-preinstall/check.mjs
Diff
commit 7f8dbb29b629779abde78cdfc605d28500595e07
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Sun Aug 30 15:02:03 2026 -0700
chore: syntax-validate + VERSION 1.0.0 (session close)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
VERSION | 1 +
tk10964-canary-guard/data/latest.json | 2 +-
.../rollback/TK-10974-preinstall/check.mjs | 41 ++++++++++++++++++++++
3 files changed, 43 insertions(+), 1 deletion(-)
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..3eefcb9
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+1.0.0
diff --git a/tk10964-canary-guard/data/latest.json b/tk10964-canary-guard/data/latest.json
index 75c521b..09436da 100644
--- a/tk10964-canary-guard/data/latest.json
+++ b/tk10964-canary-guard/data/latest.json
@@ -2,7 +2,7 @@
"skill": "zero-price-orderable-canary",
"verdict": "WARN",
"status": "WARN",
- "ts": "2026-08-30T21:27:37.393Z",
+ "ts": "2026-08-30T22:01:44.279Z",
"searched_active_unique": 4,
"zero_price_orderable": 2,
"by_vendor": {
diff --git a/tk10964-canary-guard/rollback/TK-10974-preinstall/check.mjs b/tk10964-canary-guard/rollback/TK-10974-preinstall/check.mjs
new file mode 100644
index 0000000..c6f885f
--- /dev/null
+++ b/tk10964-canary-guard/rollback/TK-10974-preinstall/check.mjs
@@ -0,0 +1,41 @@
+#!/usr/bin/env node
+// zero-price-orderable-canary — sibling of discontinued-orderable-canary (same root cause:
+// UI hides the buy button but the /cart/add endpoint still accepts the variant).
+// READ-ONLY: flags ACTIVE products with a NON-Sample variant priced $0 that is ORDERABLE
+// (availableForSale OR inventoryPolicy=CONTINUE) — a customer can check out at $0 (revenue loss)
+// or a quote-only item can be bought without a quote. Emits data/latest.json PASS/WARN/FAIL for
+// fleet-health-rollup. $0 (live Shopify reads unmetered). Ref incident: pending-approval/_approved/
+// 2026-07-29-quote-only-zero-price-checkout-exposure.md
+import fs from 'node:fs';
+import path from 'node:path';
+const HERE=path.dirname(new URL(import.meta.url).pathname);
+const OUT=path.join(HERE,'data','latest.json');
+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,1800*(a+1)));continue;}throw new Error(JSON.stringify(j.errors));}return j.data;}throw new Error('retries');}
+// Search quote-only tagged AND (separately) sample-priced tags; also catch any active with a $0 real variant.
+const SEARCH=`status:active AND (tag:'quote-only' OR tag:'Quote Only' OR tag:'quote_only' OR tag:'Quote-Only')`;
+// availableForSale===true is Shopify's authoritative "this variant is purchasable" signal.
+// (Do NOT OR-in inventoryPolicy=CONTINUE — that overcounts non-buyable rows.)
+function isBad(p){
+ return p.variants.nodes
+ .filter(v=>!/sample/i.test(v.title||''))
+ .some(v=> Number(v.price)===0 && v.availableForSale===true);
+}
+async function run(){
+ let after=null,all=[],pages=0;
+ do{ const d=await gql(`query($q:String!,$after:String){products(first:100,query:$q,after:$after){pageInfo{hasNextPage endCursor} nodes{id title variants(first:20){nodes{title price availableForSale inventoryPolicy}}}}}`,{q:SEARCH,after});
+ all.push(...d.products.nodes); after=d.products.pageInfo.hasNextPage?d.products.pageInfo.endCursor:null; pages++;
+ }while(after&&pages<40);
+ const bad=all.filter(isBad);
+ const verdict = bad.length===0 ? 'PASS' : (bad.length<=5 ? 'WARN' : 'FAIL');
+ const out={ skill:'zero-price-orderable-canary', verdict, status:verdict, ts:new Date().toISOString(),
+ quote_only_active: all.length, zero_price_orderable: bad.length,
+ detail: verdict==='PASS'?'no quote-only/active product has a $0 orderable non-sample variant':`${bad.length} products have a $0 ORDERABLE non-sample variant (checkout at $0 / quote bypass)`,
+ sample_ids: bad.slice(0,10).map(p=>({id:p.id.split('/').pop(),title:p.title})) };
+ fs.mkdirSync(path.dirname(OUT),{recursive:true}); fs.writeFileSync(OUT,JSON.stringify(out,null,2));
+ console.log(`${verdict}: ${bad.length} zero-price-orderable of ${all.length} quote-only active`);
+}
+run().catch(e=>{const out={skill:'zero-price-orderable-canary',verdict:'WARN',status:'WARN',ts:new Date().toISOString(),detail:'canary error: '+e.message};fs.mkdirSync(path.dirname(OUT),{recursive:true});fs.writeFileSync(OUT,JSON.stringify(out,null,2));console.error('ERR',e.message);process.exit(0);});
← 7e1dd31 auto-data-snapshot: 2026-08-30T14:42:48 (4 data files) — tk1
·
back to Tk 10965 Zero Price Analysis
·
auto-data-snapshot: 2026-08-30T15:20:13 (1 data files) — tk1 ae6d884 →