From b164f3273c29469c146aa8e64790e12731fab73a Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 17 Sep 2026 07:16:57 +0200 Subject: [PATCH] feat(roadmap): checklist-derived completion percentage --- src/lib/roadmap/progress.test.ts | 32 ++++++++++++++++++++++++++++++++ src/lib/roadmap/progress.ts | 17 +++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/lib/roadmap/progress.test.ts create mode 100644 src/lib/roadmap/progress.ts diff --git a/src/lib/roadmap/progress.test.ts b/src/lib/roadmap/progress.test.ts new file mode 100644 index 0000000..48ce29c --- /dev/null +++ b/src/lib/roadmap/progress.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "vitest"; +import { checklistPercent } from "@/lib/roadmap/progress"; + +describe("checklistPercent", () => { + it("returns null when there is no checklist", () => { + // A card with no subtasks has no honest completion figure. Showing 0% + // would claim nothing has been done, which is a different statement. + expect(checklistPercent(0, 0)).toBeNull(); + }); + + it("computes a whole percentage", () => { + expect(checklistPercent(3, 4)).toBe(75); + expect(checklistPercent(1, 3)).toBe(33); + }); + + it("returns 0 for an untouched checklist", () => { + expect(checklistPercent(0, 5)).toBe(0); + }); + + it("returns 100 for a complete one", () => { + expect(checklistPercent(5, 5)).toBe(100); + }); + + it("clamps impossible input rather than exceeding 100", () => { + expect(checklistPercent(9, 5)).toBe(100); + expect(checklistPercent(-1, 5)).toBe(0); + }); + + it("returns null for a negative total", () => { + expect(checklistPercent(0, -3)).toBeNull(); + }); +}); diff --git a/src/lib/roadmap/progress.ts b/src/lib/roadmap/progress.ts new file mode 100644 index 0000000..d3a155a --- /dev/null +++ b/src/lib/roadmap/progress.ts @@ -0,0 +1,17 @@ +/** + * Checklist completion for a card. + * + * The per-row percentage from the developer's Gantt reference, mapped onto the + * one thing in this product that honestly supports it: subtasks the developer + * ticked themselves. Nothing is inferred. + * + * Returns null rather than 0 for a card with no checklist. "0%" asserts that + * nothing has been done; "no figure" says nobody has broken this down. They are + * different statements and the second one is true. + */ +export function checklistPercent(done: number, total: number): number | null { + if (!Number.isFinite(total) || total <= 0) return null; + + const completed = Math.min(Math.max(done, 0), total); + return Math.round((completed / total) * 100); +}