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

A pure read picks the crew pulls with no push for 7 days, oldest first, with the note that folds each back into its issue #6541

Merged opened this

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

Merged

scout/forge:scout/6521-a-pure-read-picks-the-crew-pulls-with-no → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6521-a-pure-read-picks-the-crew-pulls-with-no was merged into main as a46bcf3.

Review

Sign in to review this change.
2 files changed +85 −0
src/flow/stale_fold.ts added +40
@@ -0,0 +1,40 @@
1 /**
2 * Stale fold (#6521): crew pulls with no push for 7 days, oldest first, each
3 * with the note that folds it back into its issue. A pure read over plain data;
4 * it only proposes, a person closes. Approved pulls are left out. Nothing shows it yet.
5 */
6
7 const DAY_MS = 86_400_000;
8 const STALE_DAYS = 7;
9
10 export interface FoldablePull {
11 readonly number: number;
12 readonly issueNumber: number | null;
13 readonly branch: string;
14 readonly lastPushAt: string;
15 readonly approved: boolean;
16 }
17
18 export interface StaleFold extends FoldablePull {
19 readonly days: number;
20 /** Null when the pull has no issue to fold back into. */
21 readonly note: string | null;
22 }
23
24 export function staleFold(pulls: readonly FoldablePull[], now: Date): StaleFold[] {
25 const out: (StaleFold & { at: number })[] = [];
26 for (const p of pulls) {
27 if (p.approved) continue;
28 const at = Date.parse(p.lastPushAt);
29 if (!Number.isFinite(at)) continue;
30 const days = Math.floor((now.getTime() - at) / DAY_MS);
31 if (days < STALE_DAYS) continue;
32 const note =
33 p.issueNumber === null
34 ? null
35 : `Folded back: pull #${p.number} had no push for ${days} days; its branch \`${p.branch}\` keeps the work.`;
36 out.push({ ...p, days, note, at });
37 }
38 out.sort((a, b) => a.at - b.at || a.number - b.number);
39 return out.map(({ at: _at, ...rest }) => rest);

Nit: dropping at through an unused-name destructure is easy to trip a lint on; map from pairs instead if the linter ever flags _at.

40 }
test/flow-stale-fold.test.ts added +45
@@ -0,0 +1,45 @@
1 /** The stale fold (#6521): pulls with no push for 7 days, oldest first, with the note. */
2 import { describe, expect, test } from "bun:test";
3 import { staleFold, type FoldablePull } from "../src/flow/stale_fold.ts";
4
5 const NOW = new Date("2026-10-26T12:00:00.000Z");
6 const ago = (d: number) => new Date(NOW.getTime() - d * 86_400_000).toISOString();
7 const pull = (number: number, d: number, extra: Partial<FoldablePull> = {}): FoldablePull => ({
8 number,
9 issueNumber: number + 1000,
10 branch: `scout/${number}`,
11 lastPushAt: ago(d),
12 approved: false,
13 ...extra,
14 });
15
16 describe("staleFold", () => {
17 test("exactly 7 days is stale, just under is not", () => {
18 const got = staleFold([pull(1, 7), pull(2, 7 - 1 / 86_400)], NOW);
19 expect(got.map((p) => p.number)).toEqual([1]);
20 });
21
22 test("oldest first, ties by number", () => {
23 const got = staleFold([pull(3, 8), pull(2, 20), pull(1, 8)], NOW);
24 expect(got.map((p) => p.number)).toEqual([2, 1, 3]);
25 });
26
27 test("approved pulls are left out", () => {
28 expect(staleFold([pull(1, 30, { approved: true }), pull(2, 30)], NOW).map((p) => p.number)).toEqual([2]);
29 });
30
31 test("the note's words", () => {
32 const [p] = staleFold([pull(5, 9.5)], NOW);
33 expect(p?.note).toBe("Folded back: pull #5 had no push for 9 days; its branch `scout/5` keeps the work.");
34 });
35
36 test("a pull with no issue is listed with no note", () => {
37 const [p] = staleFold([pull(6, 10, { issueNumber: null })], NOW);
38 expect(p?.issueNumber).toBeNull();
39 expect(p?.note).toBeNull();
40 });
41
42 test("an unreadable date is skipped", () => {
43 expect(staleFold([pull(1, 10, { lastPushAt: "nope" })], NOW)).toEqual([]);
44 });
45 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs