#!/usr/bin/env python3 """ PPT Master - Batch Project Validation Tool Checks the structural integrity and compliance of multiple projects at once. Usage: python3 scripts/batch_validate.py examples python3 scripts/batch_validate.py projects python3 scripts/batch_validate.py --all python3 scripts/batch_validate.py examples projects """ import argparse import sys from collections import defaultdict from pathlib import Path from console_encoding import configure_utf8_stdio configure_utf8_stdio() try: from project_utils import ( find_all_projects, get_project_info, validate_project_structure, validate_svg_viewbox, CANVAS_FORMATS ) except ImportError: print("Error: Unable to import project_utils module") print("Please ensure project_utils.py is in the same directory") sys.exit(1) class BatchValidator: """Batch validator""" def __init__(self): self.results: list[dict[str, object]] = [] self.summary = { 'total': 0, 'valid': 0, 'has_errors': 0, 'has_warnings': 0, 'missing_readme': 0, 'missing_spec': 0, 'svg_issues': 0 } def validate_directory(self, directory: str, recursive: bool = False) -> list[dict[str, object]]: """ Validate all projects in a directory Args: directory: Directory path recursive: Whether to recursively search subdirectories Returns: List of validation results """ dir_path = Path(directory) if not dir_path.exists(): print(f"[ERROR] Directory does not exist: {directory}") return [] print(f"\n[SCAN] Scanning directory: {directory}") print("=" * 80) projects = find_all_projects(directory) if not projects: print(f"[WARN] No projects found") return [] print(f"Found {len(projects)} project(s)\n") for project_path in projects: self.validate_project(str(project_path)) return self.results def validate_project(self, project_path: str) -> dict[str, object]: """ Validate a single project Args: project_path: Project path Returns: Validation result dictionary """ self.summary['total'] += 1 # Get project info info = get_project_info(project_path) # Validate project structure is_valid, errors, warnings = validate_project_structure(project_path) # Validate SVG viewBox svg_warnings = [] if info['svg_files']: project_path_obj = Path(project_path) svg_files = [project_path_obj / 'svg_output' / f for f in info['svg_files']] svg_warnings = validate_svg_viewbox(svg_files, info['format']) # Aggregate results result = { 'path': project_path, 'name': info['name'], 'format': info['format_name'], 'date': info['date_formatted'], 'svg_count': info['svg_count'], 'is_valid': is_valid, 'errors': errors, 'warnings': warnings + svg_warnings, 'has_readme': info['has_readme'], 'has_spec': info['has_spec'] } self.results.append(result) # Update statistics if is_valid and not warnings and not svg_warnings: self.summary['valid'] += 1 status = "[OK]" elif errors: self.summary['has_errors'] += 1 status = "[ERROR]" else: self.summary['has_warnings'] += 1 status = "[WARN]" if not info['has_readme']: self.summary['missing_readme'] += 1 if not info['has_spec']: self.summary['missing_spec'] += 1 if svg_warnings: self.summary['svg_issues'] += 1 # Print result print(f"{status} {info['name']}") print(f" Path: {project_path}") print( f" Format: {info['format_name']} | SVG: {info['svg_count']} file(s) | Date: {info['date_formatted']}") if errors: print(f" [ERROR] Errors ({len(errors)}):") for error in errors: print(f" - {error}") if warnings or svg_warnings: all_warnings = warnings + svg_warnings print(f" [WARN] Warnings ({len(all_warnings)}):") for warning in all_warnings[:3]: # Only show first 3 warnings print(f" - {warning}") if len(all_warnings) > 3: print(f" ... and {len(all_warnings) - 3} more warning(s)") print() return result def print_summary(self) -> None: """Print a summary of validation results.""" print("\n" + "=" * 80) print("[Summary] Validation Summary") print("=" * 80) print(f"\nTotal projects: {self.summary['total']}") print( f" [OK] Fully valid: {self.summary['valid']} ({self._percentage(self.summary['valid'])}%)") print( f" [WARN] With warnings: {self.summary['has_warnings']} ({self._percentage(self.summary['has_warnings'])}%)") print( f" [ERROR] With errors: {self.summary['has_errors']} ({self._percentage(self.summary['has_errors'])}%)") print(f"\nCommon issues:") print(f" Missing README.md: {self.summary['missing_readme']} project(s)") print(f" Missing design spec: {self.summary['missing_spec']} project(s)") print(f" SVG format issues: {self.summary['svg_issues']} project(s)") # Group statistics by format format_stats = defaultdict(int) for result in self.results: format_stats[result['format']] += 1 if format_stats: print(f"\nCanvas format distribution:") for fmt, count in sorted(format_stats.items(), key=lambda x: x[1], reverse=True): print(f" {fmt}: {count} project(s)") # Provide fix suggestions if self.summary['has_errors'] > 0 or self.summary['has_warnings'] > 0: print(f"\n[TIP] Fix suggestions:") if self.summary['missing_readme'] > 0: print(f" 1. Create documentation for projects missing README") print( f" Reference: examples/google_annual_report_ppt169_20251116/README.md") if self.summary['svg_issues'] > 0: print(f" 2. Check SVG root viewBox settings") print(f" The SVG root viewBox is the export canvas authority") if self.summary['missing_spec'] > 0: print(f" 3. Add design specification files") def _percentage(self, count: int) -> int: """Calculate percentage""" if self.summary['total'] == 0: return 0 return int(count / self.summary['total'] * 100) def export_report(self, output_file: str = 'validation_report.txt') -> None: """ Export validation report to file Args: output_file: Output file path """ with open(output_file, 'w', encoding='utf-8') as f: f.write("PPT Master Project Validation Report\n") f.write("=" * 80 + "\n\n") for result in self.results: status = "[OK] Valid" if result['is_valid'] and not result['warnings'] else \ "[ERROR] Error" if result['errors'] else "[WARN] Warning" f.write(f"{status} - {result['name']}\n") f.write(f"Path: {result['path']}\n") f.write( f"Format: {result['format']} | SVG: {result['svg_count']} file(s)\n") if result['errors']: f.write(f"\nErrors:\n") for error in result['errors']: f.write(f" - {error}\n") if result['warnings']: f.write(f"\nWarnings:\n") for warning in result['warnings']: f.write(f" - {warning}\n") f.write("\n" + "-" * 80 + "\n\n") # Write summary f.write("\n" + "=" * 80 + "\n") f.write("Validation Summary\n") f.write("=" * 80 + "\n\n") f.write(f"Total projects: {self.summary['total']}\n") f.write(f"Fully valid: {self.summary['valid']}\n") f.write(f"With warnings: {self.summary['has_warnings']}\n") f.write(f"With errors: {self.summary['has_errors']}\n") print(f"\n[REPORT] Validation report exported: {output_file}") def build_parser() -> argparse.ArgumentParser: """Build the command-line parser.""" parser = argparse.ArgumentParser( description="Validate one or more PPT Master project directories.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""Examples: python3 scripts/batch_validate.py examples python3 scripts/batch_validate.py projects python3 scripts/batch_validate.py examples projects python3 scripts/batch_validate.py --all """, ) parser.add_argument("directories", nargs="*", help="Directories to scan") parser.add_argument("--all", action="store_true", help="Validate examples and projects") parser.add_argument("--export", action="store_true", help="Write a validation report") parser.add_argument( "--output", default="validation_report.txt", help="Report path when --export is used", ) return parser def main(argv: list[str] | None = None) -> int: """Run the CLI entry point.""" parser = build_parser() args = parser.parse_args(argv) validator = BatchValidator() if args.all: directories = ['examples', 'projects'] else: directories = args.directories if not directories: parser.print_help() return 0 # Validate each directory for directory in directories: if Path(directory).exists(): validator.validate_directory(directory) else: print(f"[WARN] Skipping non-existent directory: {directory}\n") # Print summary validator.print_summary() # Export report (if specified) if args.export: validator.export_report(args.output) # Return exit code if validator.summary['has_errors'] > 0: return 1 elif validator.summary['has_warnings'] > 0: return 2 return 0 if __name__ == '__main__': raise SystemExit(main())