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
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/6473-a-pure-read-says-the-median-hours-from-a → main 1 commit fork
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 | }); |
new above
offline