"""Quarantine management. When a file cannot be migrated within the retry budget, it is: 1. Written to ``quarantine/__.json`` (machine-readable) 2. Written to ``quarantine/__.md`` (manual-review ticket) The Markdown ticket is the automatically generated "manual ticket" requested in the pipeline requirements. """ from __future__ import annotations import json import re from datetime import datetime, timezone from pathlib import Path from migration_pipeline.config import QUARANTINE_DIR from migration_pipeline.models import FailureClass, QuarantineEntry # --------------------------------------------------------------------------- # Qt5 deprecated-API detector # --------------------------------------------------------------------------- # A non-exhaustive list of Qt5 APIs that are removed / changed in Qt6. _QT5_DEPRECATED_RE = re.compile( r"\b(" r"QRegExp|QTextCodec|QMatrix|QDesktopWidget|" r"QSignalMapper|QSortFilterProxyModel::lessThan|" r"QAbstractItemModel::reset|" r"QApplication::desktop|" r"QFontMetrics::width\s*\(|" r"QUrl::addQueryItem|" r"QSettings::setDefaultFormat|" r"QVariant::type\b|" r"QMetaType::type\b|" r"QTest::qSleep" r")\b" ) def _extract_affected_apis(code: str) -> list[str]: return sorted(set(_QT5_DEPRECATED_RE.findall(code))) # --------------------------------------------------------------------------- # Failure classification # --------------------------------------------------------------------------- def _classify( build_errors: str, test_output: str, elapsed: float, budget: float, ) -> FailureClass: if elapsed >= budget: return FailureClass.BUDGET_EXCEEDED lower_err = build_errors.lower() if "undefined reference" in lower_err or "linker command failed" in lower_err: return FailureClass.LINK_ERROR if "error:" in build_errors: return FailureClass.COMPILE_ERROR if "failed" in test_output.lower(): return FailureClass.TEST_FAILURE return FailureClass.UNKNOWN def _recommend(fc: FailureClass, affected_apis: list[str]) -> str: api_str = ", ".join(affected_apis) if affected_apis else "unbekannte APIs" if fc == FailureClass.BUDGET_EXCEEDED: return ( "Datei hat das Zeitbudget überschritten. " "Empfehlung: Datei manuell in kleinere, unabhängige Units zerlegen und erneut starten." ) if fc == FailureClass.COMPILE_ERROR: return ( f"Compilerfehler. Prüfe Qt6-API-Änderungen für: {api_str}. " "Porting Guide: https://doc.qt.io/qt-6/portingguide.html" ) if fc == FailureClass.LINK_ERROR: return ( "Linkfehler. Prüfe Qt6-Modulabhängigkeiten in CMakeLists.txt " "(z.B. find_package(Qt6 ...) und target_link_libraries)." ) if fc == FailureClass.TEST_FAILURE: return ( "Semantische Abweichung in Tests. Manuelle Überprüfung der Fachlogik erforderlich. " "Vergleiche Qt5- und Qt6-Verhalten für betroffene APIs." ) if fc == FailureClass.CHUNK_MERGE_ERROR: return ( "Chunk-Merge ist inkonsistent. " "Empfehlung: Datei als Ganzes manuell migrieren oder Chunk-Grenzen anpassen." ) return "Unbekannter Fehler. Bitte Logs manuell inspizieren." # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- def quarantine_file( *, file_path: str, track: str, build_errors: str, test_output: str, last_code: str, elapsed: float, budget: float, retry_count: int, chunk_info: str, ) -> QuarantineEntry: """Create a QuarantineEntry and write JSON + Markdown ticket to disk.""" now = datetime.now(tz=timezone.utc) ts = now.strftime("%Y%m%dT%H%M%SZ") iso_ts = now.isoformat() fc = _classify(build_errors, test_output, elapsed, budget) apis = _extract_affected_apis(last_code) recommendation = _recommend(fc, apis) entry = QuarantineEntry( file_path=file_path, track=track, failure_class=fc.value, last_error=(build_errors or test_output)[:2000], affected_apis=apis, chunk_info=chunk_info, manual_recommendation=recommendation, timestamp=iso_ts, retry_count=retry_count, elapsed_seconds=round(elapsed, 1), ) _write_quarantine_files(entry, ts) return entry # --------------------------------------------------------------------------- # File writers # --------------------------------------------------------------------------- def _write_quarantine_files(entry: QuarantineEntry, ts: str) -> None: QUARANTINE_DIR.mkdir(parents=True, exist_ok=True) stem = Path(entry.file_path).stem base_name = f"{stem}_{entry.track}_{ts}" # --- JSON (machine-readable) ------------------------------------------- json_path = QUARANTINE_DIR / f"{base_name}.json" json_path.write_text(json.dumps(entry.__dict__, indent=2, ensure_ascii=False)) # --- Markdown ticket (manual review) ------------------------------------ md_path = QUARANTINE_DIR / f"{base_name}.md" md_path.write_text(_render_ticket(entry)) def _render_ticket(e: QuarantineEntry) -> str: apis_md = "\n".join(f"- `{a}`" for a in e.affected_apis) or "_keine erkannt_" error_block = e.last_error.strip() or "_kein Fehlertext_" return f"""# Quarantäne-Ticket: {Path(e.file_path).name} | Feld | Wert | |---|---| | **Datei** | `{e.file_path}` | | **Track** | `{e.track}` | | **Fehlerklasse** | `{e.failure_class}` | | **Versuche** | {e.retry_count} | | **Laufzeit** | {e.elapsed_seconds:.1f} s | | **Zeitstempel** | {e.timestamp} | | **Chunk-Info** | {e.chunk_info} | ## Letzte Fehlermeldung ``` {error_block} ``` ## Betroffene Qt5-APIs {apis_md} ## Empfehlung für manuelle Migration {e.manual_recommendation} --- _Automatisch generiert von der Qt5→Qt6 Migrationspipeline._ """