Public: anyone can read this; the crew can change it.
The waves and decider sentences live in one module, read by the web pages and the terminal #5660
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5622-the-waves-and-decider-sentences-live-in → main 1 commit fork
7 files
changed +75
−13
src/core/decisions.ts −5
| @@ -432,11 +432,6 @@ export function deciderCounts( | ||
| 432 | 432 | return out; |
| 433 | 433 | } |
| 434 | 434 | |
| 435 | /** The sentence for the counts, in plain words: "closed 4, deferred 2, asked you about 1". */ | |
| 436 | export function deciderWords(n: Record<DeciderPile, number>): string { | |
| 437 | return `closed ${n.closed}, deferred ${n.deferred}, asked you about ${n.asked}`; | |
| 438 | } | |
| 439 | ||
| 440 | 435 | /** What an empty pile says, for a person. */ |
| 441 | 436 | export const PILE_EMPTY: Record<DeciderPile, string> = { |
| 442 | 437 | closed: "The decider has closed nothing today.", |
src/story/words.ts added +28
| @@ -0,0 +1,28 @@ | ||
| 1 | /** | |
| 2 | * The sentences said in more than one place — the web page and the terminal | |
| 3 | * screen — typed out once, here, so they read the same wherever they are said. | |
| 4 | * Plain strings in, plain strings (or parts) out; each door does its own | |
| 5 | * escaping and layout. | |
| 6 | */ | |
| 7 | ||
| 8 | import type { DeciderPile } from "../core/decisions.ts"; | |
| 9 | ||
| 10 | /** A repository's waves, empty because there is no `waves/` to read. */ | |
| 11 | export const NO_WAVES = "This repository has no waves/ directory."; | |
| 12 | ||
| 13 | /** | |
| 14 | * "The decider closed 4, deferred 2, asked you about 1." as its parts, each | |
| 15 | * count as `count` renders it: the terminal joins the numbers, the board | |
| 16 | * links each one to its ledger rows. | |
| 17 | */ | |
| 18 | export function deciderSentence<T>(count: (pile: DeciderPile) => T): (string | T)[] { | |
| 19 | return [ | |
| 20 | "The decider closed ", | |
| 21 | count("closed"), | |
| 22 | ", deferred ", | |
| 23 | count("deferred"), | |
| 24 | ", asked you about ", | |
| 25 | count("asked"), | |
| 26 | ".", | |
| 27 | ]; | |
| 28 | } |
src/tui/screens/decider.ts +3 −3
| @@ -12,10 +12,10 @@ import { | ||
| 12 | 12 | DECIDER_PILES, |
| 13 | 13 | PILE_EMPTY, |
| 14 | 14 | deciderCounts, |
| 15 | deciderWords, | |
| 16 | 15 | listDeciderPile, |
| 17 | 16 | type DeciderPile, |
| 18 | 17 | } from "../../core/decisions.ts"; |
| 18 | import { deciderSentence } from "../../story/words.ts"; | |
| 19 | 19 | import { getIssuesByIds } from "../../core/issues.ts"; |
| 20 | 20 | import { getReposByIds } from "../../core/repos.ts"; |
| 21 | 21 | import type { User } from "../../core/users.ts"; |
| @@ -40,10 +40,10 @@ export interface DeciderRow extends ListRow { | ||
| 40 | 40 | const owned = (app: App, viewer: User | undefined): string[] => |
| 41 | 41 | viewer === undefined ? [] : returnsRepos(app, viewer).map((r) => r.id); |
| 42 | 42 | |
| 43 | /** "The decider closed 4, deferred 2, asked you about 1"; null on a quiet day. */ | |
| 43 | /** `deciderSentence` with today's counts; null on a quiet day. */ | |
| 44 | 44 | export function deciderLine(app: App, viewer: User | undefined, now = new Date()): string | null { |
| 45 | 45 | const n = deciderCounts(app.db, owned(app, viewer), startOfTodayUtc(now).toISOString()); |
| 46 | return n.closed + n.deferred + n.asked === 0 ? null : `The decider ${deciderWords(n)}.`; | |
| 46 | return n.closed + n.deferred + n.asked === 0 ? null : deciderSentence((pile) => n[pile]).join(""); | |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | 49 | /** The ledger rows behind one pile's count today, newest first. */ |
src/tui/screens/waves.ts +2 −1
| @@ -8,6 +8,7 @@ | ||
| 8 | 8 | import { canRead } from "../../core/repos.ts"; |
| 9 | 9 | import { VERSION } from "../../story/wave.ts"; |
| 10 | 10 | import { readWave, readWaves, type WaveEntry } from "../../story/waves.ts"; |
| 11 | import { NO_WAVES } from "../../story/words.ts"; | |
| 11 | 12 | import type { RepoSource } from "../../view/source.ts"; |
| 12 | 13 | import type { Ctx, Screen } from "../app/app.ts"; |
| 13 | 14 | import { binding } from "../app/keys.ts"; |
| @@ -137,7 +138,7 @@ export function wavesScreen(f: Session, ctx: Ctx, src: RepoSource): Screen { | ||
| 137 | 138 | ? "" |
| 138 | 139 | : repo === undefined |
| 139 | 140 | ? "Waves are only read on this forge." |
| 140 | : "This repository has no waves/ directory.", | |
| 141 | : NO_WAVES, | |
| 141 | 142 | }); |
| 142 | 143 | return; |
| 143 | 144 | } |
src/web/pages/board.ts +3 −3
| @@ -32,6 +32,7 @@ import { | ||
| 32 | 32 | type Decision, |
| 33 | 33 | type DecisionKind, |
| 34 | 34 | } from "../../core/decisions.ts"; |
| 35 | import { deciderSentence } from "../../story/words.ts"; | |
| 35 | 36 | import { efficiencyForRepos } from "../../core/efficiency.ts"; |
| 36 | 37 | import { reviewStats, type ReviewStats } from "../../core/review-stats.ts"; |
| 37 | 38 | import { standings, WINDOW_MS } from "../../core/standing.ts"; |
| @@ -924,15 +925,14 @@ function boardHref(ctx: Ctx, pile: DeciderPile | null): string { | ||
| 924 | 925 | return s === "" ? "/board" : `/board?${s}`; |
| 925 | 926 | } |
| 926 | 927 | |
| 927 | /** "The decider closed 4, deferred 2, asked you about 1", each count a link to its ledger rows; null on a quiet day. */ | |
| 928 | /** `deciderSentence` with today's counts, each count a link to its ledger rows; null on a quiet day. */ | |
| 928 | 929 | function deciderLine(ctx: Ctx, scope: BoardScope, since: string): Html | null { |
| 929 | 930 | const n = deciderCounts(ctx.app.db, [...scope.ownerFacts], since); |
| 930 | 931 | if (n.closed + n.deferred + n.asked === 0) return null; |
| 931 | 932 | const link = (pile: DeciderPile) => |
| 932 | 933 | html`<a href="${boardHref(ctx, pile)}#decided">${n[pile]}</a>`; |
| 933 | 934 | return html`<p class="muted small" id="decider-day"> |
| 934 | The decider closed ${link("closed")}, deferred ${link("deferred")}, asked | |
| 935 | you about ${link("asked")}. | |
| 935 | ${deciderSentence(link)} | |
| 936 | 936 | </p>`; |
| 937 | 937 | } |
| 938 | 938 |
src/web/pages/waves.ts +2 −1
| @@ -14,6 +14,7 @@ import { repoDir } from "../../git/repos.ts"; | ||
| 14 | 14 | import type { Ctx } from "../../http/context.ts"; |
| 15 | 15 | import { VERSION } from "../../story/wave.ts"; |
| 16 | 16 | import { readWave, readWaves } from "../../story/waves.ts"; |
| 17 | import { NO_WAVES } from "../../story/words.ts"; | |
| 17 | 18 | import { loadRepo, notFoundPage, repoBase, scopeHeader } from "../chrome.ts"; |
| 18 | 19 | import { escapeHtml, html } from "../html.ts"; |
| 19 | 20 | import { page } from "../layout.ts"; |
| @@ -53,7 +54,7 @@ export async function wavesPage(ctx: Ctx): Promise<Response> { | ||
| 53 | 54 | const body = html`${scopeHeader(ctx, scope, "code", { compact: true })} |
| 54 | 55 | <div class="mt"> |
| 55 | 56 | ${entries.length === 0 |
| 56 | ? empty({ title: "No waves yet", note: "This repository has no waves/ directory." }) | |
| 57 | ? empty({ title: "No waves yet", note: NO_WAVES }) | |
| 57 | 58 | : listCard(rows, { head: html`<h2>Waves</h2>` })} |
| 58 | 59 | </div>`; |
| 59 | 60 |
test/story-words.test.ts added +37
| @@ -0,0 +1,37 @@ | ||
| 1 | /** | |
| 2 | * The waves and decider sentences are typed once, in src/story/words.ts | |
| 3 | * (#5622): the pages and screens that say them import them, and a copy typed | |
| 4 | * back into any of them fails here. | |
| 5 | */ | |
| 6 | ||
| 7 | import { expect, test } from "bun:test"; | |
| 8 | import { readFileSync } from "node:fs"; | |
| 9 | import { join } from "node:path"; | |
| 10 | import { NO_WAVES, deciderSentence } from "../src/story/words.ts"; | |
| 11 | ||
| 12 | const read = (file: string) => readFileSync(join(import.meta.dir, "..", file), "utf8"); | |
| 13 | ||
| 14 | const SAYERS = [ | |
| 15 | "src/web/pages/waves.ts", | |
| 16 | "src/tui/screens/waves.ts", | |
| 17 | "src/web/pages/board.ts", | |
| 18 | "src/tui/screens/decider.ts", | |
| 19 | ]; | |
| 20 | ||
| 21 | test("the sentences read as they always have", () => { | |
| 22 | expect(NO_WAVES).toBe("This repository has no waves/ directory."); | |
| 23 | const n = { closed: 4, deferred: 2, asked: 1 }; | |
| 24 | expect(deciderSentence((p) => n[p]).join("")).toBe( | |
| 25 | "The decider closed 4, deferred 2, asked you about 1.", | |
| 26 | ); | |
| 27 | }); | |
| 28 | ||
| 29 | for (const file of SAYERS) { | |
| 30 | test(`${file} says them from words.ts, not as its own copy`, () => { | |
| 31 | const text = read(file); | |
| 32 | expect(text).toContain("story/words.ts"); | |
| 33 | expect(text).not.toContain("no waves/ directory"); | |
| 34 | expect(text).not.toContain("The decider closed"); | |
| 35 | expect(text).not.toMatch(/deferred [^,]*, asked\s+you\s+about/); | |
| 36 | }); | |
| 37 | } |
new above
offline