feat(metrics): render the operator health snapshot as a report (#19)
Quality gates / quality (pull_request) Canceled after 0s

The counters had an endpoint and no way to read them without jq. Adds a pure
formatter plus a CLI that prints the app, queue, and model boundaries.

The formatter is a function of the snapshot and the three frozen vocabularies,
so a caller-supplied failure label cannot reach the terminal: unknown classes
are simply not printed. Refusal paths are explicit — 404 means the route is off
because no token is configured, 403 means the token was rejected — and the token
is never echoed in the report or an error line.

RED: tests/model-metrics-report-format.test.js failed with ERR_MODULE_NOT_FOUND
before src/model-metrics-report.js existed; the refusal-path test failed with a
generic 'answered 404' before the 404 branch was written.

Tests: npm test 298/298, npm run test:metrics-report 8/8, syntax and diff gates
clean, real-Chromium ui acceptance green with the privacy disclosure visible.
This commit is contained in:
Timmy committed 2026-11-02 10:56:16 +00:00
1 parent 3e2253a052
commit e08b537866
6 files changed
+311 -3

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
// Renders the operator snapshot from `src/model-metrics.js` as a plain-text report.
//
// This module is a pure function of the snapshot, on purpose: the report is built from numbers and
// from the three frozen vocabularies, so there is no path by which request content could reach the
// terminal even if the counters were compromised. Anything outside the enumerated vocabularies is
// not printed — an unknown failure class is visible as a missing row, never as a new label.
import { FAILURE_CLASSES, OUTCOMES } from './model-metrics.js';
const LABEL_WIDTH = 22;
function row(label, value) {
return ` ${String(label).padEnd(LABEL_WIDTH)}${value}`;
}
function duration(value) {
if (!Number.isFinite(value) || value < 0) return 'n/a';
if (value < 1_000) return `${Math.round(value)}ms`;
const seconds = value / 1_000;
if (seconds < 60) return `${seconds.toFixed(1)}s`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ${Math.round(seconds % 60)}s`;
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
}
function count(value) {
return Number.isFinite(value) ? String(value) : '0';
}
function readinessLine(model) {
const readiness = model?.readiness ?? {};
const phase = typeof readiness.phase === 'string' && readiness.phase ? readiness.phase : 'checking';
return readiness.coldLoadMs === null || readiness.coldLoadMs === undefined
? phase
: `${phase} · cold load ${duration(readiness.coldLoadMs)}`;
}
function abstentionLine(model) {
const rate = model?.abstentionRate;
const answered = (model?.outcomes?.suggestion ?? 0) + (model?.outcomes?.abstained ?? 0);
if (rate === null || rate === undefined || answered === 0) return 'no answers yet';
return `${(rate * 100).toFixed(1)}% of ${answered} answer${answered === 1 ? '' : 's'}`;
}
export function formatOperatorReport(snapshot) {
if (!snapshot || typeof snapshot !== 'object') throw new Error('a metrics snapshot is required');
const app = snapshot.app ?? {};
const queue = snapshot.queue ?? {};
const model = snapshot.model ?? {};
const latency = model.latency ?? {};
const outcomes = OUTCOMES.map(name => `${name} ${count(model.outcomes?.[name])}`).join(' · ');
const lines = [
`Timmy model health · ${snapshot.generatedAt ?? 'unknown time'} · schema ${snapshot.schema ?? '?'}`,
'',
'APP',
row('uptime', duration(app.uptimeMs)),
row('analyze requests', count(app.analyzeRequests)),
row('refused before model', count(app.refusedBeforeModel)),
'',
'QUEUE',
row('in flight', `${count(queue.inFlight)} / ${count(queue.limit)}`),
row('accepted', count(queue.accepted)),
row('at capacity', queue.atCapacity === true ? 'yes' : 'no'),
'',
'MODEL',
row('readiness', readinessLine(model)),
row('outcomes', outcomes),
row('abstention', abstentionLine(model)),
row('latency', `mean ${duration(latency.meanMs)} · max ${duration(latency.maxMs)} · samples ${count(latency.count)}`),
...FAILURE_CLASSES.map(name => row(`failure ${name}`, count(model.failureClasses?.[name]))),
];
return `${lines.join('\n')}\n`;
}