Version Compare
Der Compare-Tab vergleicht zwei Versionen desselben Projekts über fünf Kategorien. Der Vergleich basiert ausschließlich auf gecachten Bundles (keine IFC-Parsing beim Compare), ist also schnell und deterministisch. Der gesamte Compare läuft über den Job-Runner (Ultraplan #4 — kein sync-Pfad), damit ein 5-MB-Diff einen Gunicorn-Worker nicht blockiert.
Kategorien
- Räume (
rooms) — keyed by GlobalId. added[],removed[],renamed[](gid retained, name geändert),retained_count.- Heizlast (
heizlast) — pro Raum + Gebäudetotal. per_room[gid] = {a_phi_HL_w, b_phi_HL_w, delta_w, delta_pct, component_deltas: {trans, vent, reheat}, kind}building = {a_total_w, b_total_w, delta_w, delta_pct}- Auslegung (
sizing) — aktiver Heizflächenkandidat pro Raum. per_room[gid] = {a_active, b_active, changed, still_covers_load, kind}- Presets (
presets) — flacher Key-Value-Diff mit Dotted-Paths. added{},removed{},modified{path: {a, b}}.- Oberflächen (
surfaces) — Roll-up pro Raum. per_room[gid] = {a_count, b_count, a_total_area_m2, b_total_area_m2, delta_area_m2, a_by_type, b_by_type, kind}
Material vs. Cosmetic
Jede Zeile bekommt kind: "material" | "cosmetic". Standard zeigt nur
material-Zeilen; ein Toggle blendet cosmetic ein.
| Kategorie | Schwelle für material |
|---|---|
| Heizlast | abs(delta_w) ≥ 50 W oder abs(delta_pct) ≥ 2 % |
| Sizing | Typ-Wechsel, null↔non-null, covers_load-Grenze überschritten, oder coverage_pct ≥ 1 % Drift |
| Oberflächen | abs(delta_area_m2) ≥ 0.1 m² pro Oberflächen-Typ |
| Presets | Immer material (benutzereditiert) |
| Räume | Added/removed/renamed sind immer material |
NaN / Inf
Alle Floats passieren _finite(); serialisiertes JSON enthält niemals NaN,
Infinity oder -Infinity. Ein 1000-Run-Fuzz-Test (test_version_diff_fuzz)
sichert diese Invariante gegen Regressionen.
Caching
compare_cache/<sha256_hash>.json
wobei hash = sha256(a_vid + a_bundle_mtimes + b_vid + b_bundle_mtimes). Bei
erneutem Vergleich derselben zwei Versionen (wenn keine Bundles mutiert
wurden) wird der Cache getroffen (200, kein Job). Jede Bundle-Mutation
invalidiert den Cache automatisch.
API
POST /projects/<p>/versions/compare?a=<vid>&b=<vid>
→ 200 {cached:true, cache_key} wenn Cache gültig
→ 202 {job_id, cache_key} wenn Job enqueued
GET /projects/<p>/versions/compare?a=<vid>&b=<vid>
→ 200 {full diff} wenn Cache vorhanden
→ 404 wenn kein Cache
Output-Shape (gekürzt)
{
"schema_version": 1,
"a_version": "v1", "b_version": "v3",
"generated_at": "2026-04-21T12:00:00+00:00",
"summary": {
"rooms_added": 2, "rooms_removed": 1, "rooms_renamed": 3,
"heizlast_total_delta_w": -1420, "heizlast_total_delta_pct": -2.9,
"sizing_changes": 7, "material_heizlast_changes": 6,
"material_surface_changes": 12, "preset_changes": 4
},
"rooms": { "added": [...], "removed": [...], "renamed": [...] },
"heizlast": { "per_room": {...}, "building": {...} },
"sizing": { "per_room": {...} },
"presets": { "added": {...}, "removed": {...}, "modified": {...} },
"surfaces": { "per_room": {...} },
"disclaimer": "Vergleich basiert auf gecachten Bundles ..."
}
Mandatory-Disclaimer
Vergleich basiert auf gecachten Bundles. Bei manueller Neuberechnung einer Version können sich Werte verschieben.
Erscheint als Banner oben in der Compare-Ansicht.