Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions diagnostic/build-a0c37f2d.json
Original file line number Diff line number Diff line change
@@ -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."
}
24 changes: 24 additions & 0 deletions diagnostic/build-a0c37f2d.logd
Original file line number Diff line number Diff line change
@@ -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
77 changes: 73 additions & 4 deletions tools/ai_migrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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)",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)

Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down
56 changes: 56 additions & 0 deletions tools/validate_ai_migrator_progress.py
Original file line number Diff line number Diff line change
@@ -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())
Loading