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

A pure read says the median hours from a crew pull's opening to its first review verdict, over the last 24 hours #6488

Merged opened this

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

Merged

torque/forge:torque/6473-a-pure-read-says-the-median-hours-from-a → main 1 commit fork

  • Merged. torque/6473-a-pure-read-says-the-median-hours-from-a was merged into main as 3b6e2e0.

Review

Sign in to review this change.
2 files changed +149 −0
src/flow/review_latency.ts added +48
@@ -0,0 +1,48 @@
1 /**
2 * The review latency (#6473): the median hours from a crew pull's opening to
3 * its first review verdict (`approved` or `changes_requested`; a comment is
4 * not a verdict), over pulls whose first verdict landed in the last 24 h.
5 * Set beside the lead time it says whether pulls wait on review or on builders.
6 *
7 * A crew pull is one an agent on the repository's crew opened here; an
8 * imported pull is the origin author's, whoever connected it (`voiceOf`).
9 * Hours to one decimal; null when no pull had a first verdict in the window.
10 * `publicOnly` follows `readVitals`: public repositories alone.
11 */
12
13 import type { Database } from "bun:sqlite";
14
15 const HOUR_MS = 3_600_000;
16
17 export interface ReviewLatency {
18 /** Median hours from opening to first verdict, one decimal. */
19 readonly median: number;
20 /** Crew pulls whose first verdict landed in the window. */
21 readonly count: number;
22 }
23
24 export function readReviewLatency(db: Database, now = new Date(), publicOnly = false): ReviewLatency | null {
25 const from = new Date(now.getTime() - 24 * HOUR_MS).toISOString();
26 const rows = db
27 .query<{ hours: number }, [string, string, number]>(
28 `WITH first AS (
29 SELECT v.issue_id, MIN(v.created_at) AS at FROM review v
30 WHERE v.state IN ('approved', 'changes_requested') GROUP BY v.issue_id
31 )
32 SELECT (julianday(f.at) - julianday(i.created_at)) * 24 AS hours
33 FROM first f JOIN pull p ON p.issue_id = f.issue_id
34 JOIN issue i ON i.id = f.issue_id JOIN repo r ON r.id = i.repo_id
35 JOIN user u ON u.id = i.author_id
36 WHERE f.at >= ?1 AND f.at <= ?2 AND (?3 = 0 OR r.visibility = 'public')
37 AND u.is_agent = 1 AND (i.origin_author IS NULL OR i.origin_author = '')
38 AND EXISTS (SELECT 1 FROM repo_crew c WHERE c.repo_id = i.repo_id AND c.user_id = i.author_id)
39 ORDER BY hours`,
40 )
41 .all(from, now.toISOString(), publicOnly ? 1 : 0);
42 const n = rows.length;
43 if (n === 0) return null;
44 const hours = rows.map((r) => Math.max(0, r.hours));
45 const mid = n >> 1;
46 const median = n % 2 ? hours[mid]! : (hours[mid - 1]! + hours[mid]!) / 2;
47 return { median: Math.round(median * 10) / 10, count: n };
48 }
test/flow-review-latency.test.ts added +101
@@ -0,0 +1,101 @@
1 /** The review latency (#6473): median hours from a crew pull's opening to its first verdict, 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 { addCrew, createAgentRow } from "../src/core/agents.ts";
8 import { createRepo } from "../src/core/repos.ts";
9 import { createUser } from "../src/core/users.ts";
10 import { id } from "../src/kernel/id.ts";
11 import { readReviewLatency } from "../src/flow/review_latency.ts";
12
13 const NOW = new Date("2026-10-26T12:00:00.000Z");
14 const ago = (h: number) => new Date(NOW.getTime() - h * 3_600_000).toISOString();
15
16 type Verdict = "approved" | "changes_requested" | "commented";
17
18 async function seed() {
19 const app = createApp({ dataDir: mkdtempSync(join(tmpdir(), "forge-review-latency-")), port: 0 });
20 const alice = await createUser(app.db, { username: "alice", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true, adminIfFirst: true });
21 if (!alice.ok) throw new Error(alice.error);
22 const bot = await createUser(app.db, { username: "builder", email: "[email protected]", agent: true, origin: "agent" });
23 if (!bot.ok) throw new Error(bot.error);
24 createAgentRow(app.db, { userId: bot.value.id, brief: "builder." });
25 let n = 0;
26 const repo = async (name: string, visibility: "public" | "private") => {
27 const r = await createRepo(app.db, app.reposRoot, { ownerId: alice.value.id, name, visibility, description: name });
28 if (!r.ok) throw new Error(r.error);
29 addCrew(app.db, r.value.id, bot.value.id, alice.value.id);
30 return {
31 /** A pull opened `opened` hours ago, with reviews `[state, hours ago]`. */
32 pull: (opened: number, reviews: [Verdict, number][], author = bot.value.id) => {
33 const issueId = id();
34 n++;
35 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, 'open', ?5)`).run(issueId, r.value.id, n, author, ago(opened));
36 app.db.query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id) VALUES (?1, 'main', ?2, '', '', ?3)`).run(issueId, `b/${n}`, r.value.id);
37 for (const [state, at] of reviews)
38 app.db.query(`INSERT INTO review (id, issue_id, author_id, state, created_at) VALUES (?1, ?2, ?3, ?4, ?5)`).run(id(), issueId, alice.value.id, state, ago(at));
39 },
40 };
41 };
42 return { app, repo, alice: alice.value.id };
43 }
44
45 describe("review latency", () => {
46 test("no reviewed pulls gives null", async () => {
47 const s = await seed();
48 const r = await s.repo("r", "public");
49 r.pull(3, []);
50 expect(readReviewLatency(s.app.db, NOW)).toBeNull();
51 });
52
53 test("three pulls reviewed 1, 2 and 10 h after opening give 2.0 from 3; one reviewed two days ago is left out", async () => {
54 const s = await seed();
55 const r = await s.repo("r", "public");
56 r.pull(5, [["approved", 4]]); // 1 h
57 r.pull(6, [["changes_requested", 4]]); // 2 h
58 r.pull(20, [["approved", 10]]); // 10 h
59 r.pull(50, [["approved", 48]]); // two days ago
60 expect(readReviewLatency(s.app.db, NOW)).toEqual({ median: 2.0, count: 3 });
61 });
62
63 test("the first verdict counts, not a comment or a later verdict", async () => {
64 const s = await seed();
65 const r = await s.repo("r", "public");
66 r.pull(10, [["commented", 9], ["changes_requested", 7], ["approved", 1]]); // 3 h
67 expect(readReviewLatency(s.app.db, NOW)).toEqual({ median: 3.0, count: 1 });
68 });
69
70 test("a pull whose first verdict is older than 24 h is left out even with a fresh one", async () => {
71 const s = await seed();
72 const r = await s.repo("r", "public");
73 r.pull(40, [["changes_requested", 30], ["approved", 2]]);
74 expect(readReviewLatency(s.app.db, NOW)).toBeNull();
75 });
76
77 test("an even count averages the middle two, to one decimal", async () => {
78 const s = await seed();
79 const r = await s.repo("r", "public");
80 r.pull(2, [["approved", 1]]); // 1 h
81 r.pull(2.6, [["approved", 0]]); // 2.6 h
82 expect(readReviewLatency(s.app.db, NOW)).toEqual({ median: 1.8, count: 2 });
83 });
84
85 test("a person's pull is not a crew pull", async () => {
86 const s = await seed();
87 const r = await s.repo("r", "public");
88 r.pull(5, [["approved", 4]], s.alice);
89 expect(readReviewLatency(s.app.db, NOW)).toBeNull();
90 });
91
92 test("publicOnly hides a private repository", async () => {
93 const s = await seed();
94 const pub = await s.repo("pub", "public");
95 const priv = await s.repo("priv", "private");
96 pub.pull(5, [["approved", 4]]); // 1 h
97 priv.pull(20, [["approved", 10]]); // 10 h
98 expect(readReviewLatency(s.app.db, NOW)).toEqual({ median: 5.5, count: 2 });
99 expect(readReviewLatency(s.app.db, NOW, true)).toEqual({ median: 1.0, count: 1 });
100 });
101 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs