Files
gh-duongdev-ccpm/commands/_shared-checklist-helpers.md
2025-11-29 18:24:24 +08:00

802 lines
22 KiB
Markdown

<!-- cSpell:words ccpm CCPM -->
# Shared Checklist Utilities (Unified Parsing & Update Logic)
This file provides reusable utility functions for checklist management across CCPM commands. **These functions implement robust parsing, updating, and progress calculation for Implementation Checklists in Linear issue descriptions.**
## Overview
All CCPM commands that interact with checklists should use these utilities to ensure consistent behavior:
- `parseChecklist()` - Extracts checklist from description (marker comments or header-based)
- `updateChecklistItems()` - Updates checkbox states and recalculates progress
- `calculateProgress()` - Computes completion percentage
- `formatProgressLine()` - Generates standardized progress line
- `validateChecklistStructure()` - Checks checklist integrity
**Key Benefits**:
- **Consistent parsing** - Handles both marker comments and header-based formats
- **Robust updates** - Atomic checkbox state changes with progress calculation
- **Error resilience** - Graceful handling of malformed or missing checklists
- **Maintainability** - Single source of truth for checklist logic
**Usage in commands:** Reference this file at the start of command execution:
```markdown
READ: commands/_shared-checklist-helpers.md
```
Then use the functions as described below.
---
## Functions
### 1. parseChecklist
Extracts checklist items from a Linear issue description, supporting both marker comment and header-based formats.
```javascript
/**
* Parse checklist from Linear issue description
* @param {string} description - Full issue description (markdown)
* @returns {Object|null} Parsed checklist or null if not found
* @returns {Array<Object>} items - Parsed checklist items
* @returns {number} items[].index - 0-based index
* @returns {boolean} items[].checked - Checkbox state
* @returns {string} items[].content - Item text (without checkbox)
* @returns {string} format - 'marker' or 'header'
* @returns {number} startLine - Line number where checklist starts
* @returns {number} endLine - Line number where checklist ends
* @returns {string|null} progressLine - Existing progress line text
* @returns {number|null} progressLineNumber - Line number of progress line
*/
function parseChecklist(description) {
if (!description || typeof description !== 'string') {
return null;
}
const lines = description.split('\n');
let format = null;
let startLine = -1;
let endLine = -1;
let progressLine = null;
let progressLineNumber = null;
// Strategy 1: Try marker comment detection (preferred)
const startMarkerIndex = lines.findIndex(
line => line.trim() === '<!-- ccpm-checklist-start -->'
);
if (startMarkerIndex !== -1) {
// Found start marker, look for end marker
const endMarkerIndex = lines.findIndex(
(line, idx) => idx > startMarkerIndex && line.trim() === '<!-- ccpm-checklist-end -->'
);
if (endMarkerIndex !== -1) {
format = 'marker';
startLine = startMarkerIndex + 1; // First line after marker
endLine = endMarkerIndex - 1; // Last line before marker
// Look for progress line after end marker (within 3 lines)
for (let i = endMarkerIndex + 1; i < Math.min(endMarkerIndex + 4, lines.length); i++) {
if (lines[i].match(/^Progress: \d+% \(\d+\/\d+ completed\)/)) {
progressLine = lines[i];
progressLineNumber = i;
break;
}
}
}
}
// Strategy 2: Fallback to header-based detection
if (format === null) {
const headerPatterns = [
/^## ✅ Implementation Checklist/,
/^## Implementation Checklist/,
/^## ✅ Checklist/,
/^## Checklist/
];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (headerPatterns.some(pattern => pattern.test(line))) {
format = 'header';
startLine = i + 1;
// Find end: next ## header or end of description
endLine = lines.length - 1;
for (let j = i + 1; j < lines.length; j++) {
if (lines[j].match(/^##\s+/)) {
endLine = j - 1;
break;
}
}
// Look for progress line before next header
for (let j = i + 1; j <= endLine; j++) {
if (lines[j].match(/^Progress: \d+% \(\d+\/\d+ completed\)/)) {
progressLine = lines[j];
progressLineNumber = j;
// Exclude progress line from checklist items
if (endLine >= j) {
endLine = j - 1;
}
break;
}
}
break;
}
}
}
// No checklist found
if (format === null || startLine === -1) {
return null;
}
// Extract checklist items
const items = [];
const checkboxPattern = /^- \[([ x])\] (.+)$/;
for (let i = startLine; i <= endLine; i++) {
const line = lines[i].trim();
const match = line.match(checkboxPattern);
if (match) {
items.push({
index: items.length, // 0-based index
checked: match[1] === 'x',
content: match[2].trim(),
originalLine: i
});
}
}
// Return null if no items found
if (items.length === 0) {
return null;
}
return {
items,
format,
startLine,
endLine,
progressLine,
progressLineNumber
};
}
```
**Usage Example:**
```javascript
const description = `
## ✅ Implementation Checklist
<!-- ccpm-checklist-start -->
- [ ] Task 1: First task
- [x] Task 2: Second task
- [ ] Task 3: Third task
<!-- ccpm-checklist-end -->
Progress: 33% (1/3 completed)
Last updated: 2025-11-22 14:30 UTC
`;
const checklist = parseChecklist(description);
if (!checklist) {
console.log("No checklist found");
} else {
console.log(`Found ${checklist.items.length} items`);
console.log(`Format: ${checklist.format}`);
console.log(`Progress: ${checklist.progressLine}`);
checklist.items.forEach(item => {
console.log(`[${item.checked ? 'x' : ' '}] ${item.content}`);
});
}
```
**Edge Cases Handled:**
- **No checklist** → Returns `null`
- **Marker comments only** → Prefers marker-based parsing
- **Header only** → Falls back to header detection
- **Empty checklist** → Returns `null` (no items)
- **Malformed items** → Skips lines that don't match pattern
- **Multiple checklists** → Uses first one found (marker takes precedence)
- **Progress line** → Detected and excluded from items
---
### 2. updateChecklistItems
Updates checkbox states for specific items and recalculates progress.
```javascript
/**
* Update checklist item states and recalculate progress
* @param {string} description - Original issue description
* @param {number[]} indices - Array of item indices to update (0-based)
* @param {boolean} markComplete - true = check boxes, false = uncheck boxes
* @param {Object} options - Optional configuration
* @param {boolean} options.addTimestamp - Add/update timestamp (default: true)
* @returns {Object} Update result
* @returns {string} updatedDescription - Modified description
* @returns {number} changedCount - Number of items actually changed
* @returns {Object} progress - New progress metrics
* @returns {Array<string>} changedItems - Text of changed items (for logging)
*/
function updateChecklistItems(description, indices, markComplete, options = {}) {
const addTimestamp = options.addTimestamp !== false;
// Parse existing checklist
const checklist = parseChecklist(description);
if (!checklist) {
throw new Error('No checklist found in description');
}
if (indices.length === 0) {
throw new Error('No indices provided for update');
}
// Validate indices
const invalidIndices = indices.filter(idx => idx < 0 || idx >= checklist.items.length);
if (invalidIndices.length > 0) {
throw new Error(`Invalid indices: ${invalidIndices.join(', ')}. Valid range: 0-${checklist.items.length - 1}`);
}
// Split description into lines for modification
const lines = description.split('\n');
// Track changes
const changedItems = [];
let changedCount = 0;
// Update checkbox states
indices.forEach(idx => {
const item = checklist.items[idx];
const targetState = markComplete;
// Skip if already in target state
if (item.checked === targetState) {
return;
}
// Find and update the line
const lineIdx = item.originalLine;
const currentLine = lines[lineIdx];
const newLine = markComplete
? currentLine.replace('- [ ]', '- [x]')
: currentLine.replace('- [x]', '- [ ]');
if (newLine !== currentLine) {
lines[lineIdx] = newLine;
changedItems.push(item.content);
changedCount++;
}
});
// Recalculate progress
const updatedChecklist = parseChecklist(lines.join('\n'));
if (!updatedChecklist) {
throw new Error('Failed to parse updated checklist');
}
const progress = calculateProgress(updatedChecklist.items);
// Update or insert progress line
const newProgressLine = formatProgressLine(progress.completed, progress.total, addTimestamp);
if (updatedChecklist.progressLineNumber !== null) {
// Replace existing progress line
lines[updatedChecklist.progressLineNumber] = newProgressLine;
} else {
// Insert after checklist end (or after end marker if using markers)
const insertPosition = checklist.format === 'marker'
? checklist.endLine + 2 // After <!-- ccpm-checklist-end -->
: checklist.endLine + 1; // After last checklist item
// Ensure we have a blank line before progress
if (lines[insertPosition] && lines[insertPosition].trim() !== '') {
lines.splice(insertPosition, 0, '', newProgressLine);
} else {
lines.splice(insertPosition, 0, newProgressLine);
}
}
return {
updatedDescription: lines.join('\n'),
changedCount,
progress,
changedItems
};
}
```
**Usage Example:**
```javascript
const description = `
## ✅ Implementation Checklist
<!-- ccpm-checklist-start -->
- [ ] Task 1: First task
- [ ] Task 2: Second task
- [ ] Task 3: Third task
<!-- ccpm-checklist-end -->
Progress: 0% (0/3 completed)
`;
// Mark items 0 and 2 as complete
const result = updateChecklistItems(description, [0, 2], true);
console.log(`Changed ${result.changedCount} items`);
console.log(`New progress: ${result.progress.percentage}%`);
console.log(`Changed items: ${result.changedItems.join(', ')}`);
console.log(result.updatedDescription);
// Output:
// Changed 2 items
// New progress: 67%
// Changed items: Task 1: First task, Task 3: Third task
// [Updated description with items 0 and 2 checked]
```
**Edge Cases Handled:**
- **Invalid indices** → Throws error with details
- **Already in target state** → Skips update (idempotent)
- **No progress line** → Inserts new one
- **Existing progress line** → Updates in place
- **Empty indices array** → Throws error
---
### 3. calculateProgress
Computes completion percentage and counts from checklist items.
```javascript
/**
* Calculate completion progress from checklist items
* @param {Array<Object>} items - Checklist items from parseChecklist()
* @returns {Object} Progress metrics
* @returns {number} completed - Number of checked items
* @returns {number} total - Total number of items
* @returns {number} percentage - Completion percentage (0-100, rounded)
*/
function calculateProgress(items) {
if (!items || items.length === 0) {
return {
completed: 0,
total: 0,
percentage: 0
};
}
const total = items.length;
const completed = items.filter(item => item.checked).length;
const percentage = Math.round((completed / total) * 100);
return {
completed,
total,
percentage
};
}
```
**Usage Example:**
```javascript
const items = [
{ index: 0, checked: true, content: 'Task 1' },
{ index: 1, checked: false, content: 'Task 2' },
{ index: 2, checked: true, content: 'Task 3' }
];
const progress = calculateProgress(items);
console.log(`${progress.percentage}% (${progress.completed}/${progress.total})`);
// Output: 67% (2/3)
```
**Edge Cases Handled:**
- **Empty array** → Returns 0% (0/0)
- **Null items** → Returns 0% (0/0)
- **All complete** → Returns 100%
- **None complete** → Returns 0%
- **Fractional percentages** → Rounds to nearest integer
---
### 4. formatProgressLine
Generates standardized progress line with optional timestamp.
```javascript
/**
* Format progress line for checklist
* @param {number} completed - Number of completed items
* @param {number} total - Total number of items
* @param {boolean} includeTimestamp - Add timestamp (default: true)
* @returns {string} Formatted progress line
*/
function formatProgressLine(completed, total, includeTimestamp = true) {
const percentage = total === 0 ? 0 : Math.round((completed / total) * 100);
let line = `Progress: ${percentage}% (${completed}/${total} completed)`;
if (includeTimestamp) {
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 16);
line += `\nLast updated: ${timestamp} UTC`;
}
return line;
}
```
**Usage Example:**
```javascript
// With timestamp
const line1 = formatProgressLine(2, 5);
console.log(line1);
// Output:
// Progress: 40% (2/5 completed)
// Last updated: 2025-11-22 14:30 UTC
// Without timestamp
const line2 = formatProgressLine(3, 3, false);
console.log(line2);
// Output:
// Progress: 100% (3/3 completed)
```
---
### 5. validateChecklistStructure
Validates checklist structure and returns warnings for issues.
```javascript
/**
* Validate checklist structure and return warnings
* @param {string} description - Issue description to validate
* @returns {Object} Validation result
* @returns {boolean} valid - Overall validity
* @returns {Array<string>} warnings - List of warnings
* @returns {Array<string>} suggestions - Suggestions to fix issues
* @returns {Object|null} checklist - Parsed checklist (if valid)
*/
function validateChecklistStructure(description) {
const warnings = [];
const suggestions = [];
// Try to parse
const checklist = parseChecklist(description);
if (!checklist) {
return {
valid: false,
warnings: ['No checklist found in description'],
suggestions: [
'Add a checklist using marker comments:',
' <!-- ccpm-checklist-start -->',
' - [ ] Task 1',
' - [ ] Task 2',
' <!-- ccpm-checklist-end -->',
'',
'Or use a header:',
' ## ✅ Implementation Checklist',
' - [ ] Task 1',
' - [ ] Task 2'
],
checklist: null
};
}
// Check for marker comments (preferred)
if (checklist.format === 'header') {
warnings.push('Using header-based format (marker comments preferred)');
suggestions.push(
'Consider adding marker comments for more reliable parsing:',
' <!-- ccpm-checklist-start -->',
' ... existing checklist items ...',
' <!-- ccpm-checklist-end -->'
);
}
// Check for progress line
if (!checklist.progressLine) {
warnings.push('No progress line found');
suggestions.push(
'Add a progress line after the checklist:',
' Progress: 0% (0/N completed)',
' Last updated: YYYY-MM-DD HH:MM UTC'
);
}
// Check for empty checklist
if (checklist.items.length === 0) {
warnings.push('Checklist is empty (no items)');
suggestions.push('Add checklist items using the format: - [ ] Task description');
}
// Check for very long items (>200 chars)
const longItems = checklist.items.filter(item => item.content.length > 200);
if (longItems.length > 0) {
warnings.push(`${longItems.length} item(s) exceed 200 characters`);
suggestions.push('Consider breaking long items into smaller tasks');
}
// Check for duplicate content
const contentMap = new Map();
checklist.items.forEach(item => {
const normalized = item.content.toLowerCase().trim();
if (contentMap.has(normalized)) {
contentMap.set(normalized, contentMap.get(normalized) + 1);
} else {
contentMap.set(normalized, 1);
}
});
const duplicates = Array.from(contentMap.entries()).filter(([_, count]) => count > 1);
if (duplicates.length > 0) {
warnings.push(`${duplicates.length} duplicate item(s) found`);
suggestions.push('Review checklist for duplicate tasks');
}
return {
valid: warnings.length === 0,
warnings,
suggestions,
checklist
};
}
```
**Usage Example:**
```javascript
const description = `
## Implementation Checklist
- [ ] Task 1
- [ ] Task 1
`;
const validation = validateChecklistStructure(description);
console.log(`Valid: ${validation.valid}`);
console.log('Warnings:');
validation.warnings.forEach(w => console.log(` - ${w}`));
console.log('Suggestions:');
validation.suggestions.forEach(s => console.log(` ${s}`));
// Output:
// Valid: false
// Warnings:
// - Using header-based format (marker comments preferred)
// - No progress line found
// - 1 duplicate item(s) found
// Suggestions:
// Consider adding marker comments for more reliable parsing:
// <!-- ccpm-checklist-start -->
// ... existing checklist items ...
// <!-- ccpm-checklist-end -->
// Add a progress line after the checklist:
// Progress: 0% (0/N completed)
// Last updated: YYYY-MM-DD HH:MM UTC
// Review checklist for duplicate tasks
```
---
## Integration Patterns
### Pattern 1: Simple Checklist Update
Used by: `/ccpm:sync`, `/ccpm:verify`, `/ccpm:done`
```javascript
// 1. Fetch issue
const issue = await getIssue(issueId);
// 2. Parse checklist
const checklist = parseChecklist(issue.description);
if (!checklist) {
console.log('No checklist to update');
return;
}
// 3. Determine which items to update (e.g., from user selection)
const indicesToComplete = [0, 2, 5];
// 4. Update checklist
const result = updateChecklistItems(
issue.description,
indicesToComplete,
true // mark complete
);
// 5. Update Linear
await updateIssueDescription(issueId, result.updatedDescription);
// 6. Log progress
console.log(`✅ Updated ${result.changedCount} items`);
console.log(`Progress: ${result.progress.percentage}%`);
```
### Pattern 2: Validation Before Update
Used by: `/ccpm:utils:update-checklist`, `/ccpm:planning:update`
```javascript
// 1. Validate checklist structure
const validation = validateChecklistStructure(description);
if (!validation.valid) {
console.log('⚠️ Checklist has issues:');
validation.warnings.forEach(w => console.log(` - ${w}`));
console.log('\nSuggestions:');
validation.suggestions.forEach(s => console.log(` ${s}`));
// Ask user if they want to continue
const proceed = await askUserQuestion('Continue anyway?');
if (!proceed) return;
}
// 2. Proceed with update...
```
### Pattern 3: AI-Powered Suggestion
Used by: `/ccpm:sync` (smart checklist analysis)
```javascript
// 1. Parse checklist
const checklist = parseChecklist(issue.description);
if (!checklist) return;
// 2. Analyze git changes and score items
const uncheckedItems = checklist.items.filter(item => !item.checked);
const scoredItems = scoreChecklistItems(uncheckedItems, gitChanges);
// 3. Present suggestions to user
const highConfidence = scoredItems.filter(item => item.score >= 50);
console.log('🤖 AI Suggestions (high confidence):');
highConfidence.forEach(item => {
console.log(` [${item.index}] ${item.content}`);
});
// 4. Get user confirmation via interactive selection
const selectedIndices = await askUserQuestion(/* ... */);
// 5. Update selected items
const result = updateChecklistItems(
issue.description,
selectedIndices,
true
);
```
---
## Error Handling
All functions throw descriptive errors for invalid inputs:
```javascript
try {
const result = updateChecklistItems(description, [0, 1], true);
console.log('Update successful');
} catch (error) {
if (error.message.includes('No checklist found')) {
console.error('❌ No checklist to update');
// Suggest adding a checklist
} else if (error.message.includes('Invalid indices')) {
console.error('❌ Invalid item indices:', error.message);
// Show valid range
} else {
console.error('❌ Update failed:', error.message);
throw error;
}
}
```
---
## Best Practices
1. **Always validate inputs** - Use `parseChecklist()` before updates
2. **Handle null gracefully** - Check if checklist exists before operations
3. **Preserve user data** - Never modify items outside checklist section
4. **Update atomically** - Use `updateChecklistItems()` for batch updates
5. **Track changes** - Log `changedItems` and `changedCount` for audit trail
6. **Prefer marker comments** - Encourage migration to marker-based format
7. **Validate structure** - Use `validateChecklistStructure()` for quality checks
8. **Add timestamps** - Include update timestamps for transparency
---
## Testing Helpers
Test these functions in isolation:
```javascript
// Test parsing
const testDescription = `
## ✅ Implementation Checklist
<!-- ccpm-checklist-start -->
- [ ] Task 1
- [x] Task 2
- [ ] Task 3
<!-- ccpm-checklist-end -->
Progress: 33% (1/3 completed)
`;
const checklist = parseChecklist(testDescription);
console.log('Parsed items:', checklist.items.length);
// Test update
const result = updateChecklistItems(testDescription, [0, 2], true);
console.log('Updated description:', result.updatedDescription);
console.log('Progress:', result.progress);
// Test validation
const validation = validateChecklistStructure(testDescription);
console.log('Valid:', validation.valid);
console.log('Warnings:', validation.warnings);
```
---
## Maintenance
### When to Update This File
1. **New checklist format** - Add support for new parsing patterns
2. **Progress calculation changes** - Modify `calculateProgress()` logic
3. **Timestamp format changes** - Update `formatProgressLine()` format
4. **Validation rules** - Add new checks to `validateChecklistStructure()`
### Finding Usages
To find all commands using these helpers:
```bash
grep -r "parseChecklist\|updateChecklistItems\|calculateProgress" commands/ | grep -v "_shared-checklist"
```
### Version History
- **v1.0.0** - Initial implementation (Phase 1 of PSN-37)
- Core parsing functions
- Update logic with progress calculation
- Validation utilities
- Support for both marker and header formats
---
## Related Files
- `commands/_shared-linear-helpers.md` - Linear API delegation utilities
- `commands/utils:update-checklist.md` - Interactive checklist update command
- `commands/sync.md` - Natural sync command with AI checklist analysis
- `agents/linear-operations.md` - Linear operations subagent
---
**This file is part of CCPM's unified checklist management system (PSN-37).**