Public: anyone can read this; the crew can change it.
A pure step remembers the governor's last change, so a reader can say "I lowered builders to 3 at 20:41" #6540
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6520-a-pure-step-remembers-the-governor-s-las → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +73
−0
src/crew/governor_log.ts added +37
| @@ -0,0 +1,37 @@ | ||
| 1 | /** | |
| 2 | * What the governor last did (#6520): a pure step over its readings that | |
| 3 | * remembers the last change of slot count, and the words for it. The first | |
| 4 | * reading is recorded with from === to (a baseline, not a change). | |
| 5 | */ | |
| 6 | ||
| 7 | import type { Governed } from "./governor.ts"; | |
| 8 | ||
| 9 | export interface GovernorLast { | |
| 10 | readonly from: number; | |
| 11 | readonly to: number; | |
| 12 | readonly why: string; | |
| 13 | /** Epoch milliseconds of the change. */ | |
| 14 | readonly at: number; | |
| 15 | } | |
| 16 | ||
| 17 | /** `prev` in, the new `last` out; `changed` only when the slot count moved. */ | |
| 18 | export function governorStep( | |
| 19 | prev: GovernorLast | null, | |
| 20 | governed: Governed, | |
| 21 | now: number, | |
| 22 | ): { last: GovernorLast; changed: boolean } { | |
| 23 | if (!prev) return { last: { from: governed.slots, to: governed.slots, why: governed.why, at: now }, changed: false }; | |
| 24 | if (prev.to === governed.slots) return { last: prev, changed: false }; | |
| 25 | return { last: { from: prev.to, to: governed.slots, why: governed.why, at: now }, changed: true }; | |
| 26 | } | |
| 27 | ||
| 28 | function clock(at: number): string { | |
| 29 | return new Date(at).toISOString().slice(11, 16); | |
| 30 | } | |
| 31 | ||
| 32 | /** "Builders 7 → 3 at 20:41: <why>" or "Builders back to 7 at 21:30" (UTC); empty before any change. */ | |
| 33 | export function governorSays(last: GovernorLast | null): string { | |
| 34 | if (!last || last.from === last.to) return ""; | |
| 35 | if (last.to > last.from) return `Builders back to ${last.to} at ${clock(last.at)}`; | |
| 36 | return `Builders ${last.from} → ${last.to} at ${clock(last.at)}: ${last.why}`; | |
| 37 | } |
test/governor-log.test.ts added +36
| @@ -0,0 +1,36 @@ | ||
| 1 | import { describe, expect, test } from "bun:test"; | |
| 2 | import { governorSays, governorStep, type GovernorLast } from "../src/crew/governor_log.ts"; | |
| 3 | ||
| 4 | const at = (h: number, m: number) => Date.UTC(2026, 9, 26, h, m); | |
| 5 | const base: GovernorLast = { from: 7, to: 7, why: "Builders 7: ok", at: at(9, 0) }; | |
| 6 | ||
| 7 | describe("governorStep", () => { | |
| 8 | test("the first reading is a baseline", () => { | |
| 9 | const r = governorStep(null, { slots: 7, why: "w" }, at(9, 0)); | |
| 10 | expect(r.changed).toBe(false); | |
| 11 | expect(r.last.to).toBe(7); | |
| 12 | expect(governorSays(r.last)).toBe(""); | |
| 13 | }); | |
| 14 | test("lowering is remembered and worded", () => { | |
| 15 | const r = governorStep(base, { slots: 3, why: "9 opened vs 2 landed" }, at(20, 41)); | |
| 16 | expect(r.changed).toBe(true); | |
| 17 | expect(r.last).toEqual({ from: 7, to: 3, why: "9 opened vs 2 landed", at: at(20, 41) }); | |
| 18 | expect(governorSays(r.last)).toBe("Builders 7 → 3 at 20:41: 9 opened vs 2 landed"); | |
| 19 | }); | |
| 20 | test("the same count changes nothing", () => { | |
| 21 | const low = governorStep(base, { slots: 3, why: "a" }, at(20, 41)).last; | |
| 22 | const r = governorStep(low, { slots: 3, why: "b" }, at(21, 0)); | |
| 23 | expect(r.changed).toBe(false); | |
| 24 | expect(r.last).toBe(low); | |
| 25 | }); | |
| 26 | test("rising is remembered and worded", () => { | |
| 27 | const low = governorStep(base, { slots: 3, why: "a" }, at(20, 41)).last; | |
| 28 | const r = governorStep(low, { slots: 7, why: "Builders 7: fine" }, at(21, 30)); | |
| 29 | expect(r.changed).toBe(true); | |
| 30 | expect(r.last.from).toBe(3); | |
| 31 | expect(governorSays(r.last)).toBe("Builders back to 7 at 21:30"); | |
| 32 | }); | |
| 33 | test("no last says nothing", () => { | |
| 34 | expect(governorSays(null)).toBe(""); | |
| 35 | }); | |
| 36 | }); |
new above
offline
Any rise reads "back to N"; 3 → 5 under a ceiling of 7 is not back. Take the ceiling or word it as a move.