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

A pure read says the crew's build yield over 24 hours: build runs finished, how many pushed, and how many of those pulls landed #6482

Merged opened this

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

Merged

scout/forge:scout/6474-a-pure-read-says-the-crew-s-build-yield → main 1 commit fork

  • Merged. scout/6474-a-pure-read-says-the-crew-s-build-yield was merged into main as 9189733.

Review

Sign in to review this change.
2 files changed +158 −0
src/flow/build_yield.ts added +59
@@ -0,0 +1,59 @@
1 /**
2 * The crew's build yield over 24 hours (#6474): of the agent runs that
3 * finished in the day before `now`, how many pushed, and how many of those
4 * pulls landed or were closed unmerged. A run is a build run unless it was a
5 * review. It pushed when a pull on its branch, in its repository, got a
6 * `pull.pushed` or `pull.opened` event between the run's start and end.
7 * `landed` and `closed` count those pulls, once each, as they stand now; a
8 * pull still open is in neither. Same `publicOnly` rule as `readVitals`; one read.
9 */
10
11 import type { Database } from "bun:sqlite";
12
13 const DAY_MS = 86_400_000;
14
15 export interface BuildYield {
16 readonly finished: number;
17 readonly pushed: number;
18 readonly landed: number;
19 readonly closed: number;
20 }
21
22 interface Row {
23 run_id: string;
24 pull_id: string | null;
25 merged: number | null;
26 closed: number | null;
27 }
28
29 export function readBuildYield(db: Database, now = new Date(), publicOnly = false): BuildYield {
30 const to = now.toISOString();
31 const from = new Date(now.getTime() - DAY_MS).toISOString();
32 const rows = db
33 .query<Row, [string, string, number]>(
34 `SELECT x.id AS run_id, i.id AS pull_id, p.merged_at IS NOT NULL AS merged, i.state = 'closed' AS closed
35 FROM run x JOIN repo r ON r.id = x.repo_id
36 LEFT JOIN pull p ON x.branch != '' AND p.head_branch = x.branch
37 AND EXISTS (SELECT 1 FROM issue q WHERE q.id = p.issue_id AND q.repo_id = x.repo_id)
38 AND EXISTS (SELECT 1 FROM issue q JOIN event e ON e.repo_id = q.repo_id
39 WHERE q.id = p.issue_id AND e.kind IN ('pull.pushed', 'pull.opened')
40 AND json_extract(e.payload, '$.number') = q.number
41 AND e.created_at >= x.started_at AND e.created_at <= x.finished_at)
42 LEFT JOIN issue i ON i.id = p.issue_id
43 WHERE x.kind = 'agent' AND x.reason != 'review' AND x.started_at IS NOT NULL
44 AND x.finished_at >= ?1 AND x.finished_at <= ?2 AND (?3 = 0 OR r.visibility = 'public')`,
45 )
46 .all(from, to, publicOnly ? 1 : 0);
47 const runs = new Set<string>();
48 const pushed = new Set<string>();
49 const landed = new Set<string>();
50 const closed = new Set<string>();
51 for (const row of rows) {
52 runs.add(row.run_id);
53 if (row.pull_id === null) continue;
54 pushed.add(row.run_id);
55 if (row.merged) landed.add(row.pull_id);
56 else if (row.closed) closed.add(row.pull_id);
57 }
58 return { finished: runs.size, pushed: pushed.size, landed: landed.size, closed: closed.size };
59 }
test/flow-build-yield.test.ts added +99
@@ -0,0 +1,99 @@
1 /** The crew's build yield over 24 hours (#6474). */
2 import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3 import { mkdtempSync, rmSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createUser } from "../src/core/users.ts";
8 import { id } from "../src/kernel/id.ts";
9 import { readBuildYield } from "../src/flow/build_yield.ts";
10
11 const NOW = new Date("2026-10-26T12:00:00.000Z");
12 const ago = (min: number) => new Date(NOW.getTime() - min * 60_000).toISOString();
13
14 describe("readBuildYield", () => {
15 let dir: string;
16 let app: App;
17 let owner: string;
18 let pub: string;
19 let priv: string;
20 let n = 0;
21
22 const repo = (name: string, visibility: string) => {
23 const rid = id();
24 app.db.query(`INSERT INTO repo (id, owner_id, name, visibility) VALUES (?1, ?2, ?3, ?4)`).run(rid, owner, name, visibility);
25 return rid;
26 };
27 const run = (repoId: string, branch: string, start: number, end: number, reason = "") => {
28 app.db
29 .query(`INSERT INTO run (id, repo_id, kind, branch, state, reason, started_at, finished_at) VALUES (?1, ?2, 'agent', ?3, 'ok', ?4, ?5, ?6)`)
30 .run(id(), repoId, branch, reason, ago(start), ago(end));
31 };
32 /** A pull on `branch`, with one event of `kind` `at` minutes ago. */
33 const pull = (repoId: string, branch: string, at: number, state: "open" | "merged" | "closed", kind = "pull.pushed") => {
34 const iid = id();
35 const number = ++n;
36 app.db
37 .query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at) VALUES (?1, ?2, ?3, 'pull', 't', '', ?4, ?5, ?6)`)
38 .run(iid, repoId, number, owner, state === "closed" ? "closed" : "open", ago(1000));
39 app.db
40 .query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, merged_at) VALUES (?1, 'main', ?2, '', '', ?3)`)
41 .run(iid, branch, state === "merged" ? ago(1) : null);
42 app.db.query(`INSERT INTO event (id, kind, repo_id, payload, created_at) VALUES (?1, ?2, ?3, ?4, ?5)`).run(id(), kind, repoId, JSON.stringify({ number }), ago(at));
43 };
44
45 beforeEach(async () => {
46 dir = mkdtempSync(join(tmpdir(), "forge-yield-"));
47 app = createApp({ dataDir: dir, port: 0 });
48 const u = await createUser(app.db, { username: "alice", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true });
49 if (!u.ok) throw new Error(u.error);
50 owner = u.value.id;
51 pub = repo("pub", "public");
52 priv = repo("priv", "private");
53 });
54 afterEach(() => {
55 app.db.close();
56 rmSync(dir, { recursive: true, force: true });
57 });
58
59 test("counts a run that merged, one still open, one closed and one that never pushed", () => {
60 run(pub, "a", 60, 50);
61 pull(pub, "a", 55, "merged");
62 run(pub, "b", 60, 50);
63 pull(pub, "b", 55, "open", "pull.opened");
64 run(pub, "c", 60, 50);
65 pull(pub, "c", 55, "closed");
66 run(pub, "d", 60, 50);
67 expect(readBuildYield(app.db, NOW)).toEqual({ finished: 4, pushed: 3, landed: 1, closed: 1 });
68 });
69
70 test("a push outside the run's span does not count, nor does a review run", () => {
71 run(pub, "a", 60, 50);
72 pull(pub, "a", 40, "merged");
73 run(pub, "r", 60, 50, "review");
74 pull(pub, "r", 55, "merged");
75 expect(readBuildYield(app.db, NOW)).toEqual({ finished: 1, pushed: 0, landed: 0, closed: 0 });
76 });
77
78 test("a run that finished 25 hours ago is left out, 23 is in", () => {
79 run(pub, "old", 25 * 60 + 10, 25 * 60);
80 run(pub, "new", 23 * 60 + 10, 23 * 60);
81 expect(readBuildYield(app.db, NOW).finished).toBe(1);
82 });
83
84 test("two runs on one pull push twice and land it once", () => {
85 run(pub, "a", 60, 50);
86 run(pub, "a", 40, 30);
87 pull(pub, "a", 55, "merged");
88 app.db.query(`INSERT INTO event (id, kind, repo_id, payload, created_at) VALUES (?1, 'pull.pushed', ?2, ?3, ?4)`).run(id(), pub, JSON.stringify({ number: n }), ago(35));
89 expect(readBuildYield(app.db, NOW)).toEqual({ finished: 2, pushed: 2, landed: 1, closed: 0 });
90 });
91
92 test("publicOnly hides private repositories", () => {
93 run(pub, "a", 60, 50);
94 run(priv, "p", 60, 50);
95 pull(priv, "p", 55, "merged");
96 expect(readBuildYield(app.db, NOW)).toEqual({ finished: 2, pushed: 1, landed: 1, closed: 0 });
97 expect(readBuildYield(app.db, NOW, true)).toEqual({ finished: 1, pushed: 0, landed: 0, closed: 0 });
98 });
99 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs