← back to Dw Smart Collection Canary
feat: dw-smart-collection-canary — monitors empty Shopify collections in collection_audits (TK-11488)
b6e14ea07110f0921faff44ca0bc9154054d075c · 2026-09-12 04:09:58 -0700 · steve@designerwallcoverings.com
Files touched
A README.mdA canary.mjsA com.steve.dw-smart-collection-canary.plistA data/latest.json
Diff
commit b6e14ea07110f0921faff44ca0bc9154054d075c
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Sat Sep 12 04:09:58 2026 -0700
feat: dw-smart-collection-canary — monitors empty Shopify collections in collection_audits (TK-11488)
---
README.md | 33 +++++++
canary.mjs | 135 +++++++++++++++++++++++++++++
com.steve.dw-smart-collection-canary.plist | 23 +++++
data/latest.json | 17 ++++
4 files changed, 208 insertions(+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6f6d7d6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,33 @@
+# dw-smart-collection-canary — TK-11488
+
+Monitors Shopify smart collection membership health. Flags empty collections
+that regress beyond a recorded floor, using data already captured by the
+collection-creator audit workflow in `dw_unified.collection_audits`.
+
+## Verdicts (fleet-health-rollup vocabulary)
+
+- **FAIL** — new empty collections beyond the floor (regression)
+- **WARN** — empty collections exist but within known floor
+- **PASS** — all measured collections healthy
+
+## Negative test (CLAUDE.md rule 3: ships with a negative test)
+
+```sh
+TEST_INJECT_EMPTY=5 node canary.mjs
+# → Must print FAIL with new_vs_floor=5
+```
+
+## Install launchd (Steve-gated paste)
+
+```sh
+cp com.steve.dw-smart-collection-canary.plist ~/Library/LaunchAgents/
+launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.steve.dw-smart-collection-canary.plist
+launchctl list | grep dw-smart-collection-canary
+```
+
+Undo: `launchctl bootout gui/$(id -u)/com.steve.dw-smart-collection-canary`
+
+## Data files
+
+- `data/latest.json` — most recent run result
+- `data/floor.json` — accepted empty-collection count baseline
diff --git a/canary.mjs b/canary.mjs
new file mode 100644
index 0000000..8c2b1e5
--- /dev/null
+++ b/canary.mjs
@@ -0,0 +1,135 @@
+#!/usr/bin/env node
+/**
+ * dw-smart-collection-canary — TK-11488
+ * Monitors for empty and at-risk Shopify smart collections.
+ * Reads collection_audits table (dw_unified PG) for the latest audit snapshot.
+ *
+ * Verdict: FAIL = new empty collections vs floor | WARN = any empty collections | PASS = clean
+ *
+ * Fleet-health-rollup vocabulary: PASS / WARN / FAIL
+ */
+import { execSync } from 'node:child_process';
+import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const DATA_DIR = join(__dirname, 'data');
+const LATEST_PATH = join(DATA_DIR, 'latest.json');
+const FLOOR_PATH = join(DATA_DIR, 'floor.json');
+
+// Negative-test injection: if TEST_INJECT_EMPTY=5, pretend 5 empty collections were found
+const TEST_INJECT = process.env.TEST_INJECT_EMPTY ? parseInt(process.env.TEST_INJECT_EMPTY) : 0;
+
+if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
+
+function psql(sql) {
+ try {
+ const out = execSync(`psql -h /tmp dw_unified -t -A -c "${sql}"`, { encoding: 'utf8', timeout: 30000 });
+ return out.trim();
+ } catch (e) {
+ return null;
+ }
+}
+
+function main() {
+ const now = new Date().toISOString();
+
+ // Query collection_audits for the most recent snapshot
+ const rawCount = psql("SELECT COUNT(*) FROM collection_audits;");
+ const auditCount = rawCount ? parseInt(rawCount) : 0;
+
+ if (auditCount === 0) {
+ // UNMEASURED — no audit data available; per CLAUDE.md rule: absence of bad news is NOT pass
+ const result = {
+ verdict: 'WARN',
+ checked_at: now,
+ population: 0,
+ empty_count: 0,
+ new_vs_floor: 0,
+ note: 'NOT_MEASURED — collection_audits table is empty; cannot assert any collection is healthy',
+ alert_delivered: false
+ };
+ writeFileSync(LATEST_PATH, JSON.stringify(result, null, 2));
+ console.log('WARN: collection_audits empty — cannot measure');
+ process.exit(0);
+ }
+
+ // Get the latest audit row's empty collection list
+ const rawAudit = psql("SELECT audit_data FROM collection_audits ORDER BY created_at DESC LIMIT 1;");
+ let emptyCollections = [];
+ let population = 0;
+
+ try {
+ const auditData = JSON.parse(rawAudit);
+ emptyCollections = TEST_INJECT > 0
+ ? Array.from({ length: TEST_INJECT }, (_, i) => ({ id: `test-${i}`, title: `Test Empty ${i}` }))
+ : (auditData.empty || []);
+ // Population = empty + non-empty would need a separate count; use empty as the known signal
+ population = emptyCollections.length + (auditData.total_checked || 0);
+ } catch (e) {
+ const result = {
+ verdict: 'WARN',
+ checked_at: now,
+ population: 0,
+ empty_count: 0,
+ note: `PARSE_ERROR: ${e.message}`,
+ alert_delivered: false
+ };
+ writeFileSync(LATEST_PATH, JSON.stringify(result, null, 2));
+ console.log('WARN: parse error on audit_data');
+ process.exit(0);
+ }
+
+ const emptyCount = emptyCollections.length;
+
+ // Load floor (baseline)
+ let floor = 0;
+ if (existsSync(FLOOR_PATH) && !TEST_INJECT) {
+ try { floor = JSON.parse(readFileSync(FLOOR_PATH, 'utf8')).empty_count || 0; } catch {}
+ }
+
+ const newVsFloor = Math.max(0, emptyCount - floor);
+
+ // Verdict
+ let verdict = 'PASS';
+ if (newVsFloor > 0) verdict = 'FAIL';
+ else if (emptyCount > 0) verdict = 'WARN';
+
+ const result = {
+ verdict,
+ checked_at: now,
+ population,
+ empty_count: emptyCount,
+ new_vs_floor: newVsFloor,
+ floor_at_check: floor,
+ sample_empty: emptyCollections.slice(0, 5).map(c => c.title || c.id),
+ note: verdict === 'FAIL'
+ ? `${newVsFloor} NEW empty collections beyond floor of ${floor}`
+ : verdict === 'WARN'
+ ? `${emptyCount} empty collections (known, within floor)`
+ : 'All measured collections healthy',
+ alert_delivered: false
+ };
+
+ writeFileSync(LATEST_PATH, JSON.stringify(result, null, 2));
+
+ // Update floor when clean
+ if (verdict === 'PASS' && !TEST_INJECT) {
+ writeFileSync(FLOOR_PATH, JSON.stringify({ empty_count: emptyCount, set_at: now }, null, 2));
+ }
+
+ if (verdict === 'FAIL') {
+ // Alert via CNCP (shared sender — per CLAUDE.md rule 2: never hand-roll curl)
+ try {
+ execSync(`bash ~/.claude/skills/_shared/cncp_post.sh "dw-smart-collection-canary" "${newVsFloor} new empty Shopify collections — action needed (TK-11488)"`, { timeout: 15000 });
+ result.alert_delivered = true;
+ writeFileSync(LATEST_PATH, JSON.stringify(result, null, 2));
+ } catch {}
+ }
+
+ console.log(`${verdict}: ${emptyCount} empty collections (${newVsFloor} new vs floor ${floor})`);
+ process.exit(verdict === 'FAIL' ? 1 : 0);
+}
+
+main();
diff --git a/com.steve.dw-smart-collection-canary.plist b/com.steve.dw-smart-collection-canary.plist
new file mode 100644
index 0000000..5d3e540
--- /dev/null
+++ b/com.steve.dw-smart-collection-canary.plist
@@ -0,0 +1,23 @@
+<?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-smart-collection-canary</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>/usr/local/bin/node</string>
+ <string>/Users/macstudio3/.claude/skills/dw-smart-collection-canary/canary.mjs</string>
+ </array>
+ <key>StartInterval</key>
+ <integer>14400</integer>
+ <key>StandardOutPath</key>
+ <string>/Users/macstudio3/.claude/skills/dw-smart-collection-canary/data/launchd.out</string>
+ <key>StandardErrorPath</key>
+ <string>/Users/macstudio3/.claude/skills/dw-smart-collection-canary/data/launchd.err</string>
+ <key>KeepAlive</key>
+ <false/>
+ <key>RunAtLoad</key>
+ <false/>
+</dict>
+</plist>
diff --git a/data/latest.json b/data/latest.json
new file mode 100644
index 0000000..b9023f4
--- /dev/null
+++ b/data/latest.json
@@ -0,0 +1,17 @@
+{
+ "verdict": "FAIL",
+ "checked_at": "2026-09-12T11:09:45.667Z",
+ "population": 5,
+ "empty_count": 5,
+ "new_vs_floor": 5,
+ "floor_at_check": 0,
+ "sample_empty": [
+ "Schumacher Trim",
+ "Hollywood Vinyls Vol. 1",
+ "De Gournay",
+ "Stout Wallcoverings",
+ "1838"
+ ],
+ "note": "5 NEW empty collections beyond floor of 0",
+ "alert_delivered": true
+}
\ No newline at end of file
(oldest)
·
back to Dw Smart Collection Canary
·
(newest)