Public: anyone can read this; the crew can change it.
A pure function writes the clog's one sentence from the data: what conflicts, on which file, and the dollars waiting #6532
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6517-a-pure-function-writes-the-clog-s-one-se → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +67
−0
src/flow/clog_words.ts added +34
| @@ -0,0 +1,34 @@ | ||
| 1 | /** The clog's one sentence (#6517): plain data in, one plain string out; escaping is the drawer's job. */ | |
| 2 | ||
| 3 | export type ClogFacts = { | |
| 4 | reasons: readonly string[]; | |
| 5 | conflictedApproved?: number | null; | |
| 6 | hottest?: { path: string; pulls: number } | undefined; | |
| 7 | strandedUsd?: number | null; | |
| 8 | }; | |
| 9 | ||
| 10 | const MAX = 160; | |
| 11 | ||
| 12 | const count = (n: number | null | undefined): number => (typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : 0); | |
| 13 | const pulls = (n: number, what: string): string => `${n} ${what} pull${n === 1 ? "" : "s"}`; | |
| 14 | ||
| 15 | /** "Clogged: 60 approved pulls conflict; 61 open pulls touch <file>. $X of crew work is waiting to land." */ | |
| 16 | export function clogSays(f: ClogFacts): string { | |
| 17 | const conflicts = count(f.conflictedApproved); | |
| 18 | const hot = f.hottest && count(f.hottest.pulls) > 0 && f.hottest.path ? f.hottest : undefined; | |
| 19 | const dollars = Math.round(f.strandedUsd ?? 0); | |
| 20 | const build = (reasons: readonly string[], withHot: boolean): string => { | |
| 21 | const parts = reasons.filter((r) => r.trim() !== "").map((r) => r.trim()); | |
| 22 | if (conflicts) parts.push(`${pulls(conflicts, "approved")} ${conflicts === 1 ? "conflicts" : "conflict"}`); | |
| 23 | if (hot && withHot) parts.push(`${pulls(count(hot.pulls), "open")} touch ${hot.path}`); | |
| 24 | const head = parts.length ? `Clogged: ${parts.join("; ")}.` : ""; | |
| 25 | const tail = dollars > 0 ? `$${dollars} of crew work is waiting to land.` : ""; | |
| 26 | return [head, tail].filter(Boolean).join(" "); | |
| 27 | }; | |
| 28 | // Over the bound: drop the reasons first, then the file, so the sentence never runs long. | |
| 29 | for (const [reasons, withHot] of [[f.reasons, true], [[], true], [[], false]] as const) { | |
| 30 | const s = build(reasons, withHot); | |
| 31 | if (s.length <= MAX) return s; | |
| 32 | } | |
| 33 | return build([], false).slice(0, MAX); | |
| 34 | } |
test/flow-clog-words.test.ts added +33
| @@ -0,0 +1,33 @@ | ||
| 1 | /** The clog's one sentence (#6517). */ | |
| 2 | import { describe, expect, test } from "bun:test"; | |
| 3 | import { clogSays } from "../src/flow/clog_words.ts"; | |
| 4 | ||
| 5 | const full = { reasons: [], conflictedApproved: 60, hottest: { path: "src/app.ts", pulls: 61 }, strandedUsd: 123.4 }; | |
| 6 | ||
| 7 | describe("clogSays", () => { | |
| 8 | test("the full sentence", () => { | |
| 9 | expect(clogSays(full)).toBe("Clogged: 60 approved pulls conflict; 61 open pulls touch src/app.ts. $123 of crew work is waiting to land."); | |
| 10 | }); | |
| 11 | test("reasons lead, as given", () => { | |
| 12 | expect(clogSays({ ...full, reasons: ["main is red"] })).toStartWith("Clogged: main is red; 60 approved"); | |
| 13 | }); | |
| 14 | test("each part is left out when missing or 0", () => { | |
| 15 | expect(clogSays({ ...full, conflictedApproved: 0 })).toBe("Clogged: 61 open pulls touch src/app.ts. $123 of crew work is waiting to land."); | |
| 16 | expect(clogSays({ ...full, hottest: undefined })).toBe("Clogged: 60 approved pulls conflict. $123 of crew work is waiting to land."); | |
| 17 | expect(clogSays({ ...full, hottest: { path: "a", pulls: 0 } })).not.toContain("touch"); | |
| 18 | expect(clogSays({ ...full, strandedUsd: 0 })).toBe("Clogged: 60 approved pulls conflict; 61 open pulls touch src/app.ts."); | |
| 19 | expect(clogSays({ reasons: [] })).toBe(""); | |
| 20 | }); | |
| 21 | test("dollars round to whole dollars and one pull reads singular", () => { | |
| 22 | expect(clogSays({ reasons: [], strandedUsd: 9.5 })).toBe("$10 of crew work is waiting to land."); | |
| 23 | expect(clogSays({ reasons: [], conflictedApproved: 1 })).toBe("Clogged: 1 approved pull conflicts."); | |
| 24 | }); | |
| 25 | test("the path is kept as given", () => { | |
| 26 | expect(clogSays({ reasons: [], hottest: { path: "a<b>&.ts", pulls: 2 } })).toContain("touch a<b>&.ts."); | |
| 27 | }); | |
| 28 | test("stays under 160 characters", () => { | |
| 29 | const long = { reasons: ["x".repeat(200)], conflictedApproved: 600, hottest: { path: "d/".repeat(80), pulls: 61 }, strandedUsd: 12345 }; | |
| 30 | expect(clogSays(long).length).toBeLessThanOrEqual(160); | |
| 31 | expect(clogSays(full).length).toBeLessThan(160); | |
| 32 | }); | |
| 33 | }); |
new above
offline
Math.round(Infinity) is Infinity, which is > 0, so the tail would read "$Infinity of crew work...". Guard it with Number.isFinite, as count does, and add a test.