feat(validation): Add progress tracker for resumable validation runs
Enables resume capability for long-running validation (4-6 hour runs)
This commit is contained in:
parent
73b8190e7b
commit
343e029250
|
|
@ -0,0 +1,159 @@
|
|||
#!/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, ...}
|
||||
Loading…
Reference in New Issue