"""Batch orchestrator – coordinates the full migration of one batch. Flow: 1. Read the prioritised file list (one path per line, in order). 2. Create git worktrees for Track A and Track B in parallel. 3. Run cmake --configure in each worktree. 4. Process files in priority order, with at most MAX_CONCURRENT_TASKS workers per track. Both tracks run concurrently. 5. Commit a batch-summary to each branch. 6. Produce a comparison report (Track A vs Track B). """ from __future__ import annotations import asyncio import logging import time from pathlib import Path import httpx from migration_pipeline.build_runner import cmake_configure from migration_pipeline.config import ( MAX_CONCURRENT_TASKS, REPORTS_DIR, TRACK_A_ID, TRACK_B_ID, WORKSPACE_ROOT, ) from migration_pipeline.git_manager import ( commit_batch_summary, setup_worktree, ) from migration_pipeline.migrator import migrate_file from migration_pipeline.models import BatchResult, FileResult, FileStatus, Track from migration_pipeline.reporter import write_comparison_report logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # File-list loading # --------------------------------------------------------------------------- def load_file_list(list_path: Path, source_root: Path) -> list[str]: """Read the external priority file list. Accepts: - Plain text: one file path per line (blank lines and #-comments ignored). - The paths may be absolute or relative to source_root. """ base = source_root.resolve() files: list[str] = [] for raw in list_path.read_text().splitlines(): stripped = raw.strip() if not stripped or stripped.startswith("#"): continue p = Path(stripped) if not p.is_absolute(): p = base / p files.append(str(p.resolve())) return files # --------------------------------------------------------------------------- # Single-track worker pool # --------------------------------------------------------------------------- async def _run_track( client: httpx.AsyncClient, file_list: list[str], source_root: Path, track: Track, worktree_root: Path, build_dir: Path, batch_id: str, ) -> BatchResult: semaphore = asyncio.Semaphore(MAX_CONCURRENT_TASKS) source_root = source_root.resolve() async def _worker(fp: str) -> FileResult: async with semaphore: abs_file = Path(fp).resolve() try: rel_file = abs_file.relative_to(source_root) except ValueError as exc: raise ValueError( f"File '{abs_file}' is not under source-root '{source_root}'" ) from exc source_file = worktree_root / rel_file return await migrate_file( client=client, source_file=source_file, worktree_root=worktree_root, build_dir=build_dir, track=track, batch_id=batch_id, ) logger.info("[TRACK %s] Starting %d files", track.value, len(file_list)) results: list[FileResult] = await asyncio.gather( *[_worker(fp) for fp in file_list] ) successes = [r for r in results if r.status == FileStatus.SUCCESS.value] quarantine = [r for r in results if r.status == FileStatus.QUARANTINED.value] elapsed = [r.elapsed_seconds for r in results] warnings = sum(r.gate.total_warnings for r in results) return BatchResult( batch_id=batch_id, track=track.value, branch=f"migration/{batch_id}/{track.value}", total_files=len(results), success_count=len(successes), quarantine_count=len(quarantine), total_warnings=warnings, mean_elapsed_seconds=sum(elapsed) / len(elapsed) if elapsed else 0.0, results=results, ) # --------------------------------------------------------------------------- # Public entry point # --------------------------------------------------------------------------- async def run_batch( batch_id: str, file_list_path: Path, repo_root: Path, source_root: Path, ) -> tuple[BatchResult, BatchResult]: """Run the full batch on both tracks in parallel. Returns ``(result_track_a, result_track_b)``. """ file_list = load_file_list(file_list_path, source_root) logger.info( "[BATCH %s] Loaded %d files from %s", batch_id, len(file_list), file_list_path, ) workspace = WORKSPACE_ROOT.resolve() REPORTS_DIR.mkdir(parents=True, exist_ok=True) # ---- Set up worktrees for both tracks ---------------------------------- worktree_a = setup_worktree(batch_id, TRACK_A_ID, repo_root, workspace) worktree_b = setup_worktree(batch_id, TRACK_B_ID, repo_root, workspace) build_dir_a = worktree_a / "build" build_dir_b = worktree_b / "build" # ---- Configure cmake in both worktrees (in parallel) ------------------ logger.info("[BATCH %s] Configuring cmake for both tracks...", batch_id) await asyncio.gather( cmake_configure(worktree_a, build_dir_a), cmake_configure(worktree_b, build_dir_b), ) # ---- Run both tracks concurrently ------------------------------------- async with httpx.AsyncClient() as client: result_a, result_b = await asyncio.gather( _run_track( client, file_list, source_root, Track.A, worktree_a, build_dir_a, batch_id ), _run_track( client, file_list, source_root, Track.B, worktree_b, build_dir_b, batch_id ), ) # ---- Final batch commit on each branch -------------------------------- commit_batch_summary(worktree_a, TRACK_A_ID, batch_id) commit_batch_summary(worktree_b, TRACK_B_ID, batch_id) # ---- Write comparison report ------------------------------------------ report_path = REPORTS_DIR / f"{batch_id}_comparison.md" write_comparison_report(result_a, result_b, report_path) logger.info("[BATCH %s] Report written to %s", batch_id, report_path) _print_summary(result_a, result_b) return result_a, result_b # --------------------------------------------------------------------------- # Console summary # --------------------------------------------------------------------------- def _print_summary(a: BatchResult, b: BatchResult) -> None: print("\n" + "=" * 64) print(f" Batch: {a.batch_id}") print("=" * 64) _print_track_row("Track A (konservativ)", a) _print_track_row("Track B (modern) ", b) print("=" * 64 + "\n") def _print_track_row(label: str, r: BatchResult) -> None: print( f" {label}: " f"{r.success_count}/{r.total_files} OK | " f"{r.quarantine_count} quarantined | " f"{r.total_warnings} warnings | " f"avg {r.mean_elapsed_seconds:.0f}s" )