BMAD-METHOD/scripts/lib/validation-progress-tracker.py

160 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""
Validation Progress Tracker - Track comprehensive validation progress
Purpose:
- Save progress after each story validation
- Enable resuming interrupted validation runs
- Provide real-time status updates
Created: 2026-01-02
"""
import yaml
from datetime import datetime
from pathlib import Path
from typing import Dict, List
class ValidationProgressTracker:
"""Tracks validation progress for resumability"""
def __init__(self, progress_file: str):
self.path = Path(progress_file)
self.data = self._load_or_initialize()
def _load_or_initialize(self) -> Dict:
"""Load existing progress or initialize new"""
if self.path.exists():
with open(self.path) as f:
return yaml.safe_load(f)
return {
'started_at': datetime.now().isoformat(),
'last_updated': datetime.now().isoformat(),
'epic_filter': None,
'total_stories': 0,
'stories_validated': 0,
'current_batch': 0,
'batches_completed': 0,
'status': 'in-progress',
'counters': {
'verified_complete': 0,
'needs_rework': 0,
'false_positives': 0,
'in_progress': 0,
'total_false_positive_tasks': 0,
'total_critical_issues': 0,
},
'validated_stories': {},
'remaining_stories': [],
}
def initialize(self, total_stories: int, story_list: List[str], epic_filter: str = None):
"""Initialize new validation run"""
self.data['total_stories'] = total_stories
self.data['remaining_stories'] = story_list
self.data['epic_filter'] = epic_filter
self.save()
def mark_story_validated(self, story_id: str, result: Dict):
"""Mark a story as validated with results"""
self.data['stories_validated'] += 1
self.data['validated_stories'][story_id] = {
'category': result.get('category'),
'score': result.get('verification_score'),
'false_positives': result.get('false_positive_count', 0),
'critical_issues': result.get('critical_issues_count', 0),
'validated_at': datetime.now().isoformat(),
}
# Update counters
category = result.get('category')
if category == 'VERIFIED_COMPLETE':
self.data['counters']['verified_complete'] += 1
elif category == 'FALSE_POSITIVE':
self.data['counters']['false_positives'] += 1
elif category == 'NEEDS_REWORK':
self.data['counters']['needs_rework'] += 1
elif category == 'IN_PROGRESS':
self.data['counters']['in_progress'] += 1
self.data['counters']['total_false_positive_tasks'] += result.get('false_positive_count', 0)
self.data['counters']['total_critical_issues'] += result.get('critical_issues_count', 0)
# Remove from remaining
if story_id in self.data['remaining_stories']:
self.data['remaining_stories'].remove(story_id)
self.data['last_updated'] = datetime.now().isoformat()
self.save()
def mark_batch_complete(self, batch_number: int):
"""Mark a batch as complete"""
self.data['batches_completed'] = batch_number
self.data['current_batch'] = batch_number + 1
self.save()
def mark_complete(self):
"""Mark entire validation as complete"""
self.data['status'] = 'complete'
self.data['completed_at'] = datetime.now().isoformat()
# Calculate duration
started = datetime.fromisoformat(self.data['started_at'])
completed = datetime.fromisoformat(self.data['completed_at'])
duration = completed - started
self.data['duration_hours'] = round(duration.total_seconds() / 3600, 1)
self.save()
def get_progress_percentage(self) -> float:
"""Get completion percentage"""
if self.data['total_stories'] == 0:
return 0
return round((self.data['stories_validated'] / self.data['total_stories']) * 100, 1)
def get_summary(self) -> Dict:
"""Get current progress summary"""
return {
'progress': f"{self.data['stories_validated']}/{self.data['total_stories']} ({self.get_progress_percentage()}%)",
'verified_complete': self.data['counters']['verified_complete'],
'false_positives': self.data['counters']['false_positives'],
'needs_rework': self.data['counters']['needs_rework'],
'remaining': len(self.data['remaining_stories']),
'status': self.data['status'],
}
def save(self):
"""Save progress to file"""
with open(self.path, 'w') as f:
yaml.dump(self.data, f, default_flow_style=False, sort_keys=False)
def get_remaining_stories(self) -> List[str]:
"""Get list of stories not yet validated"""
return self.data['remaining_stories']
def is_complete(self) -> bool:
"""Check if validation is complete"""
return self.data['status'] == 'complete'
if __name__ == '__main__':
# Example usage
tracker = ValidationProgressTracker('.validation-progress-2026-01-02.yaml')
# Initialize
tracker.initialize(100, ['story-1.md', 'story-2.md', '...'], epic_filter='16e')
# Mark story validated
tracker.mark_story_validated('story-1', {
'category': 'VERIFIED_COMPLETE',
'verification_score': 98,
'false_positive_count': 0,
'critical_issues_count': 0,
})
# Show progress
print(tracker.get_summary())
# Output: {'progress': '1/100 (1.0%)', 'verified_complete': 1, ...}