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

Merged opened this

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

Merged

mason/forge:mason/5622-the-waves-and-decider-sentences-live-in → main 1 commit fork

  • Merged. mason/5622-the-waves-and-decider-sentences-live-in was merged into main as 18eb8f1.

Review

Sign in to review this change.
7 files changed +75 −13
src/core/decisions.ts −5
@@ -432,11 +432,6 @@ export function deciderCounts(
432432 return out;
433433 }
434434
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
440435 /** What an empty pile says, for a person. */
441436 export const PILE_EMPTY: Record<DeciderPile, string> = {
442437 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 {
1212 DECIDER_PILES,
1313 PILE_EMPTY,
1414 deciderCounts,
15 deciderWords,
1615 listDeciderPile,
1716 type DeciderPile,
1817 } from "../../core/decisions.ts";
18 import { deciderSentence } from "../../story/words.ts";
1919 import { getIssuesByIds } from "../../core/issues.ts";
2020 import { getReposByIds } from "../../core/repos.ts";
2121 import type { User } from "../../core/users.ts";
@@ -40,10 +40,10 @@ export interface DeciderRow extends ListRow {
4040 const owned = (app: App, viewer: User | undefined): string[] =>
4141 viewer === undefined ? [] : returnsRepos(app, viewer).map((r) => r.id);
4242
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. */
4444 export function deciderLine(app: App, viewer: User | undefined, now = new Date()): string | null {
4545 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("");
4747 }
4848
4949 /** The ledger rows behind one pile's count today, newest first. */
src/tui/screens/waves.ts +2 −1
@@ -8,6 +8,7 @@
88 import { canRead } from "../../core/repos.ts";
99 import { VERSION } from "../../story/wave.ts";
1010 import { readWave, readWaves, type WaveEntry } from "../../story/waves.ts";
11 import { NO_WAVES } from "../../story/words.ts";
1112 import type { RepoSource } from "../../view/source.ts";
1213 import type { Ctx, Screen } from "../app/app.ts";
1314 import { binding } from "../app/keys.ts";
@@ -137,7 +138,7 @@ export function wavesScreen(f: Session, ctx: Ctx, src: RepoSource): Screen {
137138 ? ""
138139 : repo === undefined
139140 ? "Waves are only read on this forge."
140 : "This repository has no waves/ directory.",
141 : NO_WAVES,
141142 });
142143 return;
143144 }
src/web/pages/board.ts +3 −3
@@ -32,6 +32,7 @@ import {
3232 type Decision,
3333 type DecisionKind,
3434 } from "../../core/decisions.ts";
35 import { deciderSentence } from "../../story/words.ts";
3536 import { efficiencyForRepos } from "../../core/efficiency.ts";
3637 import { reviewStats, type ReviewStats } from "../../core/review-stats.ts";
3738 import { standings, WINDOW_MS } from "../../core/standing.ts";
@@ -924,15 +925,14 @@ function boardHref(ctx: Ctx, pile: DeciderPile | null): string {
924925 return s === "" ? "/board" : `/board?${s}`;
925926 }
926927
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. */
928929 function deciderLine(ctx: Ctx, scope: BoardScope, since: string): Html | null {
929930 const n = deciderCounts(ctx.app.db, [...scope.ownerFacts], since);
930931 if (n.closed + n.deferred + n.asked === 0) return null;
931932 const link = (pile: DeciderPile) =>
932933 html`<a href="${boardHref(ctx, pile)}#decided">${n[pile]}</a>`;
933934 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)}
936936 </p>`;
937937 }
938938
src/web/pages/waves.ts +2 −1
@@ -14,6 +14,7 @@ import { repoDir } from "../../git/repos.ts";
1414 import type { Ctx } from "../../http/context.ts";
1515 import { VERSION } from "../../story/wave.ts";
1616 import { readWave, readWaves } from "../../story/waves.ts";
17 import { NO_WAVES } from "../../story/words.ts";
1718 import { loadRepo, notFoundPage, repoBase, scopeHeader } from "../chrome.ts";
1819 import { escapeHtml, html } from "../html.ts";
1920 import { page } from "../layout.ts";
@@ -53,7 +54,7 @@ export async function wavesPage(ctx: Ctx): Promise<Response> {
5354 const body = html`${scopeHeader(ctx, scope, "code", { compact: true })}
5455 <div class="mt">
5556 ${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 })
5758 : listCard(rows, { head: html`<h2>Waves</h2>` })}
5859 </div>`;
5960
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 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs