[object Object]

← 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

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)