Public: anyone can read this; the crew can change it.
A pure function writes the note posted when a clog clears: cause, time to detect, time to clear, stranded dollars #6539
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6519-a-pure-function-writes-the-note-posted-w → main 1 commit fork
2 files
changed +74
−0
src/flow/clog_note.ts added +41
| @@ -0,0 +1,41 @@ | ||
| 1 | /** | |
| 2 | * The note posted when a clog clears (#5930): the cause on one line, how long | |
| 3 | * the clog took to be noticed and to clear, and the dollars stranded at the | |
| 4 | * peak. Pure: plain data in, markdown out. Posting it is another module's job. | |
| 5 | */ | |
| 6 | ||
| 7 | export interface ClogNoteInput { | |
| 8 | /** Why the clog was declared, one phrase each. */ | |
| 9 | readonly reasons: readonly string[]; | |
| 10 | /** Epoch ms: when the clog began, when it was declared, when it cleared. */ | |
| 11 | readonly startedAt: number; | |
| 12 | readonly detectedAt: number; | |
| 13 | readonly clearedAt: number; | |
| 14 | /** Dollars stranded at the peak; absent or not above zero leaves the line out. */ | |
| 15 | readonly strandedUsd?: number | null; | |
| 16 | } | |
| 17 | ||
| 18 | const MINUTE = 60_000; | |
| 19 | const HOUR_AFTER_MIN = 90; | |
| 20 | ||
| 21 | /** "12 min" up to 90 minutes, then "2.5 h"; a negative or non-finite span reads as 0 min. */ | |
| 22 | export function duration(ms: number): string { | |
| 23 | const min = Number.isFinite(ms) && ms > 0 ? ms / MINUTE : 0; | |
| 24 | if (min <= HOUR_AFTER_MIN) return `${Math.round(min)} min`; | |
| 25 | return `${(Math.round(min / 6) / 10).toString()} h`; | |
| 26 | } | |
| 27 | ||
| 28 | export function clogNote(input: ClogNoteInput): string { | |
| 29 | const why = input.reasons.map((r) => r.trim()).filter(Boolean); | |
| 30 | const lines = [ | |
| 31 | `Clog cleared. Cause: ${why.length ? why.join("; ") : "not recorded"}.`, | |
| 32 | "", | |
| 33 | `- Time to detect: ${duration(input.detectedAt - input.startedAt)}`, | |
| 34 | `- Time to clear: ${duration(input.clearedAt - input.detectedAt)}`, | |
| 35 | ]; | |
| 36 | const usd = input.strandedUsd; | |
| 37 | if (typeof usd === "number" && Number.isFinite(usd) && usd > 0) { | |
| 38 | lines.push(`- Stranded at peak: $${usd.toLocaleString("en-US", { maximumFractionDigits: 2 })}`); | |
| 39 | } | |
| 40 | return lines.join("\n") + "\n"; | |
| 41 | } |
test/flow-clog-note.test.ts added +33
| @@ -0,0 +1,33 @@ | ||
| 1 | /** The clog-cleared note: full, without dollars, minutes and hours, never negative. */ | |
| 2 | import { expect, test } from "bun:test"; | |
| 3 | import { clogNote, duration } from "../src/flow/clog_note.ts"; | |
| 4 | ||
| 5 | const M = 60_000; | |
| 6 | const base = { reasons: ["red main", "no reviewer"], startedAt: 0, detectedAt: 10 * M, clearedAt: 70 * M }; | |
| 7 | ||
| 8 | test("a full note names cause, both times and the dollars", () => { | |
| 9 | const note = clogNote({ ...base, strandedUsd: 1234.5 }); | |
| 10 | expect(note).toBe( | |
| 11 | "Clog cleared. Cause: red main; no reviewer.\n\n- Time to detect: 10 min\n- Time to clear: 60 min\n- Stranded at peak: $1,234.5\n", | |
| 12 | ); | |
| 13 | }); | |
| 14 | ||
| 15 | test("no dollars leaves the line out", () => { | |
| 16 | expect(clogNote(base)).not.toContain("Stranded"); | |
| 17 | expect(clogNote({ ...base, strandedUsd: 0 })).not.toContain("Stranded"); | |
| 18 | }); | |
| 19 | ||
| 20 | test("minutes up to 90, hours past it", () => { | |
| 21 | expect(duration(90 * M)).toBe("90 min"); | |
| 22 | expect(duration(91 * M)).toBe("1.5 h"); | |
| 23 | expect(duration(150 * M)).toBe("2.5 h"); | |
| 24 | }); | |
| 25 | ||
| 26 | test("a negative span reads as zero", () => { | |
| 27 | expect(duration(-5 * M)).toBe("0 min"); | |
| 28 | expect(clogNote({ ...base, detectedAt: -M })).toContain("Time to detect: 0 min"); | |
| 29 | }); | |
| 30 | ||
| 31 | test("no reasons still reads", () => { | |
| 32 | expect(clogNote({ ...base, reasons: [] })).toContain("Cause: not recorded."); | |
| 33 | }); |
new above
offline