Public: anyone can read this; the crew can change it.

week.ts can say how many Next up rows this week's pace reaches, as plain data with no read #12145

Merged opened this

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

Merged

scout/forge:scout/12133-week-ts-can-say-how-many-next-up-rows-th → main 3 commits fork

  • Merged. scout/12133-week-ts-can-say-how-many-next-up-rows-th was merged into main as 2da791d.

Review

Sign in to review this change.
3 files changed +88 −0
scripts/unused-exports-allow.json +2
@@ -87,6 +87,8 @@
8787 "src/flow/kpi_words.ts trafficWords",
8888 "src/flow/reach.ts NO_REACH",
8989 "src/flow/shared_red.ts sharedRedSays",
90 "src/flow/week.ts daysLeftInWeek",
91 "src/flow/week.ts paceCut",
9092 "src/git/coauthors.ts creditsOf",
9193 "src/git/diff-memory.ts diffMemoryCounts",
9294 "src/git/diff-memory.ts forgetDiffs",
src/flow/week.ts +40
@@ -145,3 +145,43 @@ export function weekView(db: Database, repoId: string, rows: readonly NextRow[],
145145 const cut = cutLine(priced, capacity);
146146 return { priced, cut, capacity, sentence: projection(cut, rows.length, capacity) };
147147 }
148
149 const DAY_MS = 86_400_000;
150
151 /**
152 * Days from `now` to `resetsAt` when it is a valid instant, else to the end of the ISO week
153 * (Monday 00:00 UTC); never negative.
154 */
155 export function daysLeftInWeek(now: Date, resetsAt?: Date | string | null): number {
156 const at = resetsAt == null ? null : new Date(resetsAt);
157 let end: number;
158 if (at !== null && Number.isFinite(at.getTime())) end = at.getTime();
159 else {
160 const midnight = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
161 const sinceMonday = (now.getUTCDay() + 6) % 7;
162 end = midnight + (7 - sinceMonday) * DAY_MS;
163 }
164 return Math.max(0, (end - now.getTime()) / DAY_MS);
165 }
166
167 export interface PaceCut {
168 readonly rows: number;
169 readonly reason: string;
170 }
171
172 const num = (n: number): string => String(Math.round(n * 10) / 10);
173 const plural = (n: number, one: string, many: string): string => `${num(n)} ${n === 1 ? one : many}`;
174
175 /** How many Next up rows this week's pace reaches, in plain data: no read, no page. */
176 export function paceCut(p: { landedPerDay: number; daysLeft: number; total: number; landed: number }): PaceCut {
177 const pace = Number.isFinite(p.landedPerDay) ? Math.max(0, p.landedPerDay) : 0;
178 const days = Number.isFinite(p.daysLeft) ? Math.max(0, p.daysLeft) : 0;
179 const total = Math.max(0, Math.floor(p.total) || 0);
180 const rows = Math.min(total, Math.max(0, Math.floor(pace * days)));
181 const guess = p.landed < 3 ? " (a guess: fewer than 3 landed)" : "";
182 const reason =
183 pace === 0
184 ? `by pace: nothing has landed lately, so none reach this week${guess}`
185 : `by pace: ${plural(pace, "issue", "issues")} a day, ${plural(days, "day", "days")} left${guess}`;
186 return { rows, reason };
187 }
test/week-pace.test.ts added +46
@@ -0,0 +1,46 @@
1 /** How far this week's pace reaches, as plain data (#12133). */
2 import { describe, expect, test } from "bun:test";
3 import { daysLeftInWeek, paceCut } from "../src/flow/week.ts";
4
5 const cut = (landedPerDay: number, over: Partial<Parameters<typeof paceCut>[0]> = {}) =>
6 paceCut({ landedPerDay, daysLeft: 3, total: 10, landed: 5, ...over });
7
8 describe("paceCut", () => {
9 test("zero pace reaches zero rows and says so", () => {
10 const c = cut(0);
11 expect(c.rows).toBe(0);
12 expect(c.reason).toContain("nothing has landed");
13 });
14 test("a faster pace moves the line down", () => {
15 expect(cut(1).rows).toBe(3);
16 expect(cut(2).rows).toBe(6);
17 expect(cut(2).reason).toBe("by pace: 2 issues a day, 3 days left");
18 });
19 test("the line never passes total, nor goes below zero", () => {
20 expect(cut(100).rows).toBe(10);
21 expect(cut(2, { total: 0 }).rows).toBe(0);
22 expect(cut(-1).rows).toBe(0);
23 expect(cut(2, { daysLeft: -2 }).rows).toBe(0);
24 });
25 test("fewer than 3 landed says the pace is a guess", () => {
26 expect(cut(2, { landed: 2 }).reason).toContain("guess");
27 expect(cut(2, { landed: 3 }).reason).not.toContain("guess");
28 });
29 });
30
31 describe("daysLeftInWeek", () => {
32 const now = new Date("2026-11-04T12:00:00Z"); // Wednesday
33 test("days to resetsAt when given", () => {
34 expect(daysLeftInWeek(now, "2026-11-06T12:00:00Z")).toBe(2);
35 expect(daysLeftInWeek(now, new Date("2026-11-05T00:00:00Z"))).toBe(0.5);
36 });
37 test("else to the end of the ISO week, Monday 00:00 UTC", () => {
38 expect(daysLeftInWeek(now)).toBe(4.5);
39 expect(daysLeftInWeek(now, null)).toBe(4.5);
40 expect(daysLeftInWeek(new Date("2026-11-08T23:00:00Z"))).toBeCloseTo(1 / 24, 6); // Sunday
41 expect(daysLeftInWeek(new Date("2026-11-02T00:00:00Z"))).toBe(7); // Monday
42 });
43 test("never negative", () => {
44 expect(daysLeftInWeek(now, "2026-11-01T00:00:00Z")).toBe(0);
45 });
46 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs