← 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();