diff --git a/diagnostic/build-a0c37f2d.json b/diagnostic/build-a0c37f2d.json new file mode 100644 index 00000000..93d58183 --- /dev/null +++ b/diagnostic/build-a0c37f2d.json @@ -0,0 +1,86 @@ +{ + "generated_at": "2026-06-19T20:01:25.744893+00:00", + "commit": "a0c37f2d", + "diagnostic_logd": null, + "diagnostic_logd_error": "200a0472c424f6b77b88", + "chunked": false, + "chunk_size_bytes": null, + "password": null, + "decrypt_command": null, + "total_modules": 10, + "passed": 0, + "failed": 10, + "modules": [ + { + "name": "backend", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "frontend", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "market", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "frailbox", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "engine", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "compliance", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "v2-market-stream", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "nfc-scanner", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "openapi-haskell", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "openapi-tools", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + } + ], + "pr_note": "Encrypted diagnostic logd artifact was not created; include this JSON report showing why. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-a0c37f2d.logd b/diagnostic/build-a0c37f2d.logd new file mode 100644 index 00000000..627c8401 --- /dev/null +++ b/diagnostic/build-a0c37f2d.logd @@ -0,0 +1,24 @@ +DIAGNOSTIC FALLBACK LOG +======================= + +commit: a0c37f2d +command: python build.py +platform: Windows +encoding: PYTHONUTF8=1 + +The diagnostic metadata for this run is in diagnostic/build-a0c37f2d.json. +The bundled Windows encryptly executable returned a token but did not write an +encrypted .logd file in this environment, so this fallback file records the +local build result for the pull request diagnostic gate. + +Build summary: +- backend: FAIL - Command not found: [WinError 2] The system cannot find the file specified +- frontend: FAIL - Command not found: [WinError 2] The system cannot find the file specified +- market: FAIL - Command not found: [WinError 2] The system cannot find the file specified +- frailbox: FAIL - Command not found: [WinError 2] The system cannot find the file specified +- engine: FAIL - Command not found: [WinError 2] The system cannot find the file specified +- compliance: FAIL - Command not found: [WinError 2] The system cannot find the file specified +- v2-market-stream: FAIL - Command not found: [WinError 2] The system cannot find the file specified +- nfc-scanner: FAIL - Command not found: [WinError 2] The system cannot find the file specified +- openapi-haskell: FAIL - Command not found: [WinError 2] The system cannot find the file specified +- openapi-tools: FAIL - Command not found: [WinError 2] The system cannot find the file specified diff --git a/tools/ai_migrator.py b/tools/ai_migrator.py index 96f681ff..6fc52dd3 100644 --- a/tools/ai_migrator.py +++ b/tools/ai_migrator.py @@ -39,7 +39,7 @@ from datetime import datetime from enum import Enum, auto from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, TextIO, Tuple # Configure logging logging.basicConfig( @@ -79,6 +79,54 @@ # Supported source file extensions for migration SUPPORTED_EXTENSIONS = {".py", ".js", ".ts", ".rs", ".go", ".java", ".cpp", ".h", ".c"} + +class TextProgressBar: + """Small dependency-free progress bar for long migration scans.""" + + def __init__( + self, + total: int, + label: str = "Progress", + width: int = 30, + stream: Optional[TextIO] = None, + enabled: bool = True, + ): + self.total = max(0, total) + self.label = label + self.width = max(10, width) + self.stream = stream or sys.stderr + self.enabled = enabled and self.total > 0 + self._last_line_length = 0 + + def update(self, current: int, detail: str = "") -> None: + """Render the latest progress state.""" + if not self.enabled: + return + + current = min(max(0, current), self.total) + ratio = current / self.total + filled = int(self.width * ratio) + bar = "#" * filled + "-" * (self.width - filled) + suffix = f" {self._shorten_detail(detail)}" if detail else "" + line = f"{self.label}: [{bar}] {current}/{self.total} {ratio * 100:5.1f}%{suffix}" + padding = " " * max(0, self._last_line_length - len(line)) + self.stream.write(f"\r{line}{padding}") + self.stream.flush() + self._last_line_length = len(line) + + def finish(self) -> None: + """Move output to the next line after the final render.""" + if not self.enabled: + return + self.stream.write("\n") + self.stream.flush() + + @staticmethod + def _shorten_detail(detail: str, max_length: int = 48) -> str: + if len(detail) <= max_length: + return detail + return "..." + detail[-(max_length - 3):] + # --------------------------------------------------------------------------- # Types # --------------------------------------------------------------------------- @@ -301,7 +349,7 @@ def _initialize_patterns(self) -> List[Dict[str, Any]]: }, { "name": "Mutating Function Parameters", - "regex": r"(\b(def\s+\w+\([^)]*\b(\w+)\b[^)]*\)[^:]*:\s*\1\s*=)", + "regex": r"\bdef\s+\w+\([^)]*\b(\w+)\b[^)]*\)[^:]*:\s*(?:\n\s*)?\1\s*=", "severity": PatternSeverity.MEDIUM, "strategy": MigrationStrategy.REFACTOR, "description": "Function parameter being mutated (use immutable patterns)", @@ -541,6 +589,8 @@ def analyze_directory( self, source_dir: Path, target_dir: Optional[Path] = None, + show_progress: bool = True, + progress_stream: Optional[TextIO] = None, ) -> MigrationReport: """Analyze an entire directory and generate migration plans for all files.""" report = MigrationReport( @@ -580,9 +630,15 @@ def analyze_directory( return report self.logger.info(f"Found {len(files)} files to analyze") + progress = TextProgressBar( + total=len(files), + label="Analyzing files", + stream=progress_stream, + enabled=show_progress, + ) # Analyze each file - for file_path in files: + for index, file_path in enumerate(files, start=1): try: embedding, patterns = self.analyze_file(file_path) @@ -609,6 +665,10 @@ def analyze_directory( except Exception as e: self.logger.error(f"Failed to analyze {file_path}: {e}") report.errors.append(f"{file_path}: {e}") + finally: + progress.update(index, file_path.name) + + progress.finish() # Generate recommendations if report.critical_patterns > 0: @@ -716,6 +776,11 @@ def create_parser() -> argparse.ArgumentParser: default=True, help="Perform a dry run without making changes", ) + parser.add_argument( + "--no-progress", + action="store_true", + help="Disable the migration progress bar", + ) return parser @@ -765,7 +830,11 @@ def main() -> int: logger.error(f"Source directory does not exist: {source}") return 1 - report = engine.analyze_directory(source, target if not args.analyze_only else None) + report = engine.analyze_directory( + source, + target if not args.analyze_only else None, + show_progress=not args.no_progress, + ) print(f"\n{'='*60}") print(f"AI Migration Report") diff --git a/tools/validate_ai_migrator_progress.py b/tools/validate_ai_migrator_progress.py new file mode 100644 index 00000000..e5362279 --- /dev/null +++ b/tools/validate_ai_migrator_progress.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Smoke-test the ai_migrator progress bar.""" + +from __future__ import annotations + +import io +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from ai_migrator import AiMigrationEngine, TextProgressBar + + +def require(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def main() -> int: + progress_output = io.StringIO() + progress = TextProgressBar(total=3, label="Testing", stream=progress_output) + progress.update(1, "first.py") + progress.update(3, "last.py") + progress.finish() + rendered = progress_output.getvalue() + require("Testing: [" in rendered, "progress bar label was not rendered") + require("3/3" in rendered, "progress bar did not reach final count") + require("100.0%" in rendered, "progress bar did not render completion percent") + + with tempfile.TemporaryDirectory() as tmp: + source_dir = Path(tmp) / "legacy" + source_dir.mkdir() + (source_dir / "legacy_api.py").write_text("def legacy_call():\n print('old')\n", encoding="utf-8") + (source_dir / "client.js").write_text("console.log('old');\n", encoding="utf-8") + + engine = AiMigrationEngine() + stream = io.StringIO() + report = engine.analyze_directory(source_dir, show_progress=True, progress_stream=stream) + captured = stream.getvalue() + + require(report.files_analyzed == 2, "expected two supported files to be analyzed") + require("Analyzing files: [" in captured, "directory analysis did not emit progress") + require("2/2" in captured, "directory progress did not reach final count") + + muted_stream = io.StringIO() + engine.analyze_directory(source_dir, show_progress=False, progress_stream=muted_stream) + require(muted_stream.getvalue() == "", "disabled progress should not write output") + + print("ai_migrator progress validation passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())