From e8c8376db482d048b4e4face4b11b407635f6110 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 7 Apr 2026 15:39:01 +0900 Subject: [PATCH] fix(boulder): support both structured and simple plan formats in getPlanProgress Structured plans (with ## TODOs section) use strict numbered-label parsing. Simple plans (without sections) fall back to regex checkbox counting. This fixes 9 test failures from the #3066 merge. --- src/features/boulder-state/storage.ts | 113 ++++++++++++++++---------- 1 file changed, 71 insertions(+), 42 deletions(-) diff --git a/src/features/boulder-state/storage.ts b/src/features/boulder-state/storage.ts index f72a57037..1d5dc2a59 100644 --- a/src/features/boulder-state/storage.ts +++ b/src/features/boulder-state/storage.ts @@ -224,57 +224,86 @@ export function getPlanProgress(planPath: string): PlanProgress { try { const content = readFileSync(planPath, "utf-8") const lines = content.split(/\r?\n/) - let section: ProgressSection = "other" - let total = 0 - let completed = 0 - for (const line of lines) { - if (SECOND_LEVEL_HEADING_PATTERN.test(line)) { - section = TODO_HEADING_PATTERN.test(line) - ? "todo" - : FINAL_VERIFICATION_HEADING_PATTERN.test(line) - ? "final-wave" - : "other" - continue - } + // Check if the plan has structured sections (## TODOs / ## Final Verification Wave) + const hasStructuredSections = lines.some((line) => TODO_HEADING_PATTERN.test(line)) - if (section !== "todo" && section !== "final-wave") { - continue - } - - const checkedMatch = line.match(CHECKED_CHECKBOX_PATTERN) - const uncheckedMatch = checkedMatch ? null : line.match(UNCHECKED_CHECKBOX_PATTERN) - const match = checkedMatch ?? uncheckedMatch - if (!match) { - continue - } - - if (match[1].length > 0) { - continue - } - - const taskBody = match[2].trim() - const labelPattern = section === "todo" ? TODO_TASK_PATTERN : FINAL_WAVE_TASK_PATTERN - if (!labelPattern.test(taskBody)) { - continue - } - - total++ - if (checkedMatch) { - completed++ - } + if (hasStructuredSections) { + // Structured plan: only count top-level checkboxes with numbered labels + // under ## TODOs and ## Final Verification Wave sections + return getStructuredPlanProgress(lines) } - return { - total, - completed, - isComplete: total > 0 && completed === total, - } + // Simple plan: count all top-level checkboxes anywhere + return getSimplePlanProgress(content) } catch { return { total: 0, completed: 0, isComplete: true } } } +function getStructuredPlanProgress(lines: string[]): PlanProgress { + let section: ProgressSection = "other" + let total = 0 + let completed = 0 + + for (const line of lines) { + if (SECOND_LEVEL_HEADING_PATTERN.test(line)) { + section = TODO_HEADING_PATTERN.test(line) + ? "todo" + : FINAL_VERIFICATION_HEADING_PATTERN.test(line) + ? "final-wave" + : "other" + continue + } + + if (section !== "todo" && section !== "final-wave") { + continue + } + + const checkedMatch = line.match(CHECKED_CHECKBOX_PATTERN) + const uncheckedMatch = checkedMatch ? null : line.match(UNCHECKED_CHECKBOX_PATTERN) + const match = checkedMatch ?? uncheckedMatch + if (!match) { + continue + } + + if (match[1].length > 0) { + continue + } + + const taskBody = match[2].trim() + const labelPattern = section === "todo" ? TODO_TASK_PATTERN : FINAL_WAVE_TASK_PATTERN + if (!labelPattern.test(taskBody)) { + continue + } + + total++ + if (checkedMatch) { + completed++ + } + } + + return { + total, + completed, + isComplete: total > 0 && completed === total, + } +} + +function getSimplePlanProgress(content: string): PlanProgress { + const uncheckedMatches = content.match(/^\s*[-*]\s*\[\s*\]/gm) || [] + const checkedMatches = content.match(/^\s*[-*]\s*\[[xX]\]/gm) || [] + + const total = uncheckedMatches.length + checkedMatches.length + const completed = checkedMatches.length + + return { + total, + completed, + isComplete: total > 0 && completed === total, + } +} + /** * Extract plan name from file path. */