Skip to content

Commit 40acb10

Browse files
feat(engine): add summarizePlanProgress plan DAG helper
Closes #2298 Mirrors hosted planProgress counts and overall status field. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b98229d commit 40acb10

4 files changed

Lines changed: 173 additions & 0 deletions

File tree

packages/gittensory-engine/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ dashboard progress summaries:
541541
- `resolvePlanOverallStatus(plan)` — coarse status (`pending` | `running` | `completed` | `failed` | `blocked`); mirrors `planProgress`'s `status`
542542
- `hasPlanReadySteps(plan)` — any step is runnable now (`pending` with satisfied dependencies; mirrors `nextReadySteps(plan).length > 0`)
543543
- `isPlanTerminated(plan)` — plan reached a terminal outcome (`failed` step or every step `completed`/`skipped`; empty plans are not terminated)
544+
- `summarizePlanProgress(plan)` — per-status counts plus overall `status`; mirrors hosted `planProgress`
544545

545546
## Opportunity competition
546547

packages/gittensory-engine/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export { isPlanProgressComplete } from "./plan-progress-complete.js";
151151
export { resolvePlanOverallStatus, type PlanOverallStatus } from "./plan-overall-status.js";
152152
export { hasPlanReadySteps } from "./plan-ready.js";
153153
export { isPlanTerminated } from "./plan-terminated.js";
154+
export { summarizePlanProgress, type PlanProgressSummary } from "./plan-progress-summary.js";
154155
export * from "./plan-templates.js";
155156
export * from "./portfolio/queue.js";
156157
export {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { PlanDag, PlanStepStatus } from "./plan-export.js";
2+
import { resolvePlanOverallStatus, type PlanOverallStatus } from "./plan-overall-status.js";
3+
4+
export type PlanProgressSummary = {
5+
total: number;
6+
completed: number;
7+
failed: number;
8+
running: number;
9+
pending: number;
10+
skipped: number;
11+
status: PlanOverallStatus;
12+
};
13+
14+
/**
15+
* Aggregate per-status counts plus overall status. Mirrors hosted `planProgress`. Pure — reads the plan DAG only.
16+
*/
17+
export function summarizePlanProgress(plan: PlanDag): PlanProgressSummary {
18+
const count = (status: PlanStepStatus) => plan.steps.filter((step) => step.status === status).length;
19+
return {
20+
total: plan.steps.length,
21+
completed: count("completed"),
22+
failed: count("failed"),
23+
running: count("running"),
24+
pending: count("pending"),
25+
skipped: count("skipped"),
26+
status: resolvePlanOverallStatus(plan),
27+
};
28+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { summarizePlanProgress } from "../../packages/gittensory-engine/src/plan-progress-summary";
4+
import type { PlanStep } from "../../packages/gittensory-engine/src/plan-export";
5+
6+
function step(over: Partial<PlanStep> & { id: string; title: string }): PlanStep {
7+
return {
8+
actionClass: undefined,
9+
dependsOn: [],
10+
status: "pending",
11+
attempts: 0,
12+
maxAttempts: 3,
13+
lastError: null,
14+
...over,
15+
};
16+
}
17+
18+
describe("summarizePlanProgress", () => {
19+
it("returns zero counts and pending status for an empty plan", () => {
20+
expect(summarizePlanProgress({ steps: [] })).toEqual({
21+
total: 0,
22+
completed: 0,
23+
failed: 0,
24+
running: 0,
25+
pending: 0,
26+
skipped: 0,
27+
status: "pending",
28+
});
29+
});
30+
31+
it("counts each status and resolves completed overall status", () => {
32+
expect(
33+
summarizePlanProgress({
34+
steps: [
35+
step({ id: "a", title: "Build", status: "completed" }),
36+
step({ id: "b", title: "Deploy", status: "skipped" }),
37+
],
38+
}),
39+
).toEqual({
40+
total: 2,
41+
completed: 1,
42+
failed: 0,
43+
running: 0,
44+
pending: 0,
45+
skipped: 1,
46+
status: "completed",
47+
});
48+
});
49+
50+
it("reports failed status when any step failed", () => {
51+
expect(
52+
summarizePlanProgress({
53+
steps: [
54+
step({ id: "a", title: "Build", status: "running" }),
55+
step({ id: "b", title: "Deploy", status: "failed" }),
56+
],
57+
}),
58+
).toEqual({
59+
total: 2,
60+
completed: 0,
61+
failed: 1,
62+
running: 1,
63+
pending: 0,
64+
skipped: 0,
65+
status: "failed",
66+
});
67+
});
68+
69+
it("reports running status when a step is in flight and none failed", () => {
70+
expect(
71+
summarizePlanProgress({
72+
steps: [
73+
step({ id: "a", title: "Build", status: "running" }),
74+
step({ id: "b", title: "Test", status: "pending" }),
75+
],
76+
}),
77+
).toEqual({
78+
total: 2,
79+
completed: 0,
80+
failed: 0,
81+
running: 1,
82+
pending: 1,
83+
skipped: 0,
84+
status: "running",
85+
});
86+
});
87+
88+
it("reports blocked status for a cyclic deadlock", () => {
89+
expect(
90+
summarizePlanProgress({
91+
steps: [
92+
step({ id: "a", title: "A", dependsOn: ["b"] }),
93+
step({ id: "b", title: "B", dependsOn: ["a"] }),
94+
],
95+
}),
96+
).toEqual({
97+
total: 2,
98+
completed: 0,
99+
failed: 0,
100+
running: 0,
101+
pending: 2,
102+
skipped: 0,
103+
status: "blocked",
104+
});
105+
});
106+
107+
it("reports pending status when runnable steps remain", () => {
108+
expect(
109+
summarizePlanProgress({
110+
steps: [
111+
step({ id: "a", title: "Build", status: "pending" }),
112+
step({ id: "b", title: "Test", status: "pending", dependsOn: ["a"] }),
113+
],
114+
}),
115+
).toEqual({
116+
total: 2,
117+
completed: 0,
118+
failed: 0,
119+
running: 0,
120+
pending: 2,
121+
skipped: 0,
122+
status: "pending",
123+
});
124+
});
125+
126+
it("is exported from the package barrel", async () => {
127+
const barrel = await import("../../packages/gittensory-engine/src/index");
128+
expect(typeof barrel.summarizePlanProgress).toBe("function");
129+
expect(
130+
barrel.summarizePlanProgress({
131+
steps: [step({ id: "a", title: "A", status: "completed" })],
132+
}),
133+
).toEqual({
134+
total: 1,
135+
completed: 1,
136+
failed: 0,
137+
running: 0,
138+
pending: 0,
139+
skipped: 0,
140+
status: "completed",
141+
});
142+
});
143+
});

0 commit comments

Comments
 (0)