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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6474-a-pure-read-says-the-crew-s-build-yield → main 1 commit fork
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 | }); |
new above
offline