fix(start-work): add CHECKED_CHECKBOX_PATTERN for plan progress tracking (#3066)
Plan progress now correctly counts checked checkboxes when determining current task vs completed tasks. TDD verified. tsc clean. Closes #3066
This commit is contained in:
@@ -433,63 +433,89 @@ describe("boulder-state", () => {
|
||||
})
|
||||
|
||||
describe("getPlanProgress", () => {
|
||||
test("should count completed and uncompleted checkboxes", () => {
|
||||
// given - plan file with checkboxes
|
||||
test("should count only top-level tasks under TODOs and Final Verification Wave sections", () => {
|
||||
// given - plan with top-level tasks in tracked sections
|
||||
const planPath = join(TEST_DIR, "test-plan.md")
|
||||
writeFileSync(planPath, `# Plan
|
||||
- [ ] Task 1
|
||||
- [x] Task 2
|
||||
- [ ] Task 3
|
||||
- [X] Task 4
|
||||
|
||||
## TODOs
|
||||
- [ ] 1. Task 1
|
||||
- [x] 2. Task 2
|
||||
- [ ] 3. Task 3
|
||||
- [X] 4. Task 4
|
||||
|
||||
## Final Verification Wave
|
||||
- [ ] F1. Final review
|
||||
`)
|
||||
|
||||
// when
|
||||
const progress = getPlanProgress(planPath)
|
||||
|
||||
// then
|
||||
expect(progress.total).toBe(4)
|
||||
expect(progress.total).toBe(5)
|
||||
expect(progress.completed).toBe(2)
|
||||
expect(progress.isComplete).toBe(false)
|
||||
})
|
||||
|
||||
test("should count space-indented unchecked checkbox", () => {
|
||||
// given - plan file with a two-space indented checkbox
|
||||
const planPath = join(TEST_DIR, "space-indented-plan.md")
|
||||
test("should ignore nested Acceptance Criteria checkboxes under TODOs (issue #3066)", () => {
|
||||
// given - plan with 9 completed top-level tasks and unchecked nested acceptance criteria
|
||||
const planPath = join(TEST_DIR, "issue-3066-plan.md")
|
||||
writeFileSync(planPath, `# Plan
|
||||
- [ ] indented task
|
||||
|
||||
## TODOs
|
||||
- [x] 1. Implement feature A
|
||||
|
||||
**Acceptance Criteria**
|
||||
- [ ] criterion 1
|
||||
- [ ] criterion 2
|
||||
|
||||
- [x] 2. Implement feature B
|
||||
|
||||
**Acceptance Criteria**
|
||||
- [ ] criterion 3
|
||||
- [ ] criterion 4
|
||||
|
||||
- [x] 3. Implement feature C
|
||||
- [x] 4. Implement feature D
|
||||
- [x] 5. Implement feature E
|
||||
- [x] 6. Implement feature F
|
||||
- [x] 7. Implement feature G
|
||||
- [x] 8. Implement feature H
|
||||
- [x] 9. Implement feature I
|
||||
|
||||
## Final Verification Wave
|
||||
- [ ] F1. Final review
|
||||
`)
|
||||
|
||||
// when
|
||||
const progress = getPlanProgress(planPath)
|
||||
|
||||
// then
|
||||
expect(progress.total).toBe(1)
|
||||
expect(progress.completed).toBe(0)
|
||||
expect(progress.total).toBe(10)
|
||||
expect(progress.completed).toBe(9)
|
||||
expect(progress.isComplete).toBe(false)
|
||||
})
|
||||
|
||||
test("should count tab-indented unchecked checkbox", () => {
|
||||
// given - plan file with a tab-indented checkbox
|
||||
const planPath = join(TEST_DIR, "tab-indented-plan.md")
|
||||
test("should ignore checkboxes outside TODOs and Final Verification Wave sections", () => {
|
||||
// given - plan with checkboxes in Work Objectives, Success Criteria, and other sections
|
||||
const planPath = join(TEST_DIR, "ignore-other-sections-plan.md")
|
||||
writeFileSync(planPath, `# Plan
|
||||
- [ ] tab-indented task
|
||||
`)
|
||||
|
||||
// when
|
||||
const progress = getPlanProgress(planPath)
|
||||
## Work Objectives
|
||||
|
||||
// then
|
||||
expect(progress.total).toBe(1)
|
||||
expect(progress.completed).toBe(0)
|
||||
expect(progress.isComplete).toBe(false)
|
||||
})
|
||||
### Definition of Done
|
||||
- [ ] Verifiable condition with command
|
||||
|
||||
test("should count mixed top-level checked and indented unchecked checkboxes", () => {
|
||||
// given - plan file with checked top-level and unchecked indented task
|
||||
const planPath = join(TEST_DIR, "mixed-indented-plan.md")
|
||||
writeFileSync(planPath, `# Plan
|
||||
- [x] top-level completed task
|
||||
- [ ] nested unchecked task
|
||||
## TODOs
|
||||
- [x] 1. Real task one
|
||||
- [ ] 2. Real task two
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Final Checklist
|
||||
- [ ] All Must Have present
|
||||
- [ ] All Must NOT Have absent
|
||||
- [ ] All tests pass
|
||||
`)
|
||||
|
||||
// when
|
||||
@@ -501,11 +527,14 @@ describe("boulder-state", () => {
|
||||
expect(progress.isComplete).toBe(false)
|
||||
})
|
||||
|
||||
test("should count space-indented completed checkbox", () => {
|
||||
// given - plan file with a two-space indented completed checkbox
|
||||
const planPath = join(TEST_DIR, "indented-completed-plan.md")
|
||||
test("should ignore indented checkboxes under top-level tasks", () => {
|
||||
// given - plan with indented unchecked nested checkboxes
|
||||
const planPath = join(TEST_DIR, "nested-indented-plan.md")
|
||||
writeFileSync(planPath, `# Plan
|
||||
- [x] indented completed task
|
||||
|
||||
## TODOs
|
||||
- [x] 1. top-level completed task
|
||||
- [ ] nested unchecked task
|
||||
`)
|
||||
|
||||
// when
|
||||
@@ -517,20 +546,67 @@ describe("boulder-state", () => {
|
||||
expect(progress.isComplete).toBe(true)
|
||||
})
|
||||
|
||||
test("should return isComplete true when all checked", () => {
|
||||
// given - all tasks completed
|
||||
const planPath = join(TEST_DIR, "complete-plan.md")
|
||||
test("should require proper task label format in TODOs", () => {
|
||||
// given - plan with malformed labels (no numeric prefix)
|
||||
const planPath = join(TEST_DIR, "malformed-labels-plan.md")
|
||||
writeFileSync(planPath, `# Plan
|
||||
- [x] Task 1
|
||||
- [X] Task 2
|
||||
|
||||
## TODOs
|
||||
- [ ] no number prefix
|
||||
- [x] 1. Valid numbered task
|
||||
`)
|
||||
|
||||
// when
|
||||
const progress = getPlanProgress(planPath)
|
||||
|
||||
// then
|
||||
expect(progress.total).toBe(2)
|
||||
expect(progress.total).toBe(1)
|
||||
expect(progress.completed).toBe(1)
|
||||
expect(progress.isComplete).toBe(true)
|
||||
})
|
||||
|
||||
test("should require F-prefix label format in Final Verification Wave", () => {
|
||||
// given - plan with malformed final-wave labels
|
||||
const planPath = join(TEST_DIR, "malformed-final-plan.md")
|
||||
writeFileSync(planPath, `# Plan
|
||||
|
||||
## TODOs
|
||||
- [x] 1. Implementation done
|
||||
|
||||
## Final Verification Wave
|
||||
- [ ] missing F-prefix
|
||||
- [ ] F1. Proper final review
|
||||
- [x] F2. Another final review
|
||||
`)
|
||||
|
||||
// when
|
||||
const progress = getPlanProgress(planPath)
|
||||
|
||||
// then
|
||||
expect(progress.total).toBe(3)
|
||||
expect(progress.completed).toBe(2)
|
||||
expect(progress.isComplete).toBe(false)
|
||||
})
|
||||
|
||||
test("should return isComplete true when all top-level tasks checked", () => {
|
||||
// given - all top-level tasks completed
|
||||
const planPath = join(TEST_DIR, "complete-plan.md")
|
||||
writeFileSync(planPath, `# Plan
|
||||
|
||||
## TODOs
|
||||
- [x] 1. Task 1
|
||||
- [X] 2. Task 2
|
||||
|
||||
## Final Verification Wave
|
||||
- [x] F1. Final review
|
||||
`)
|
||||
|
||||
// when
|
||||
const progress = getPlanProgress(planPath)
|
||||
|
||||
// then
|
||||
expect(progress.total).toBe(3)
|
||||
expect(progress.completed).toBe(3)
|
||||
expect(progress.isComplete).toBe(true)
|
||||
})
|
||||
|
||||
@@ -555,6 +631,25 @@ describe("boulder-state", () => {
|
||||
expect(progress.total).toBe(0)
|
||||
expect(progress.isComplete).toBe(true)
|
||||
})
|
||||
|
||||
test("should support asterisk bullet top-level tasks", () => {
|
||||
// given - plan with asterisk bullet tasks
|
||||
const planPath = join(TEST_DIR, "asterisk-bullet-plan.md")
|
||||
writeFileSync(planPath, `# Plan
|
||||
|
||||
## TODOs
|
||||
* [x] 1. Task using asterisk bullet
|
||||
* [ ] 2. Another asterisk task
|
||||
`)
|
||||
|
||||
// when
|
||||
const progress = getPlanProgress(planPath)
|
||||
|
||||
// then
|
||||
expect(progress.total).toBe(2)
|
||||
expect(progress.completed).toBe(1)
|
||||
expect(progress.isComplete).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getPlanName", () => {
|
||||
|
||||
@@ -196,8 +196,25 @@ export function findPrometheusPlans(directory: string): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
const TODO_HEADING_PATTERN = /^##\s+TODOs\b/i
|
||||
const FINAL_VERIFICATION_HEADING_PATTERN = /^##\s+Final Verification Wave\b/i
|
||||
const SECOND_LEVEL_HEADING_PATTERN = /^##\s+/
|
||||
const UNCHECKED_CHECKBOX_PATTERN = /^(\s*)[-*]\s*\[\s*\]\s*(.+)$/
|
||||
const CHECKED_CHECKBOX_PATTERN = /^(\s*)[-*]\s*\[[xX]\]\s*(.+)$/
|
||||
const TODO_TASK_PATTERN = /^\d+\.\s+/
|
||||
const FINAL_WAVE_TASK_PATTERN = /^F\d+\.\s+/i
|
||||
|
||||
type ProgressSection = "todo" | "final-wave" | "other"
|
||||
|
||||
/**
|
||||
* Parse a plan file and count checkbox progress.
|
||||
*
|
||||
* Only top-level (zero-indent) checkboxes under `## TODOs` and
|
||||
* `## Final Verification Wave` sections are counted. The checkbox
|
||||
* body must carry a valid task label (`N.` for TODOs, `FN.` for
|
||||
* Final Verification Wave). Nested acceptance-criteria checkboxes
|
||||
* and checkboxes in other sections are intentionally ignored so
|
||||
* that progress tracking stays aligned with `readCurrentTopLevelTask`.
|
||||
*/
|
||||
export function getPlanProgress(planPath: string): PlanProgress {
|
||||
if (!existsSync(planPath)) {
|
||||
@@ -206,13 +223,47 @@ export function getPlanProgress(planPath: string): PlanProgress {
|
||||
|
||||
try {
|
||||
const content = readFileSync(planPath, "utf-8")
|
||||
|
||||
// Match markdown checkboxes: - [ ] or - [x] or - [X]
|
||||
const uncheckedMatches = content.match(/^\s*[-*]\s*\[\s*\]/gm) || []
|
||||
const checkedMatches = content.match(/^\s*[-*]\s*\[[xX]\]/gm) || []
|
||||
const lines = content.split(/\r?\n/)
|
||||
let section: ProgressSection = "other"
|
||||
let total = 0
|
||||
let completed = 0
|
||||
|
||||
const total = uncheckedMatches.length + checkedMatches.length
|
||||
const completed = checkedMatches.length
|
||||
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,
|
||||
|
||||
@@ -6,6 +6,7 @@ const TODO_HEADING_PATTERN = /^##\s+TODOs\b/i
|
||||
const FINAL_VERIFICATION_HEADING_PATTERN = /^##\s+Final Verification Wave\b/i
|
||||
const SECOND_LEVEL_HEADING_PATTERN = /^##\s+/
|
||||
const UNCHECKED_CHECKBOX_PATTERN = /^(\s*)[-*]\s*\[\s*\]\s*(.+)$/
|
||||
const CHECKED_CHECKBOX_PATTERN = /^(\s*)[-*]\s*\[[xX]\]\s*(.+)$/
|
||||
const TODO_TASK_PATTERN = /^(\d+)\.\s+(.+)$/
|
||||
const FINAL_WAVE_TASK_PATTERN = /^(F\d+)\.\s+(.+)$/i
|
||||
|
||||
|
||||
Reference in New Issue
Block a user