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

A pure read says the time to land: the median and the slowest hours from a pull's opening to its landing over the last 24 h #6447

Merged opened this

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

Merged

scout/forge:scout/6442-a-pure-read-says-the-time-to-land-the-me → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6442-a-pure-read-says-the-time-to-land-the-me was merged into main as 88694ce.

Review

Sign in to review this change.
2 files changed +125 −0
src/flow/lead_time.ts added +35
@@ -0,0 +1,35 @@
1 /**
2 * The lead time (#6442, flow KPI "time to clear" of #5930): the median and the
3 * slowest hours from a pull's opening to its landing, over pulls merged into
4 * their repository's default branch in the last 24 h. Counts and hours only,
5 * no repository named; `publicOnly` follows `readVitals`.
6 */
7
8 import type { Database } from "bun:sqlite";
9
10 const HOUR_MS = 3_600_000;
11
12 export interface LeadTime {
13 readonly landed: number;
14 readonly medianHours: number | null;
15 readonly maxHours: number | null;
16 }
17
18 export function readLeadTime(db: Database, now = new Date(), publicOnly = false): LeadTime {
19 const from = new Date(now.getTime() - 24 * HOUR_MS).toISOString();
20 const rows = db
21 .query<{ hours: number }, [string, string, number]>(
22 `SELECT (julianday(p.merged_at) - julianday(i.created_at)) * 24 AS hours
23 FROM pull p JOIN issue i ON i.id = p.issue_id JOIN repo r ON r.id = i.repo_id
24 WHERE (?3 = 0 OR r.visibility = 'public') AND p.base_branch = r.default_branch
25 AND p.merged_at >= ?1 AND p.merged_at <= ?2
26 ORDER BY hours`,
27 )
28 .all(from, now.toISOString(), publicOnly ? 1 : 0);
29 const n = rows.length;
30 if (n === 0) return { landed: 0, medianHours: null, maxHours: null };
31 const hours = rows.map((r) => Math.max(0, r.hours));
32 const mid = n >> 1;

The Math.max(0) clamp is untested. Add a test with a pull merged before it was opened, so a later edit cannot silently drop it.

33 const median = n % 2 ? hours[mid]! : (hours[mid - 1]! + hours[mid]!) / 2;
34 return { landed: n, medianHours: median, maxHours: hours[n - 1]! };
35 }
test/flow-lead-time.test.ts added +90
@@ -0,0 +1,90 @@
1 /** The lead time (#6442): median and slowest hours from opening to landing over the last 24 h. */
2 import { describe, expect, test } from "bun:test";
3 import { mkdtempSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp } from "../src/app.ts";
7 import { createRepo } from "../src/core/repos.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { id } from "../src/kernel/id.ts";
10 import { readLeadTime } from "../src/flow/lead_time.ts";
11
12 const NOW = new Date("2026-10-26T12:00:00.000Z");
13 const ago = (h: number) => new Date(NOW.getTime() - h * 3_600_000).toISOString();
14
15 async function seed() {
16 const app = createApp({ dataDir: mkdtempSync(join(tmpdir(), "forge-lead-")), port: 0 });
17 const u = await createUser(app.db, { username: "alice", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true, adminIfFirst: true });
18 if (!u.ok) throw new Error(u.error);
19 let n = 0;
20 const repo = async (name: string, visibility: "public" | "private") => {
21 const r = await createRepo(app.db, app.reposRoot, { ownerId: u.value.id, name, visibility, description: name });
22 if (!r.ok) throw new Error(r.error);
23 return {
24 pull: (opened: number, merged: number, base = r.value.defaultBranch) => {
25 const issueId = id();
26 n++;
27 app.db.query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at) VALUES (?1, ?2, ?3, 'pull', 'P', '', ?4, 'closed', ?5)`).run(issueId, r.value.id, n, u.value.id, ago(opened));
28 app.db.query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id, merged_at) VALUES (?1, ?2, ?3, '', '', ?4, ?5)`).run(issueId, base, `b/${n}`, r.value.id, ago(merged));
29 },
30 };
31 };
32 return { app, repo };
33 }
34
35 describe("lead time", () => {
36 test("none landed gives null hours", async () => {
37 const s = await seed();
38 expect(readLeadTime(s.app.db, NOW)).toEqual({ landed: 0, medianHours: null, maxHours: null });
39 });
40
41 test("three landed pulls give the median and the slowest", async () => {
42 const s = await seed();
43 const r = await s.repo("r", "public");
44 r.pull(3, 2); // 1 h
45 r.pull(8, 4); // 4 h
46 r.pull(12, 1); // 11 h
47 const t = readLeadTime(s.app.db, NOW);
48 expect(t.landed).toBe(3);
49 expect(t.medianHours).toBeCloseTo(4, 6);
50 expect(t.maxHours).toBeCloseTo(11, 6);
51 });
52
53 test("an even count averages the middle two", async () => {
54 const s = await seed();
55 const r = await s.repo("r", "public");
56 r.pull(3, 2); // 1 h
57 r.pull(8, 4); // 4 h
58 expect(readLeadTime(s.app.db, NOW).medianHours).toBeCloseTo(2.5, 6);
59 });
60
61 test("a pull merged into a non-default branch is left out", async () => {
62 const s = await seed();
63 const r = await s.repo("r", "public");
64 r.pull(3, 2);
65 r.pull(20, 1, "side");
66 const t = readLeadTime(s.app.db, NOW);
67 expect(t.landed).toBe(1);
68 expect(t.maxHours).toBeCloseTo(1, 6);
69 });
70
71 test("a pull merged more than 24 h ago is left out", async () => {
72 const s = await seed();
73 const r = await s.repo("r", "public");
74 r.pull(3, 2);
75 r.pull(40, 25);
76 expect(readLeadTime(s.app.db, NOW).landed).toBe(1);
77 });
78
79 test("publicOnly hides private repositories", async () => {
80 const s = await seed();
81 const pub = await s.repo("pub", "public");
82 const priv = await s.repo("priv", "private");
83 pub.pull(3, 2);
84 priv.pull(20, 1);
85 expect(readLeadTime(s.app.db, NOW).landed).toBe(2);
86 const t = readLeadTime(s.app.db, NOW, true);
87 expect(t.landed).toBe(1);
88 expect(t.maxHours).toBeCloseTo(1, 6);
89 });
90 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs