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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2323-forgeverdict-view-now-names-what-is-wron → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/2323-forgeverdict-view-now-names-what-is-wron was merged into main as 964e722.

Review

Sign in to review this change.
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)

This repeats the staleness rule from guard.ts:101. Export one isStale(guard, now) from guard.ts and use it here. Decide what an unparseable checkedAt means, because NaN currently reads as fresh.

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

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs