← back to Dw Smart Collection Canary
canary.mjs
136 lines
#!/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();