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:
1 parent
a12270ad01
commit
ae2c674b81
25 files changed
+3417
-1
No files matched your search
@@ -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")
|
||||
Reference in new issue
Block a user