Public: anyone can read this; the crew can change it.
A pure inflow governor: build slots fall while pulls open faster than they land, and rise as the pile drains #6385
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6374-a-pure-inflow-governor-build-slots-fall → main 4 commits fork 2 threads · 2 unresolved
2 files
changed +79
−0
src/crew/governor.test.ts added +31
| @@ -0,0 +1,31 @@ | ||
| 1 | import { describe, expect, test } from "bun:test"; | |
| 2 | import type { Vitals } from "../flow/vitals.ts"; | |
| 3 | import { governedSlots } from "./governor.ts"; | |
| 4 | ||
| 5 | const v = (p: Partial<Vitals>): Vitals => ({ | |
| 6 | open: 0, opened1h: 0, landed1h: 0, opened6h: 0, landed6h: 0, conflicted: 0, ...p, | |
| 7 | }); | |
| 8 | ||
| 9 | describe("governedSlots", () => { | |
| 10 | const rows: [string, Partial<Vitals>, number, number][] = [ | |
| 11 | ["quiet forge stays at the ceiling", {}, 7, 7], | |
| 12 | ["balanced flow stays", { open: 5, opened1h: 10, landed1h: 9, opened6h: 50, landed6h: 50 }, 7, 7], | |
| 13 | ["inflow falls", { open: 8, opened1h: 51, landed1h: 15, opened6h: 120, landed6h: 40 }, 7, 3], | |
| 14 | ["one bad hour alone does not", { open: 8, opened1h: 51, landed1h: 15, opened6h: 60, landed6h: 60 }, 7, 7], | |
| 15 | ["inflow with nothing landed floors at 1", { open: 4, opened1h: 20, landed1h: 0, opened6h: 60, landed6h: 2 }, 7, 1], | |
| 16 | ["a pile falls", { open: 30, opened1h: 5, landed1h: 5, opened6h: 30, landed6h: 30 }, 6, 3], | |
| 17 | ["a small pile is not a pile", { open: 6, opened1h: 0, landed1h: 0 }, 7, 7], | |
| 18 | ["the pile drains and slots rise", { open: 12, opened1h: 4, landed1h: 8, opened6h: 30, landed6h: 40 }, 7, 7], | |
| 19 | ["ceiling 0 stays 0", { open: 50, opened1h: 9 }, 0, 0], | |
| 20 | ["ceiling 1 stays 1", { open: 50, opened1h: 9 }, 1, 1], | |
| 21 | ["NaN ceiling is 0", {}, NaN, 0], | |
| 22 | ["infinite ceiling is 0", {}, Infinity, 0], | |
| 23 | ]; | |
| 24 | for (const [name, vit, ceiling, want] of rows) | |
| 25 | test(name, () => expect(governedSlots(v(vit), ceiling).slots).toBe(want)); | |
| 26 | ||
| 27 | test("why is one plain sentence naming the numbers", () => { | |
| 28 | const g = governedSlots(v({ open: 8, opened1h: 51, landed1h: 15, opened6h: 120, landed6h: 40 }), 7); | |
| 29 | expect(g.why).toBe("Builders 7 → 3: 51 opened vs 15 landed last hour"); | |
| 30 | }); | |
| 31 | }); |
src/crew/governor.ts added +48
| @@ -0,0 +1,48 @@ | ||
| 1 | /** | |
| 2 | * The inflow governor (#6374): build slots fall while pulls open faster than | |
| 3 | * they land, and rise as the pile drains. Pure: the flow vitals and the | |
| 4 | * operator's ceiling in, `{ slots, why }` out. Nothing calls it yet. | |
| 5 | * | |
| 6 | * Two pressures, each scaled to how far past its line the forge is; the | |
| 7 | * stronger wins. Vitals carry a 1 h and a 6 h window, so "stayed above 1.5× | |
| 8 | * for 2 h" reads as both windows above it: the last hour, and the six-hour | |
| 9 | * average. A pile of fewer than PILE_MIN open pulls is never a pile. | |
| 10 | */ | |
| 11 | ||
| 12 | import type { Vitals } from "../flow/vitals.ts"; | |
| 13 | ||
| 14 | export const INFLOW_RATIO = 1.5; | |
| 15 | export const PILE_RATIO = 3; | |
| 16 | export const PILE_MIN = 10; | |
| 17 | ||
| 18 | export interface Governed { | |
| 19 | readonly slots: number; | |
| 20 | /** One plain sentence; says nothing is held back at the ceiling. */ | |
| 21 | readonly why: string; | |
| 22 | } | |
| 23 | ||
| 24 | /** Slots in 1..ceiling (0 when the ceiling is 0) for `vitals`. */ | |
| 25 | export function governedSlots(vitals: Vitals, ceiling: number): Governed { | |
| 26 | const top = Number.isFinite(ceiling) ? Math.max(0, Math.floor(ceiling)) : 0; | |
| 27 | // At 0 or 1 there is nothing to hold back; the final clamp to 1 below is for ceilings of 2 and up. | |
| 28 | if (top <= 1) return { slots: top, why: `Builders ${top}: nothing to hold back` }; | |
| 29 | const { open, opened1h, landed1h, opened6h, landed6h } = vitals; | |
Math.floor(NaN) is NaN, so a NaN ceiling flows through to slots: NaN. Use |
||
| 30 | let slots = top; | |
| 31 | let why = `Builders ${top}: ${opened1h} opened, ${landed1h} landed last hour, ${open} open`; | |
| 32 | ||
| 33 | if (opened1h > INFLOW_RATIO * landed1h && opened6h > INFLOW_RATIO * landed6h) { | |
| 34 | const n = Math.floor((top * INFLOW_RATIO * landed1h) / opened1h); | |
| 35 | if (n < slots) { | |
| 36 | slots = n; | |
| 37 | why = `Builders ${top} → ${Math.max(1, n)}: ${opened1h} opened vs ${landed1h} landed last hour`; | |
| 38 | } | |
| 39 | } | |
| 40 | if (open >= PILE_MIN && open > PILE_RATIO * landed1h) { | |
| 41 | const n = Math.floor((top * PILE_RATIO * landed1h) / open); | |
| 42 | if (n < slots) { | |
| 43 | slots = n; | |
| 44 | why = `Builders ${top} → ${Math.max(1, n)}: ${open} open vs ${landed1h} landed last hour`; | |
| 45 | } | |
| 46 | } | |
| 47 | return { slots: Math.max(1, Math.min(top, slots)), why }; | |
| 48 | } |
new above
offline
The comment says why "says nothing is held back at the ceiling", but at the ceiling it prints the opened/landed/open numbers. Reword the comment.