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

Shipped this week, and what a shipped change cost, are counted not sampled #177

Merged opened this

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

Merged

mason/forge:mason/106-shipped-this-week-and-what-a-shipped-cha → main 1 commit fork

  • Merged. mason/106-shipped-this-week-and-what-a-shipped-cha was merged into main as f213b62.

Review

Sign in to review this change.
3 files changed +180 −6
src/core/pulls.ts +26
@@ -288,6 +288,32 @@ export function mergedPullsForRepos(
288288 );
289289 }
290290
291 /**
292 * How many of a repository's pulls have landed since an instant — what
293 * "shipped this week" counts, and what the cost of a shipped change
294 * divides by. Counted by the database over the whole window, not off a
295 * page of closed pulls: past a hundred of those the week under-reports,
296 * and the cost per change reads high with nothing saying why.
297 *
298 * Closed is not shipped. A pull closed without a merge has no `merged_at`,
299 * and `NULL >= anything` is `NULL` — never true, so it never counts; the
300 * `IS NOT NULL` says so to the reader as well as the planner.
301 */
302 export function mergedPullCount(
303 db: Database,
304 repoId: string,
305 since: string,
306 ): number {
307 const row = db
308 .query<{ n: number }, [string, string]>(
309 `SELECT COUNT(*) AS n FROM issue JOIN pull ON pull.issue_id = issue.id
310 WHERE issue.repo_id = ?1
311 AND pull.merged_at IS NOT NULL AND pull.merged_at >= ?2`,
312 )
313 .get(repoId, since);
314 return row?.n ?? 0;
315 }
316
291317 /** The pull rows for a page of issues, keyed by issue id. */
292318 export function pullsForIssues(
293319 db: Database,
src/web/studio/app.ts +9 −5
@@ -11,7 +11,11 @@ import { listCrewRoles } from "../../core/agents.ts";
1111 import type { AppRecord } from "../../core/apps.ts";
1212 import { decisionStats, type DecisionStat } from "../../core/decisions.ts";
1313 import { getIssueById, listIssues, type Issue } from "../../core/issues.ts";
14 import { listPulls, openPullsClosing } from "../../core/pulls.ts";
14 import {
15 listPulls,
16 mergedPullCount,
17 openPullsClosing,
18 } from "../../core/pulls.ts";
1519 import { canSeeOwnerFacts, type Repo } from "../../core/repos.ts";
1620 import {
1721 agentRunTotals,
@@ -158,10 +162,10 @@ export async function studioApp(
158162 const decisions = withLikely(ctx, decisionsFor(ctx, [repo]));
159163 const openPulls = listPulls(db, repo.id, { state: "open", limit: 10 });
160164 const week = new Date(Date.now() - 7 * 24 * 60 * 60_000).toISOString();
161 const shipped = listPulls(db, repo.id, {
162 state: "closed",
163 limit: 100,
164 }).filter((p) => p.mergedAt !== null && p.mergedAt >= week).length;
165 // What landed this week, counted by the database like the runs and the
166 // spend below — a page of closed pulls would stop at a hundred and the
167 // cost per shipped change would read high off the short count.
168 const shipped = mergedPullCount(db, repo.id, week);
165169 // The rest is spend, worked out only for a viewer who is going to see it —
166170 // counted by the database over the whole week, not off a page of rows.
167171 const weekTotals = ownerFacts
test/studio.test.ts +145 −1
@@ -29,7 +29,7 @@ import { setCrewRole } from "../src/core/agents.ts";
2929 import { recordDecision } from "../src/core/decisions.ts";
3030 import { getIssue, listIssues, type Issue } from "../src/core/issues.ts";
3131 import { recordLook } from "../src/core/looks.ts";
32 import { getPull, type Pull } from "../src/core/pulls.ts";
32 import { getPull, mergedPullCount, type Pull } from "../src/core/pulls.ts";
3333 import { setDirectionIssue, type Repo } from "../src/core/repos.ts";
3434 import {
3535 agentRunTotals,
@@ -4659,6 +4659,150 @@ describe("this week's runs and spend are counted by the database, not read off a
46594659 });
46604660 });
46614661
4662 describe("this week's shipping is counted by the database, not read off a page of closed pulls", () => {
4663 let dataDir: string;
4664 let app: App;
4665 let servers: Servers;
4666 let base: string;
4667 let alice: User;
4668 let busy: Repo;
4669 let ownerCookie: string;
4670
4671 /** How many changes landed this week — past the page of closed pulls the
4672 * count used to be filtered out of. */
4673 const SHIPPED = 150;
4674 /** One agent run's cost, so the page prints a cost per shipped change. */
4675 const SPENT = 30;
4676
4677 /** A pull row on a closed issue: `mergedAt` null is closed without a merge. */
4678 const seedPull = (repo: Repo, title: string, mergedAt: string | null) => {
4679 const opened = openIssue(app, alice, repo, { title });
4680 if (!opened.ok) throw new Error(String(opened.error));
4681 app.db
4682 .query(
4683 `INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, merged_at)
4684 VALUES (?1, 'main', ?2, 'a', 'b', ?3)`,
4685 )
4686 .run(opened.value.id, `f${opened.value.number}`, mergedAt);
4687 app.db
4688 .query("UPDATE issue SET kind = 'pull', state = 'closed' WHERE id = ?1")
4689 .run(opened.value.id);
4690 };
4691
4692 beforeAll(async () => {
4693 dataDir = await mkdtemp(join(tmpdir(), "forge-week-shipped-"));
4694 app = createApp({ dataDir, port: 0 });
4695 servers = await serve(app);
4696 base = servers.url;
4697 const a = await createUser(app.db, {
4698 username: "alice",
4699 email: "[email protected]",
4700 password: "a-good-passphrase",
4701 adminIfFirst: true,
4702 });
4703 if (!a.ok) throw new Error(a.error);
4704 alice = a.value;
4705 ownerCookie = (
4706 (
4707 await fetch(`${base}/signin`, {
4708 method: "POST",
4709 body: new URLSearchParams({
4710 username: "alice",
4711 password: "a-good-passphrase",
4712 }),
4713 redirect: "manual",
4714 })
4715 ).headers.get("set-cookie") ?? ""
4716 ).split(";")[0]!;
4717
4718 for (const name of ["busy", "quiet"] as const) {
4719 const made = await createRepo(app, alice, {
4720 name,
4721 visibility: "private",
4722 description: "",
4723 });
4724 if (!made.ok) throw new Error(String(made.error));
4725 const seeded = await commitToBranch(app, alice, made.value, {
4726 message: "Start",
4727 files: [{ path: "README.md", content: "start\n" }],
4728 });
4729 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
4730 const enabled = await enableApp(app, alice, made.value, { slug: name });
4731 if (!enabled.ok) throw new Error(String(enabled.error));
4732 if (name === "busy") busy = made.value;
4733 }
4734
4735 // 150 changes landed inside the week — past the hundred closed pulls the
4736 // page used to read — plus two that must not count: one closed without a
4737 // merge, and one merged ten days back.
4738 const now = Date.now();
4739 for (let i = 0; i < SHIPPED; i++)
4740 seedPull(busy, `landed ${i}`, new Date(now - i * 60_000).toISOString());
4741 seedPull(busy, "abandoned", null);
4742 seedPull(
4743 busy,
4744 "landed last week",
4745 new Date(now - 10 * 24 * 60 * 60_000).toISOString(),
4746 );
4747
4748 const keeper = await createAgent(app, alice, { username: "keeper" });
4749 if (!keeper.ok) throw new Error(String(keeper.error));
4750 expect(addToCrew(app, alice, busy, "keeper").ok).toBe(true);
4751 const run = createRun(app.db, {
4752 repoId: busy.id,
4753 kind: "agent",
4754 actorId: getUserByName(app.db, "keeper")!.id,
4755 reason: "assigned",
4756 });
4757 claimRun(app.db, run.id);
4758 finishRun(app.db, run.id, {
4759 state: "ok",
4760 summary: "a week of work",
4761 costUsd: SPENT,
4762 });
4763 });
4764
4765 afterAll(async () => {
4766 servers.stop();
4767 app.close();
4768 await rm(dataDir, { recursive: true, force: true });
4769 });
4770
4771 const get = (path: string) =>
4772 fetch(`${base}${path}`, {
4773 headers: { cookie: `${ownerCookie}; forge_mode=studio` },
4774 redirect: "manual",
4775 });
4776
4777 test("the aggregate counts the merged pulls in the window and nothing else", () => {
4778 const week = new Date(Date.now() - 7 * 24 * 60 * 60_000).toISOString();
4779 expect(mergedPullCount(app.db, busy.id, week)).toBe(SHIPPED);
4780
4781 // Window opened wide: the older merge is there after all, the pull closed
4782 // without a merge still is not.
4783 expect(mergedPullCount(app.db, busy.id, "2000-01-01T00:00:00.000Z")).toBe(
4784 SHIPPED + 1,
4785 );
4786
4787 // COUNT over no rows is 0, and another repo's merges are never this one's.
4788 expect(mergedPullCount(app.db, "no-such-repo", week)).toBe(0);
4789 });
4790
4791 test("the app page says 150 changes shipped, not the 100 a page of closed pulls holds", async () => {
4792 const page = await (await get("/alice/busy")).text();
4793 expect(page).toMatch(/Shipped<\/dt>\s*<dd>150 changes<\/dd>/);
4794 expect(page).not.toContain("100 changes");
4795 // And the cost of a shipped change divides by all 150.
4796 expect(page).toContain(`$${(SPENT / SHIPPED).toFixed(2)} per shipped change`);
4797 });
4798
4799 test("an app that shipped nothing this week reads 0 changes and no cost per change", async () => {
4800 const page = await (await get("/alice/quiet")).text();
4801 expect(page).toMatch(/Shipped<\/dt>\s*<dd>0 changes<\/dd>/);
4802 expect(page).not.toContain("per shipped change");
4803 });
4804 });
4805
46624806 describe("finding things in the studio: /search", () => {
46634807 let dataDir: string;
46644808 let app: App;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs