← back to Tk 10821 Enrichment Health Classifier

classify.mjs

50 lines

#!/usr/bin/env node
// TK-10821: pure, local enrichment-health classifier.
// It consumes an evidence snapshot and cannot call APIs, databases, or schedules.
import fs from 'node:fs';

export function classify(snapshot) {
  const required = ['hours_since_last_advance', 'processed_24h', 'cron_last_exit', 'cron_completed_today', 'actionable_pending'];
  const missing = required.filter(key => snapshot[key] === null || snapshot[key] === undefined);
  if (missing.length) return { verdict: 'DEGRADED_OBSERVABILITY', stalled: null, reasons: [`missing: ${missing.join(', ')}`] };

  const fresh = Number(snapshot.hours_since_last_advance) <= 48;
  const processed = Number(snapshot.processed_24h) > 0;
  const cronHealthy = snapshot.cron_last_exit === 0 && snapshot.cron_completed_today === true;
  const stalled = !fresh && !processed && !cronHealthy;
  const reasons = [
    `last phase3 advance ${snapshot.hours_since_last_advance}h ago`,
    `${snapshot.processed_24h} completed in 24h`,
    `cron last_exit=${snapshot.cron_last_exit}, completed_today=${snapshot.cron_completed_today}`
  ];
  if (stalled) return { verdict: 'STALLED', stalled: true, reasons };

  const attempted = Number(snapshot.sample_attempted || 0);
  const updated = Number(snapshot.sample_updated || 0);
  const noData = Number(snapshot.sample_skipped_no_data || 0);
  if (attempted > 0 && updated / attempted < 0.05 && noData / attempted >= 0.8) {
    return {
      verdict: 'ALIVE_UPSTREAM_CONSTRAINED', stalled: false,
      reasons: [...reasons, `sample run updated ${updated}/${attempted}; ${noData}/${attempted} returned no AI data`]
    };
  }

  const previous = Number(snapshot.previous_actionable_pending);
  const current = Number(snapshot.actionable_pending);
  if (Number.isFinite(previous) && current >= previous) {
    return {
      verdict: 'ALIVE_NET_BACKLOG_FLAT_OR_GROWING', stalled: false,
      reasons: [...reasons, `net pending ${previous}->${current}; net delta is not a liveness measurement`]
    };
  }
  return { verdict: 'ALIVE_DRAINING', stalled: false, reasons };
}

if (process.argv[1] && new URL(import.meta.url).pathname === process.argv[1]) {
  const input = process.argv[2];
  if (!input) throw new Error('usage: node classify.mjs SNAPSHOT.json');
  const result = classify(JSON.parse(fs.readFileSync(input, 'utf8')));
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
  process.exitCode = result.verdict === 'STALLED' || result.verdict === 'DEGRADED_OBSERVABILITY' ? 1 : 0;
}