Public: anyone can read this; the crew can change it.
A pure read says the stranded spend: dollars of crew runs on pulls that are open and not landed #6434
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6421-a-pure-read-says-the-stranded-spend-doll → main 4 commits fork 3 threads · 3 unresolved
2 files
changed +140
−0
src/flow/stranded.ts added +52
| @@ -0,0 +1,52 @@ | ||
| 1 | /** | |
| 2 | * The stranded spend (#5930): what the crew's runs on pulls that are open and | |
| 3 | * not landed have cost so far. One read; the same `publicOnly` rule as | |
| 4 | * `readVitals`, so a reader who may not see private repositories learns | |
| 5 | * nothing about them. Numbers and dollars only, no repository named. | |
| 6 | */ | |
| 7 | ||
| 8 | import type { Database } from "bun:sqlite"; | |
| 9 | ||
| 10 | const COSTLIEST = 5; | |
| 11 | ||
| 12 | export interface StrandedPull { | |
| 13 | readonly number: number; | |
| 14 | readonly usd: number; | |
| 15 | } | |
| 16 | ||
| 17 | export interface Stranded { | |
| 18 | /** Dollars of runs on open, unmerged pulls. */ | |
| 19 | readonly usd: number; | |
| 20 | /** Open, unmerged pulls, whether or not they have cost anything yet. */ | |
| 21 | readonly pulls: number; | |
| 22 | /** The five that cost most, costliest first. */ | |
| 23 | readonly costliest: readonly StrandedPull[]; | |
| 24 | } | |
| 25 | ||
| 26 | interface Row { | |
| 27 | readonly number: number; | |
| 28 | readonly usd: number; | |
| 29 | readonly total_usd: number; | |
| 30 | readonly total_pulls: number; | |
| 31 | } | |
| 32 | ||
| 33 | export function readStranded(db: Database, publicOnly = false): Stranded { | |
| 34 | const rows = db | |
| 35 | .query<Row, [number, number]>( | |
| 36 | `WITH open_pull AS ( | |
| 37 | SELECT i.number AS number, | |
| 38 | COALESCE((SELECT SUM(u.cost_usd) FROM run u WHERE u.issue_id = p.issue_id), 0) AS usd | |
| 39 | FROM pull p JOIN issue i ON i.id = p.issue_id JOIN repo r ON r.id = i.repo_id | |
| 40 | WHERE (?1 = 0 OR r.visibility = 'public') AND i.state = 'open' AND p.merged_at IS NULL | |
| 41 | ) | |
| 42 | SELECT number, usd, SUM(usd) OVER () AS total_usd, COUNT(*) OVER () AS total_pulls | |
| 43 | FROM open_pull ORDER BY usd DESC, number ASC LIMIT ?2`, | |
| 44 | ) | |
| 45 | .all(publicOnly ? 1 : 0, COSTLIEST); | |
| 46 | const first = rows[0]; | |
| 47 | return { | |
| 48 | usd: first?.total_usd ?? 0, | |
| 49 | pulls: first?.total_pulls ?? 0, | |
| 50 | costliest: rows.map((r) => ({ number: r.number, usd: r.usd })), | |
| 51 | }; | |
| 52 | } |
test/flow-stranded.test.ts added +88
| @@ -0,0 +1,88 @@ | ||
| 1 | /** The stranded spend (#5930): runs on open, unmerged pulls, and nothing else. */ | |
| 2 | import { afterAll, beforeAll, 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 } 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 { readStranded } from "../src/flow/stranded.ts"; | |
| 11 | ||
| 12 | describe("readStranded", () => { | |
| 13 | let root: string; | |
| 14 | let app: ReturnType<typeof createApp>; | |
| 15 | let owner: string; | |
| 16 | let pub: string; | |
| 17 | let priv: string; | |
| 18 | const counter = new Map<string, number>(); | |
| 19 | ||
| 20 | beforeAll(async () => { | |
| 21 | root = mkdtempSync(join(tmpdir(), "forge-stranded-")); | |
| 22 | app = createApp({ dataDir: root, port: 0 }); | |
| 23 | const made = await createUser(app.db, { username: "alice", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true, adminIfFirst: true }); | |
| 24 | if (!made.ok) throw new Error(made.error); | |
| 25 | owner = made.value.id; | |
| 26 | for (const [name, visibility] of [["pub", "public"], ["priv", "private"]] as const) { | |
| 27 | const r = await createRepo(app.db, app.reposRoot, { ownerId: owner, name, visibility, description: name }); | |
| 28 | if (!r.ok) throw new Error(r.error); | |
| 29 | if (name === "pub") pub = r.value.id; | |
| 30 | else priv = r.value.id; | |
| 31 | } | |
| 32 | }); | |
| 33 | afterAll(() => { | |
| 34 | app.db.close(); | |
| 35 | rmSync(root, { recursive: true, force: true }); | |
| 36 | }); | |
| 37 | ||
| 38 | const pull = (repo: string, state: string, merged: string | null, costs: number[]) => { | |
| 39 | const issueId = id(); | |
| 40 | const n = (counter.get(repo) ?? 0) + 1; | |
| 41 | counter.set(repo, n); | |
| 42 | app.db | |
| 43 | .query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state) VALUES (?1, ?2, ?3, 'pull', 'P', '', ?4, ?5)`) | |
| 44 | .run(issueId, repo, n, owner, state); | |
| 45 | app.db | |
| 46 | .query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id, merged_at) VALUES (?1, 'main', ?2, '', '', ?3, ?4)`) | |
| 47 | .run(issueId, `b/${n}`, repo, merged); | |
| 48 | for (const c of costs) { | |
| 49 | app.db | |
| 50 | .query(`INSERT INTO run (id, repo_id, kind, issue_id, state, cost_usd) VALUES (?1, ?2, 'agent', ?3, 'ok', ?4)`) | |
| 51 | .run(id(), repo, issueId, c); | |
| 52 | } | |
| 53 | return n; | |
| 54 | }; | |
| 55 | ||
| 56 | test("nothing open reads as zero", () => { | |
| 57 | expect(readStranded(app.db)).toEqual({ usd: 0, pulls: 0, costliest: [] }); | |
| 58 | }); | |
| 59 | ||
| 60 | test("counts open pulls, leaves out landed and closed ones, ranks the costliest", () => { | |
| 61 | const a = pull(pub, "open", null, [1, 0.5]); | |
| 62 | pull(pub, "closed", "2026-10-26T00:00:00.000Z", [100]); // landed | |
| 63 | pull(pub, "closed", null, [50]); // closed unmerged | |
This assertion proves nothing about readStranded; remove it. |
||
| 64 | const b = pull(pub, "open", null, [4]); | |
| 65 | const c = pull(pub, "open", null, []); | |
| 66 | const s = readStranded(app.db); | |
| 67 | expect(s.usd).toBeCloseTo(5.5); | |
| 68 | expect(s.pulls).toBe(3); | |
| 69 | expect(s.costliest.map((p) => p.number)).toEqual([b, a, c]); | |
| 70 | expect(s.costliest[0]!.usd).toBeCloseTo(4); | |
| 71 | }); | |
| 72 | ||
| 73 | test("publicOnly leaves out private repositories", () => { | |
| 74 | pull(priv, "open", null, [10]); | |
| 75 | expect(readStranded(app.db).usd).toBeCloseTo(15.5); | |
| 76 | const s = readStranded(app.db, true); | |
| 77 | expect(s.usd).toBeCloseTo(5.5); | |
| 78 | expect(s.pulls).toBe(3); | |
| 79 | }); | |
| 80 | ||
| 81 | test("keeps only the five costliest, with 200 open pulls and 2,000 runs", () => { | |
| 82 | for (let i = 0; i < 200; i++) pull(pub, "open", null, Array.from({ length: 10 }, () => 0.1)); | |
| 83 | const s = readStranded(app.db, true); | |
| 84 | expect(s.costliest).toHaveLength(5); | |
| 85 | expect(s.pulls).toBe(203); | |
| 86 | expect(s.usd).toBeCloseTo(5.5 + 200); | |
| 87 | }); | |
| 88 | }); |
new above
offline
The header cites #6421, but the module comment cites #5930. Use one issue number.