← 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;
}