chore(repo): replace app symlink/submodule with in-repo app directory; enable committing app/Dockerfile and tooling\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This commit is contained in:
torsten committed 2026-07-17 13:13:02 +02:00
1 parent a12270ad01
commit ae2c674b81
25 files changed
+3417 -1

No files matched your search

+221
View File
@@ -0,0 +1,221 @@
"""Batch comparison reporter.
Generates a Markdown report that places Track A and Track B side-by-side
so the burn-phase decision can be made on objective metrics.
"""
from __future__ import annotations
import json
from pathlib import Path
from migration_pipeline.models import BatchResult, FileResult, FileStatus
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def write_comparison_report(
result_a: BatchResult,
result_b: BatchResult,
output_path: Path,
) -> None:
"""Write a Markdown comparison of Track A vs Track B to *output_path*."""
output_path.parent.mkdir(parents=True, exist_ok=True)
md = _render_markdown(result_a, result_b)
output_path.write_text(md, encoding="utf-8")
# Machine-readable companion file.
json_path = output_path.with_suffix(".json")
json_path.write_text(
json.dumps(_to_dict(result_a, result_b), indent=2, ensure_ascii=False),
encoding="utf-8",
)
# ---------------------------------------------------------------------------
# Markdown rendering
# ---------------------------------------------------------------------------
def _render_markdown(a: BatchResult, b: BatchResult) -> str:
ts = _utc_now()
lines: list[str] = [
f"# Migrationsbericht – Batch `{a.batch_id}`",
f"_Erstellt: {ts}_",
"",
"## Übersicht",
"",
"| Metrik | Track A – konservativ | Track B – modern |",
"|---|---|---|",
f"| Branch | `{a.branch}` | `{b.branch}` |",
f"| Dateien gesamt | {a.total_files} | {b.total_files} |",
f"| Erfolge | {a.success_count} ({a.success_rate:.0%}) | {b.success_count} ({b.success_rate:.0%}) |",
f"| Quarantäne | {a.quarantine_count} ({a.quarantine_rate:.0%}) | {b.quarantine_count} ({b.quarantine_rate:.0%}) |",
f"| Warnungen (gesamt, advisory) | {a.total_warnings} | {b.total_warnings} |",
f"| Mittlere Laufzeit | {a.mean_elapsed_seconds:.0f} s | {b.mean_elapsed_seconds:.0f} s |",
"",
"## Datei-Detailtabelle",
"",
"| Datei | Track A Status | Track A Zeit | Track A Warn | Track B Status | Track B Zeit | Track B Warn |",
"|---|---|---|---|---|---|---|",
]
# Build lookup for track B by file path.
b_by_file = {r.file_path: r for r in b.results}
for ra in a.results:
rb = b_by_file.get(ra.file_path)
name = Path(ra.file_path).name
lines.append(
f"| `{name}` "
f"| {_status_badge(ra.status)} "
f"| {ra.elapsed_seconds:.0f} s "
f"| {ra.gate.total_warnings} "
f"| {_status_badge(rb.status) if rb else '—'} "
f"| {f'{rb.elapsed_seconds:.0f} s' if rb else '—'} "
f"| {rb.gate.total_warnings if rb else '—'} |"
)
lines += [
"",
"## Quarantäne-Dateien",
"",
]
quarantine_a = [r for r in a.results if r.status == FileStatus.QUARANTINED.value]
quarantine_b = [r for r in b.results if r.status == FileStatus.QUARANTINED.value]
if quarantine_a or quarantine_b:
lines += [
"| Datei | Track | Fehlerklasse | Versuche | Empfehlung |",
"|---|---|---|---|---|",
]
for r in quarantine_a + quarantine_b:
q = r.quarantine_entry
if q:
name = Path(r.file_path).name
rec = q.manual_recommendation.replace("|", "\\|")[:80]
lines.append(
f"| `{name}` | {r.track} | `{q.failure_class}` "
f"| {q.retry_count} | {rec}… |"
)
else:
lines.append("_Keine Dateien in Quarantäne._")
lines += [
"",
"## Warnungen (Advisory)",
"",
"> Warnungen blockieren keine Quality Gates. Sie sind hier zur "
"Entscheidungshilfe für die Burn-Phase aufgeführt.",
"",
]
warn_rows = [
r for r in (a.results + b.results) if r.gate.total_warnings > 0
]
if warn_rows:
lines += [
"| Datei | Track | Anzahl Warnungen |",
"|---|---|---|",
]
for r in sorted(warn_rows, key=lambda x: -x.gate.total_warnings):
lines.append(
f"| `{Path(r.file_path).name}` | {r.track} "
f"| {r.gate.total_warnings} |"
)
else:
lines.append("_Keine Warnungen aufgetreten._")
lines += [
"",
"## Burn-Phase Empfehlung",
"",
_burn_recommendation(a, b),
"",
"---",
"_Automatisch generiert von der Qt5→Qt6 Migrationspipeline._",
]
return "\n".join(lines)
def _status_badge(status: str) -> str:
icons = {
FileStatus.SUCCESS.value: "✅ Erfolg",
FileStatus.QUARANTINED.value: "🔴 Quarantäne",
FileStatus.IN_PROGRESS.value: "⏳ Läuft",
FileStatus.PENDING.value: "⏸ Ausstehend",
}
return icons.get(status, status)
def _burn_recommendation(a: BatchResult, b: BatchResult) -> str:
if a.success_rate > b.success_rate + 0.05:
return (
"**Track A (konservativ)** hat eine höhere Erfolgsquote. "
"Empfehlung für die Burn-Phase: Track A als Basis, Track B für "
"ausgewählte, gut getestete Module."
)
if b.success_rate > a.success_rate + 0.05:
return (
"**Track B (modern)** hat eine höhere Erfolgsquote. "
"Empfehlung: Track B als Basis übernehmen."
)
if a.total_warnings < b.total_warnings:
return (
"Beide Tracks haben ähnliche Erfolgsquoten. "
"Track A hat weniger Warnungen – bevorzuge Track A für risikoarme Module, "
"Track B für Module mit guter Testabdeckung."
)
return (
"Beide Tracks sind gleichwertig. "
"Entscheidung liegt beim Team – empfohlene Kriterien: "
"Wartbarkeit, Warnungsfreiheit, vorhandene Testabdeckung."
)
# ---------------------------------------------------------------------------
# JSON serialisation helper
# ---------------------------------------------------------------------------
def _to_dict(a: BatchResult, b: BatchResult) -> dict:
def _result(r: BatchResult) -> dict:
return {
"batch_id": r.batch_id,
"track": r.track,
"branch": r.branch,
"total_files": r.total_files,
"success_count": r.success_count,
"quarantine_count": r.quarantine_count,
"total_warnings": r.total_warnings,
"mean_elapsed_seconds": round(r.mean_elapsed_seconds, 1),
"success_rate": round(r.success_rate, 4),
"quarantine_rate": round(r.quarantine_rate, 4),
"files": [_file_result(f) for f in r.results],
}
def _file_result(f: FileResult) -> dict:
d: dict = {
"file_path": f.file_path,
"status": f.status,
"iterations": f.iterations,
"elapsed_seconds": f.elapsed_seconds,
"warnings": f.gate.total_warnings,
"test_name": f.test_name,
}
if f.quarantine_entry:
d["quarantine"] = f.quarantine_entry.__dict__
return d
return {"track_a": _result(a), "track_b": _result(b)}
# ---------------------------------------------------------------------------
# Utility
# ---------------------------------------------------------------------------
def _utc_now() -> str:
from datetime import datetime, timezone
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M UTC")