Public: anyone can read this; the crew can change it.
A pure step says when a clog starts and when it clears, once each, across readings #6527
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6518-a-pure-step-says-when-a-clog-starts-and → main 1 commit fork
2 files
changed +77
−0
src/flow/clog_watch.ts added +37
| @@ -0,0 +1,37 @@ | ||
| 1 | /** | |
| 2 | * Clog watch (#6518): turns a stream of clog readings into one "started" and | |
| 3 | * one "cleared" event per clog. A reading is the list of reasons the flow is | |
| 4 | * clogged now (empty when it is not). A clog clears only after QUIET_TO_CLEAR | |
| 5 | * quiet readings in a row, so a flapping reason does not notify twice. | |
| 6 | */ | |
| 7 | ||
| 8 | export const QUIET_TO_CLEAR = 2; | |
| 9 | ||
| 10 | export interface ClogState { | |
| 11 | /** When the clog started, epoch ms. */ | |
| 12 | readonly since: number; | |
| 13 | /** The reasons on the reading that started it. */ | |
| 14 | readonly firstReasons: readonly string[]; | |
| 15 | /** Quiet readings in a row so far; absent when zero. */ | |
| 16 | readonly quiet?: number; | |
| 17 | } | |
| 18 | ||
| 19 | export type ClogEvent = "started" | "cleared"; | |
| 20 | ||
| 21 | export interface ClogStep { | |
| 22 | readonly state: ClogState | null; | |
| 23 | readonly event: ClogEvent | null; | |
| 24 | /** On "cleared": when the clog had started. */ | |
| 25 | readonly since?: number; | |
| 26 | } | |
| 27 | ||
| 28 | export function clogStep(prev: ClogState | null, reading: readonly string[], now: number): ClogStep { | |
| 29 | if (!prev) { | |
| 30 | if (reading.length === 0) return { state: null, event: null }; | |
| 31 | return { state: { since: now, firstReasons: [...reading] }, event: "started" }; | |
| 32 | } | |
| 33 | if (reading.length > 0) return { state: { since: prev.since, firstReasons: prev.firstReasons }, event: null }; | |
| 34 | const quiet = (prev.quiet ?? 0) + 1; | |
| 35 | if (quiet >= QUIET_TO_CLEAR) return { state: null, event: "cleared", since: prev.since }; | |
| 36 | return { state: { ...prev, quiet }, event: null }; | |
| 37 | } |
test/flow-clog-watch.test.ts added +40
| @@ -0,0 +1,40 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { clogStep } from "../src/flow/clog_watch.ts"; | |
| 3 | ||
| 4 | test("quiet with no clog stays null", () => { | |
| 5 | expect(clogStep(null, [], 1)).toEqual({ state: null, event: null }); | |
| 6 | }); | |
| 7 | ||
| 8 | test("a reason starts a clog once and it stands without repeats", () => { | |
| 9 | const a = clogStep(null, ["red main"], 10); | |
| 10 | expect(a.event).toBe("started"); | |
| 11 | expect(a.state).toEqual({ since: 10, firstReasons: ["red main"] }); | |
| 12 | const b = clogStep(a.state, ["red main", "stuck"], 20); | |
| 13 | expect(b.event).toBeNull(); | |
| 14 | expect(b.state).toEqual({ since: 10, firstReasons: ["red main"] }); | |
| 15 | }); | |
| 16 | ||
| 17 | test("one quiet reading is still standing; a reason resets the count", () => { | |
| 18 | const a = clogStep(null, ["x"], 10); | |
| 19 | const b = clogStep(a.state, [], 20); | |
| 20 | expect(b.event).toBeNull(); | |
| 21 | expect(b.state?.since).toBe(10); | |
| 22 | const c = clogStep(b.state, ["x"], 30); | |
| 23 | const d = clogStep(c.state, [], 40); | |
| 24 | expect(d.event).toBeNull(); | |
| 25 | expect(d.state).not.toBeNull(); | |
| 26 | }); | |
| 27 | ||
| 28 | test("two quiet readings in a row clear it with the start time", () => { | |
| 29 | let s = clogStep(null, ["x"], 10).state; | |
| 30 | s = clogStep(s, [], 20).state; | |
| 31 | const c = clogStep(s, [], 30); | |
| 32 | expect(c).toEqual({ state: null, event: "cleared", since: 10 }); | |
| 33 | }); | |
| 34 | ||
| 35 | test("a clog after a clear starts anew", () => { | |
| 36 | const c = clogStep(clogStep(clogStep(null, ["x"], 10).state, [], 20).state, [], 30); | |
| 37 | const r = clogStep(c.state, ["y"], 50); | |
| 38 | expect(r.event).toBe("started"); | |
| 39 | expect(r.state).toEqual({ since: 50, firstReasons: ["y"] }); | |
| 40 | }); |
new above
offline