Public: anyone can read this; the crew can change it.
forgeVerdict(view, now) names what is wrong with the forge itself, in one pure function #2330
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2323-forgeverdict-view-now-names-what-is-wron → main 2 commits fork 1 thread · 1 unresolved
2 files
changed +61
−0
src/kernel/guard_verdict.ts added +28
| @@ -0,0 +1,28 @@ | ||
| 1 | /** | |
| 2 | * What is wrong with the forge itself, in one sentence, from the guard view | |
| 3 | * and the time. Pure: no reads, no clock. Undefined means nothing to say; a | |
| 4 | * view that was never written (no `checkedAt`) is not stale. | |
| 5 | */ | |
| 6 | ||
| 7 | import { GUARD_STALE_MS, type Guard } from "./guard.ts"; | |
| 8 | ||
| 9 | /** Draining longer than this is a release that did not finish. */ | |
| 10 | export const DRAIN_LIMIT_MS = 5 * 60_000; | |
| 11 | ||
| 12 | export interface VerdictView extends Guard { | |
| 13 | /** When this process began draining (epoch ms), if it has. */ | |
| 14 | readonly drainingSince?: number | null; | |
| 15 | } | |
| 16 | ||
| 17 | export function forgeVerdict(view: VerdictView, now: number): string | undefined { | |
| 18 | const found = view.incidents.find( | |
| 19 | (i) => i.key === "held" || i.key === "stuck" || i.key.startsWith("oom:"), | |
| 20 | ); | |
| 21 | if (found) return `${found.key}: ${found.message}`; | |
| 22 | const since = view.drainingSince; | |
| 23 | if (typeof since === "number" && now - since > DRAIN_LIMIT_MS) | |
| 24 | return `draining for ${Math.floor((now - since) / 60_000)} minutes`; | |
| 25 | if (view.checkedAt !== null && now - Date.parse(view.checkedAt) > GUARD_STALE_MS) | |
| 26 | return `the platform's view is stale: its last check was ${view.checkedAt}`; | |
| 27 | return undefined; | |
| 28 | } |
test/guard-verdict.test.ts added +33
| @@ -0,0 +1,33 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { forgeVerdict } from "../src/kernel/guard_verdict.ts"; | |
| 3 | ||
| 4 | const NOW = Date.parse("2026-10-02T12:00:00Z"); | |
| 5 | const fresh = "2026-10-02T11:59:00Z"; | |
| 6 | const view = (incidents: Record<string, string>, checkedAt: string | null = fresh) => ({ | |
| 7 | incidents: Object.entries(incidents).map(([key, message]) => ({ key, message })), | |
| 8 | checkedAt, | |
| 9 | }); | |
| 10 | ||
| 11 | test("a held incident is named with its own message", () => { | |
| 12 | expect(forgeVerdict(view({ held: "preflight refused 1.2" }), NOW)).toContain("preflight refused 1.2"); | |
| 13 | }); | |
| 14 | ||
| 15 | test("stuck and oom incidents are named; edge alone is not", () => { | |
| 16 | expect(forgeVerdict(view({ stuck: "rollout cannot finish" }), NOW)).toContain("cannot finish"); | |
| 17 | expect(forgeVerdict(view({ "oom:web": "killed" }), NOW)).toContain("killed"); | |
| 18 | expect(forgeVerdict(view({ edge: "slow" }), NOW)).toBeUndefined(); | |
| 19 | }); | |
| 20 | ||
| 21 | test("a stale view gives a text", () => { | |
| 22 | expect(forgeVerdict(view({}, "2026-10-02T11:00:00Z"), NOW)).toContain("stale"); | |
| 23 | }); | |
| 24 | ||
| 25 | test("draining past five minutes gives a text, before that none", () => { | |
| 26 | expect(forgeVerdict({ ...view({}), drainingSince: NOW - 6 * 60_000 }, NOW)).toContain("draining"); | |
| 27 | expect(forgeVerdict({ ...view({}), drainingSince: NOW - 4 * 60_000 }, NOW)).toBeUndefined(); | |
| 28 | }); | |
| 29 | ||
| 30 | test("an empty view and a never-written view give undefined", () => { | |
| 31 | expect(forgeVerdict(view({}), NOW)).toBeUndefined(); | |
| 32 | expect(forgeVerdict(view({}, null), NOW)).toBeUndefined(); | |
| 33 | }); |
new above
offline
This repeats the staleness rule from guard.ts:101. Export one
isStale(guard, now)from guard.ts and use it here. Decide what an unparseablecheckedAtmeans, because NaN currently reads as fresh.