From 56a1ada833f0db2bfcd30317300290f876b6c03d Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:34:34 +0200 Subject: [PATCH] feat(engine): add hasPlanSkippedSteps and document plan DAG helpers Closes #2298 Co-authored-by: Cursor --- packages/gittensory-engine/README.md | 14 +++++ packages/gittensory-engine/src/index.ts | 1 + .../gittensory-engine/src/plan-skipped.ts | 8 +++ test/unit/plan-skipped.test.ts | 54 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 packages/gittensory-engine/src/plan-skipped.ts create mode 100644 test/unit/plan-skipped.test.ts diff --git a/packages/gittensory-engine/README.md b/packages/gittensory-engine/README.md index 3469aff75e..363f8d1a10 100644 --- a/packages/gittensory-engine/README.md +++ b/packages/gittensory-engine/README.md @@ -517,6 +517,20 @@ rendering. Each builder returns `RawPlanStep[]` in the shape accepted by `gittensory_build_plan`. Templates are pure data — they describe step ordering via `dependsOn` but never actuate anything. +## Plan DAG status helpers + +`plan-export.ts` renders a validated `PlanDag`; the helpers below are pure predicates over that shape for miner and +dashboard progress summaries: + +- `countPlanSteps(plan)` — total step count +- `countPlanStepsByStatus(plan, status)` — steps matching a `PlanStepStatus` +- `isPlanEmpty(plan)` — whether the plan has no steps +- `isPlanFullyCompleted(plan)` — every step is `completed` (empty plans are not complete) +- `hasPlanFailedSteps(plan)` — any step is `failed` +- `hasPlanPendingSteps(plan)` — any step is `pending` +- `hasPlanRunningSteps(plan)` — any step is `running` +- `hasPlanSkippedSteps(plan)` — any step is `skipped` + ## Opportunity competition `computeOpportunityCompetition(highRiskDuplicateClusters, openPullRequests)` mirrors the hosted diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index d1ef6cba82..3fe4e1f595 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -143,6 +143,7 @@ export { isPlanFullyCompleted } from "./plan-completion.js"; export { hasPlanFailedSteps } from "./plan-failure.js"; export { hasPlanPendingSteps } from "./plan-pending.js"; export { hasPlanRunningSteps } from "./plan-running.js"; +export { hasPlanSkippedSteps } from "./plan-skipped.js"; export * from "./plan-templates.js"; export * from "./portfolio/queue.js"; export { diff --git a/packages/gittensory-engine/src/plan-skipped.ts b/packages/gittensory-engine/src/plan-skipped.ts new file mode 100644 index 0000000000..1dc48a8bde --- /dev/null +++ b/packages/gittensory-engine/src/plan-skipped.ts @@ -0,0 +1,8 @@ +import type { PlanDag } from "./plan-export.js"; + +/** + * Return whether any step in the plan was skipped. Pure — reads the plan DAG only. + */ +export function hasPlanSkippedSteps(plan: PlanDag): boolean { + return plan.steps.some((step) => step.status === "skipped"); +} diff --git a/test/unit/plan-skipped.test.ts b/test/unit/plan-skipped.test.ts new file mode 100644 index 0000000000..f21f3f154c --- /dev/null +++ b/test/unit/plan-skipped.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; + +import { hasPlanSkippedSteps } from "../../packages/gittensory-engine/src/plan-skipped"; +import type { PlanStep } from "../../packages/gittensory-engine/src/plan-export"; + +function step(over: Partial & { id: string; title: string }): PlanStep { + return { + actionClass: undefined, + dependsOn: [], + status: "pending", + attempts: 0, + maxAttempts: 3, + lastError: null, + ...over, + }; +} + +describe("hasPlanSkippedSteps", () => { + it("returns false for an empty plan", () => { + expect(hasPlanSkippedSteps({ steps: [] })).toBe(false); + }); + + it("returns false when no step was skipped", () => { + expect( + hasPlanSkippedSteps({ + steps: [ + step({ id: "a", title: "Build", status: "completed" }), + step({ id: "b", title: "Test", status: "pending" }), + ], + }), + ).toBe(false); + }); + + it("returns true when at least one step was skipped", () => { + expect( + hasPlanSkippedSteps({ + steps: [ + step({ id: "a", title: "Build", status: "completed" }), + step({ id: "b", title: "Deploy", status: "skipped" }), + ], + }), + ).toBe(true); + }); + + it("is exported from the package barrel", async () => { + const barrel = await import("../../packages/gittensory-engine/src/index"); + expect(typeof barrel.hasPlanSkippedSteps).toBe("function"); + expect( + barrel.hasPlanSkippedSteps({ + steps: [step({ id: "a", title: "A", status: "skipped" })], + }), + ).toBe(true); + }); +});