From 343e02925043fa1dae1bafe4ea3e28024cc435ac Mon Sep 17 00:00:00 2001 From: Jonah Schulte Date: Fri, 2 Jan 2026 19:21:26 -0500 Subject: [PATCH] feat(validation): Add progress tracker for resumable validation runs Enables resume capability for long-running validation (4-6 hour runs) --- scripts/lib/validation-progress-tracker.py | 159 +++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 scripts/lib/validation-progress-tracker.py diff --git a/scripts/lib/validation-progress-tracker.py b/scripts/lib/validation-progress-tracker.py new file mode 100644 index 00000000..b241377b --- /dev/null +++ b/scripts/lib/validation-progress-tracker.py @@ -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, ...}