← back to Designer Wallcoverings
Add stock-2026-guard: self-healing enforcement of the active-products-never-OOS hard rule
8664e4edc5380824e73b84b6dc4b7ac28836ae4c · 2026-06-20 11:53:54 -0700 · Steve
Scans the live store for any ACTIVE product whose TRACKED inventory drifted to <=0 and
re-sets every tracked variant to 2026 on_hand at the Ventura Blvd location (the exact
inventorySetQuantities mechanism cadence-import.js uses at activation). Skips untracked
POD/sample items (their 0 is intentional). Dry-run by default; --commit applies. Scheduled
every 6h via com.steve.dw-stock-2026-guard. First live run: 2540 candidates scanned, 0 tracked
products needed fixing (catalog already compliant). Backs the import-time rule as a drift net.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
M .gitignoreA shopify/scripts/cadence/com.steve.dw-stock-2026-guard.plistA shopify/scripts/cadence/stock-2026-guard.js
Diff
commit 8664e4edc5380824e73b84b6dc4b7ac28836ae4c
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jun 20 11:53:54 2026 -0700
Add stock-2026-guard: self-healing enforcement of the active-products-never-OOS hard rule
Scans the live store for any ACTIVE product whose TRACKED inventory drifted to <=0 and
re-sets every tracked variant to 2026 on_hand at the Ventura Blvd location (the exact
inventorySetQuantities mechanism cadence-import.js uses at activation). Skips untracked
POD/sample items (their 0 is intentional). Dry-run by default; --commit applies. Scheduled
every 6h via com.steve.dw-stock-2026-guard. First live run: 2540 candidates scanned, 0 tracked
products needed fixing (catalog already compliant). Backs the import-time rule as a drift net.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
.gitignore | 1 +
.../cadence/com.steve.dw-stock-2026-guard.plist | 13 +++
shopify/scripts/cadence/stock-2026-guard.js | 111 +++++++++++++++++++++
3 files changed, 125 insertions(+)
diff --git a/.gitignore b/.gitignore
index d6dd3bfe..f34e79d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,3 +70,4 @@ DW-Websites/LosAngelesFabrics/node_modules/
DW-Programming/room-setting-app/data/gallery/renders.json
DW-Programming/data/color-clean-ledger.jsonl
shopify/scripts/gmc-race-clean/out/
+shopify/scripts/cadence/stock-2026-guard.latest.json
diff --git a/shopify/scripts/cadence/com.steve.dw-stock-2026-guard.plist b/shopify/scripts/cadence/com.steve.dw-stock-2026-guard.plist
new file mode 100644
index 00000000..c3751661
--- /dev/null
+++ b/shopify/scripts/cadence/com.steve.dw-stock-2026-guard.plist
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0"><dict>
+ <key>Label</key><string>com.steve.dw-stock-2026-guard</string>
+ <key>ProgramArguments</key><array>
+ <string>/bin/bash</string><string>-lc</string>
+ <string>/usr/local/bin/node "$HOME/Projects/Designer-Wallcoverings/shopify/scripts/cadence/stock-2026-guard.js" --commit >> /tmp/dw-stock-2026-guard.log 2>&1</string>
+ </array>
+ <key>StartInterval</key><integer>21600</integer>
+ <key>RunAtLoad</key><false/>
+ <key>StandardOutPath</key><string>/tmp/dw-stock-2026-guard.launchd.out.log</string>
+ <key>StandardErrorPath</key><string>/tmp/dw-stock-2026-guard.launchd.err.log</string>
+</dict></plist>
diff --git a/shopify/scripts/cadence/stock-2026-guard.js b/shopify/scripts/cadence/stock-2026-guard.js
new file mode 100644
index 00000000..c6baf24c
--- /dev/null
+++ b/shopify/scripts/cadence/stock-2026-guard.js
@@ -0,0 +1,111 @@
+'use strict';
+/**
+ * stock-2026-guard.js — HARD RULE "active products are never out of stock" (Steve 2026-06-20).
+ *
+ * Scans the LIVE store for any ACTIVE product whose TRACKED inventory has drifted to <= 0
+ * (out of stock / oversold) and re-sets EVERY tracked variant of that product to 2026 on_hand
+ * at the Ventura Blvd location — the exact mechanism cadence-import.js uses at activation.
+ * This is the self-healing net behind the import-time rule: if a tracked active product ever
+ * reads 0/negative (oversell, manual edit, activation outside the cadence path), it gets restored.
+ *
+ * Deliberately SKIPS untracked variants (print-on-demand / sample / made-to-order items) —
+ * their "0" is intentional and forcing 2026 on them would be wrong.
+ *
+ * SAFE BY DEFAULT: dry-run unless --commit is passed. The scheduled launchd job passes --commit.
+ *
+ * Run: node stock-2026-guard.js # dry-run, report only
+ * node stock-2026-guard.js --commit # apply the fix
+ */
+const fs = require('fs');
+const path = require('path');
+
+// ---- portable secret load (Mac2 secrets master, co-located .env, or Kamatera /root/.env) ----
+const ENV_PATHS = [
+ process.env.GUARD_ENV_FILE,
+ path.join(__dirname, '.env'),
+ '/Users/stevestudio2/Projects/secrets-manager/.env',
+ process.env.HOME ? path.join(process.env.HOME, '.env') : null,
+ '/root/.env',
+].filter(Boolean);
+let ENVTXT = '';
+for (const p of ENV_PATHS) { try { ENVTXT = fs.readFileSync(p, 'utf8'); break; } catch (e) {} }
+const env = k => process.env[k] || (ENVTXT.match(new RegExp('^' + k + '=(.+)$', 'm')) || [])[1];
+
+const STORE = (env('SHOPIFY_STORE') || 'designer-laboratory-sandbox.myshopify.com').replace(/^https?:\/\//, '');
+const TOKEN = env('SHOPIFY_ADMIN_TOKEN');
+const EP = `https://${STORE}/admin/api/2024-10/graphql.json`;
+const INV_LOCATION = 'gid://shopify/Location/5795643504'; // 15442 Ventura Blvd. (matches cadence-import.js)
+const QTY = 2026;
+const COMMIT = process.argv.includes('--commit');
+const OUT = path.join(__dirname, 'stock-2026-guard.latest.json');
+
+if (!TOKEN) { console.error('[stock-2026-guard] no SHOPIFY_ADMIN_TOKEN — abort'); process.exit(2); }
+
+async function gql(query, variables) {
+ for (let attempt = 0; attempt < 5; attempt++) {
+ const r = await fetch(EP, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }) });
+ if (r.status === 429 || r.status >= 500) { await new Promise(s => setTimeout(s, 1000 * (attempt + 1))); continue; }
+ const j = await r.json();
+ if (j.errors && /throttl/i.test(JSON.stringify(j.errors))) { await new Promise(s => setTimeout(s, 1500 * (attempt + 1))); continue; }
+ return j;
+ }
+ throw new Error('gql retries exhausted');
+}
+
+const SCAN = `query($cursor:String){ products(first:100, after:$cursor, query:"status:active inventory_total:<1"){
+ pageInfo{ hasNextPage endCursor }
+ nodes{ id title vendor variants(first:50){ nodes{ sku inventoryQuantity inventoryItem{ id tracked } } } }
+} }`;
+const INV_SET = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{ field message code } } }`;
+
+(async () => {
+ const started = new Date().toISOString();
+ const targets = []; // { title, vendor, items:[inventoryItemId], skus:[] }
+ let cursor = null, pages = 0, scanned = 0;
+ do {
+ const j = await gql(SCAN, { cursor });
+ const conn = j.data && j.data.products;
+ if (!conn) { console.error('[stock-2026-guard] scan error:', JSON.stringify(j.errors || j)); break; }
+ for (const p of conn.nodes) {
+ scanned++;
+ const trackedVars = p.variants.nodes.filter(v => v.inventoryItem && v.inventoryItem.tracked && v.inventoryItem.id);
+ // only act when a TRACKED variant is actually <= 0 (skip untracked POD/sample items)
+ const hasZero = trackedVars.some(v => (v.inventoryQuantity ?? 0) <= 0);
+ if (hasZero && trackedVars.length) {
+ targets.push({ title: p.title, vendor: p.vendor, items: trackedVars.map(v => v.inventoryItem.id), skus: trackedVars.map(v => v.sku) });
+ }
+ }
+ cursor = conn.pageInfo.hasNextPage ? conn.pageInfo.endCursor : null;
+ pages++;
+ } while (cursor && pages < 200);
+
+ const pairs = targets.flatMap(t => t.items.map(id => ({ inventoryItemId: id, locationId: INV_LOCATION, quantity: QTY })));
+ console.log(`[stock-2026-guard] scanned ${scanned} active-≤0 candidates · ${targets.length} products with a TRACKED variant ≤0 · ${pairs.length} variant-sets needed`);
+ targets.slice(0, 15).forEach(t => console.log(' ·', String(t.title).slice(0, 50), '|', t.skus.join(', ')));
+
+ let applied = 0; const errors = [];
+ if (COMMIT && pairs.length) {
+ for (let i = 0; i < pairs.length; i += 250) {
+ const r = await gql(INV_SET, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true, quantities: pairs.slice(i, i + 250) } });
+ const ue = (r.data && r.data.inventorySetQuantities && r.data.inventorySetQuantities.userErrors) || [];
+ if (ue.length) errors.push(...ue); else applied += pairs.slice(i, i + 250).length;
+ }
+ console.log(`[stock-2026-guard] COMMIT: set ${applied}/${pairs.length} variants to ${QTY}` + (errors.length ? ` · ${errors.length} userErrors` : ''));
+ } else if (pairs.length) {
+ console.log(`[stock-2026-guard] DRY-RUN: would set ${pairs.length} variants to ${QTY}. Pass --commit to apply.`);
+ } else {
+ console.log('[stock-2026-guard] nothing to fix — every active tracked product is in stock. ✅');
+ }
+
+ const report = { ts: started, store: STORE, committed: COMMIT, scanned, productsFixed: targets.length, variantSets: pairs.length, applied, errors: errors.slice(0, 20) };
+ try { fs.writeFileSync(OUT, JSON.stringify(report, null, 2)); } catch (e) {}
+
+ // best-effort CNCP card when it actually fixed something (matches the DW canary fleet pattern)
+ if (COMMIT && applied) {
+ try {
+ await fetch('http://127.0.0.1:3333/api/wins', { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: AbortSignal.timeout(2500),
+ body: JSON.stringify({ project: 'dw-shopify', title: `stock-2026-guard restored ${targets.length} active product(s) to 2026`, summary: `Active products had drifted to ≤0 tracked inventory; reset ${applied} variants to 2026 at Ventura Blvd.` }) });
+ } catch (e) {}
+ }
+ process.exit(errors.length ? 1 : 0);
+})().catch(e => { console.error('[stock-2026-guard] fatal:', e.message); process.exit(2); });
← 84e9e2e1 cadence: backfill+normalize widths to DW canonical 'N Inches
·
back to Designer Wallcoverings
·
fix(activation-gate): stem-compare images + non-blocking all 09a7f4be →