fix(uok-status): surface manualAttention bucket in status uok output

Codex audit follow-up (fix A). manual-attention outcomes were counted
by getGateRunStats but dropped from the user-facing surface — they
inflated `total` invisibly with no distinct column or key, so an
operator couldn't tell a gate with 5 pass / 3 manual-attention apart
from a gate with 5 pass / 3 fail.

Adds `manualAttention: number` to GateHealthEntry and renders it as
its own column between Fail and Retry in the human table. JSON
consumers get the new key alongside pass/fail/retry.

Test count for headless-uok-status.test.mjs: 30/30 (+2 new — column
present in header, distinguishable from fail in row).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-14 18:46:28 +02:00
parent 7794208340
commit 7000373e88
2 changed files with 31 additions and 6 deletions

View file

@ -79,6 +79,14 @@ export interface GateHealthEntry {
total: number;
pass: number;
fail: number;
/**
* Manual-attention outcomes (codex audit follow-up). Previously
* counted by getGateRunStats but dropped from the JSON/table output,
* which inflated `total` without an operator-visible bucket. Now
* surfaced as its own column so manual-attention gates are
* distinguishable from pass/fail in the at-a-glance view.
*/
manualAttention: number;
retry: number;
lastEvaluatedAt: string | null;
circuitBreaker: string;
@ -185,8 +193,8 @@ function formatTable(gates: GateHealthEntry[]): string {
return "No gate run data found in the last 24h.\n";
}
const header =
"| Gate | Scope | Coverage | Pass% | Pass | Fail | Retry | CB | Streak | Last Evaluated |\n" +
"|------|-------|----------|-------|------|------|-------|----|--------|----------------|\n";
"| Gate | Scope | Coverage | Pass% | Pass | Fail | Manual | Retry | CB | Streak | Last Evaluated |\n" +
"|------|-------|----------|-------|------|------|--------|-------|----|--------|----------------|\n";
const rows = gates
.map((g) => {
const last = g.lastEvaluatedAt
@ -195,7 +203,7 @@ function formatTable(gates: GateHealthEntry[]): string {
.replace("T", " ")
.slice(0, 19)
: "never";
return `| ${g.id} | ${g.scope} | ${coverageIcon(g.coverageStatus)} | ${passRate(g)} | ${g.pass} | ${g.fail} | ${g.retry} | ${cbIcon(g.circuitBreaker)} ${g.circuitBreaker} | ${g.failureStreak} | ${last} |`;
return `| ${g.id} | ${g.scope} | ${coverageIcon(g.coverageStatus)} | ${passRate(g)} | ${g.pass} | ${g.fail} | ${g.manualAttention} | ${g.retry} | ${cbIcon(g.circuitBreaker)} ${g.circuitBreaker} | ${g.failureStreak} | ${last} |`;
})
.join("\n");
return `${header}${rows}\n`;
@ -387,6 +395,7 @@ export async function handleUokStatus(
total: stats.total ?? 0,
pass: stats.pass ?? 0,
fail: stats.fail ?? 0,
manualAttention: stats.manualAttention ?? 0,
retry: stats.retry ?? 0,
// prefer stats window result; fall back to quality_gates last entry
lastEvaluatedAt: stats.lastEvaluatedAt ?? runContext.lastEvaluatedAt,

View file

@ -38,8 +38,8 @@ function formatTable(gates) {
return "No gate run data found in the last 24h.\n";
}
const header =
"| Gate | Scope | Coverage | Pass% | Pass | Fail | Retry | CB | Streak | Last Evaluated |\n" +
"|------|-------|----------|-------|------|------|-------|----|--------|----------------|\n";
"| Gate | Scope | Coverage | Pass% | Pass | Fail | Manual | Retry | CB | Streak | Last Evaluated |\n" +
"|------|-------|----------|-------|------|------|--------|-------|----|--------|----------------|\n";
const rows = gates
.map((g) => {
const last = g.lastEvaluatedAt
@ -48,7 +48,7 @@ function formatTable(gates) {
.replace("T", " ")
.slice(0, 19)
: "never";
return `| ${g.id} | ${g.scope} | ${coverageIcon(g.coverageStatus)} | ${passRate(g)} | ${g.pass} | ${g.fail} | ${g.retry} | ${cbIcon(g.circuitBreaker)} ${g.circuitBreaker} | ${g.failureStreak} | ${last} |`;
return `| ${g.id} | ${g.scope} | ${coverageIcon(g.coverageStatus)} | ${passRate(g)} | ${g.pass} | ${g.fail} | ${g.manualAttention ?? 0} | ${g.retry} | ${cbIcon(g.circuitBreaker)} ${g.circuitBreaker} | ${g.failureStreak} | ${last} |`;
})
.join("\n");
return `${header}${rows}\n`;
@ -105,6 +105,7 @@ function makeGate(overrides = {}) {
total: 10,
pass: 8,
fail: 2,
manualAttention: 0,
retry: 0,
lastEvaluatedAt: "2026-05-11T12:00:00.000Z",
circuitBreaker: "closed",
@ -172,6 +173,21 @@ describe("formatTable", () => {
assert.ok(result.includes("⚠ stale"));
});
it("includes_manual_attention_column_distinct_from_fail", () => {
// Codex audit follow-up: manualAttention used to be counted by
// getGateRunStats but dropped from the rendered output, inflating
// `total` invisibly. Now it's a column between Fail and Retry.
const result = formatTable([
makeGate({ id: "g-manual", fail: 0, manualAttention: 3 }),
]);
assert.ok(result.includes("| Manual |"), "header has Manual column");
// The row contains "| 0 | 3 |" — fail=0, manualAttention=3.
assert.ok(
/\|\s*0\s*\|\s*3\s*\|/.test(result),
"row shows manualAttention=3 distinct from fail=0",
);
});
it("includes_separator_row", () => {
const result = formatTable([makeGate()]);
assert.ok(result.includes("|------|"));