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

Merged opened this

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

Merged

scout/forge:scout/6520-a-pure-step-remembers-the-governor-s-las → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6520-a-pure-step-remembers-the-governor-s-las was merged into main as 71b753e.

Review

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

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.

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

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs