Report-Spezifikation — JSON-Contract
Dieses Dokument definiert den vollständigen Report-JSON-Contract von LandNutzen. Ziel: ein Entwickler kann danach die deterministische Szenario-/Cost-Engine, den LLM-Prompt und die beiden PDF-Templates (React-PDF Standard, Puppeteer Premium) bauen, ohne raten zu müssen.
Querverweise: PRODUCT.md · ARCHITECTURE.md · ADR-0015 Kostenschätzung · ADR-0013 PDF · ADR-0006 Auth/Multitenant · ADR-0016 Grundbuch · ADR-0014 HORA-Pass · DATA-SOURCES.md · POC-PLAN.md
1. Grundprinzipien
1.1 Trennung Engine ↔ LLM (verbindlich)
Konsistent mit ADR-0015:
``` [Deterministische Engine berechnet ALLE Zahlen + Ampeln + Scores] ↓ [strukturiertes Report-JSON (dieser Contract)] ↓ [LLM erhält Report-JSON + Quellenliste + Schreib-Anweisung] ↓ [LLM rendert NUR narrative Felder, zitiert Engine-Werte 1:1] ↓ [Zahlen-Konsistenz-Guard: Regex auf €/kWh/m² → Abgleich mit JSON] ```
Der LLM erfindet, rechnet oder rundet keine Zahlen. Er befüllt ausschließlich Felder, die unten als Herkunft `llm` markiert sind, und darf darin keine Zahl nennen, die nicht 1:1 aus einem `engine`-Feld stammt. Bei Diskrepanz: Auto-Retry mit härterer Anweisung, sonst Fallback auf Template-Render (statischer Satzbaustein).
1.2 Herkunfts-Klassen pro Feld
| Herkunft | Bedeutung |
|---|---|
| `engine` | Deterministische Regel-/Geo-/Cost-Engine. Reproduzierbar bei gleichem Input. Nie LLM. |
| `llm` | LLM-narrativer Text (Sonnet 4.6 Standard / Opus 4.7 Premium). Keine neuen Zahlen. |
| `static` | Konstante aus Konfig/Recht (Disclaimer-Boilerplate, Lizenz-Strings, Tarif-Mapping). |
| `external` | 1:1 von externer Quelle übernommen (BEV-Geometrie-Ref, HORA-Pass-PDF-Key). |
1.3 Determinismus & Caching
Gleicher `(parcel_id, region, report_tier, engine_version, config_version)` → identische `engine`/`external`-Felder. `llm`-Felder dürfen variieren (Temperatur > 0), müssen aber den Zahlen-Guard bestehen. `meta.as_of` und `meta.generated_at` machen jeden Report stichtagsfähig.
2. Top-Level-Schema
```jsonc { "schema_version": "1.0.0", // SemVer dieses Contracts; static "report_id": "uuid", // engine "meta": { ... }, // §3 "parcel": { ... }, // §4 "steckbrief": [ { ... } ], // Ampel-Übersicht aller Indikatoren (Aggregat; engine) "standort_bilanz": { ... } | null,// Risiko-Rollup aus dem Steckbrief (Szenario-Kontext; engine) "widmung": { ... }, // §5 "risiko": { ... }, // §6 "bestand": { ... }, // §7 "grundbuch": { ... } | null, // §8 — nur Premium, sonst null "scenarios": [ { ... } ], // §9 — 3..5 Elemente "foerderungen": [ { ... } ], // §10 "sources": [ { ... } ], // §11 — Attribution-Pflicht "confidence": { ... }, // §12 "disclaimer": { ... }, // §13 "narrative": { ... } // §14 — die einzigen rein-llm-Blöcke } ```
Pflicht-Top-Level-Keys für alle Tiers: `schema_version`, `report_id`, `meta`, `parcel`, `widmung`, `risiko`, `bestand`, `scenarios`, `foerderungen`, `sources`, `confidence`, `disclaimer`, `narrative`. `grundbuch` ist immer vorhanden, aber `null` außerhalb des Premium-Pfads (siehe §8 und §15).
`steckbrief` ist optional und rein abgeleitet: eine Ampel-Übersicht aller vorhandenen Indikator-Layer (Widmung, Hochwasser, Altlasten, Radon, Lärm, Schutzgebiete, Denkmal, Breitband) für den schnellen Blick oben im Report. Keine eigene Datenquelle, kein Gesamt-Verdikt. Einträge: `{ schluessel, label, ampel: gruen|gelb|rot|unbekannt, kurz }`; feste Reihenfolge, fehlende Layer werden ausgelassen (Lücke steht in `meta.degraded`). Die Altlasten-Ampel wird abgeleitet (intersect → rot, Nähe < 1 km → gelb, sonst grün).
`standort_bilanz` ist optional/null und rein abgeleitet aus dem
`steckbrief`: ein kompakter Risiko-Rollup, den die Szenario-Engine als
Entscheidungs-Kontext über den Nutzungsszenarien ausweist. Form:
`{ rot: [{schluessel,label}], gelb: [{schluessel,label}], gruen_count: int, hinweis: string }`.
Reine Zählung/Auflistung der nicht-grünen Indikatoren — KEIN Gesamt-Verdikt
(eine einzelne „Gesamtampel" wäre über-claimend). null, wenn kein Indikator vorliegt.
3. `meta` — Identität, Stichtag, Tier
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `meta.address.raw` | string | ja | external | Vom User eingegebene Adresse (roh). Wird NICHT an LLM gegeben (PII-Minimierung, ARCHITECTURE §Compliance). |
| `meta.address.normalized` | string | ja | engine | Normalisierte Adresse (Geocoder-Output). |
| `meta.coordinate.lat` | number | ja | engine | WGS84, Geocoding (Nominatim/BEV). |
| `meta.coordinate.lon` | number | ja | engine | WGS84. |
| `meta.coordinate.source` | enum `nominatim`|`bev` | ja | engine | Geocoder. |
| `meta.region.slug` | string | ja | engine | z. B. `at-noe` (ADR-0006 Region-Achse). |
| `meta.region.name` | string | ja | static | „Niederösterreich". |
| `meta.report_tier` | enum `free`|`standard`|`premium`|`planer` | ja | engine | Steuert Sektions-Sichtbarkeit (§15). `free`=anonym, `standard`=registriert, `premium`=490-€-Beratung, `planer`=Abo-Export. |
| `meta.org_id` | int | null | ja | engine | Org-Kontext (RLS, ADR-0006). `null` bei anonym (`_public`-Org). |
| `meta.requested_by` | enum `anonymous`|`user`|`platform_admin_impersonating` | ja | engine | Audit-Hinweis (ADR-0006/0007). |
| `meta.as_of` | ISO-8601 date | ja | engine | Fachlicher Daten-Stichtag (max. Alter aller verwendeten Layer). |
| `meta.generated_at` | ISO-8601 datetime | ja | engine | Generierungs-Zeitpunkt. |
| `meta.engine_version` | string | ja | static | Version der Szenario-/Cost-Engine. |
| `meta.config_version` | string | ja | static | Version `bodenwerte.json` + `herstellungskosten.json` + BPI-Stand. |
| `meta.degraded` | object[] | ja | engine | Liste ausgefallener Layer mit Defensive-Default-Hinweis (ARCHITECTURE §Failure-Modi). Leer wenn alles ok. |
| `meta.timings` | object[] | optional | engine | Diagnostische Layer-Latenzen (Wall-Clock je OGD-Abruf), absteigend sortiert (Flaschenhals zuerst). Nicht deterministisch — kein Teil der fachlichen Aussage, dient dem Erkennen langsamer Endpunkte. |
| `meta.report_id` | string | optional | engine | Permalink-ID, falls der Report persistiert wurde (nur POST, ADR-0022). Fehlt bei fail-open-Persistenzfehler. |
| `meta.permalink` | string | optional | engine | Teilbarer Pfad `/r/{report_id}`, falls persistiert. |
| `meta.saved` | bool | optional | engine | `true`, wenn der Report einem Konto fest zugeordnet ist (eingeloggt erstellt oder per `?claim=1` beansprucht, Migration 0021) → kein 90-Tage-Ablauf. Fehlend/`false` = anonym (90-Tage-Retention) oder nicht persistiert. Steuert „gespeichert"-/Registrierungs-Nudge im UI. |
`meta.degraded[]`-Element: `{ "layer": "geosphere", "reason": "rate_limit", "fallback": "static_aggregate" }`. `meta.timings[]`-Element: `{ "layer": "parcel", "ms": 812 }` (gemessen über die in build.ts konkurrent gestarteten Layer-Calls; der langsamste ≈ Gesamt-Wall-Clock).
4. `parcel` — BEV-Kataster
Quelle: BEV-WFS Live-Call (ARCHITECTURE §Datenfluss 4a), 30-Tage-Cache.
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `parcel.found` | boolean | ja | engine | Ob Adresse einer Parzelle zugeordnet werden konnte. Wenn `false` → nur Teilreport, Banner. |
| `parcel.grundstuecksnummer` | string | wenn `found` | external | BEV-Grundstücksnummer. |
| `parcel.katastralgemeinde` | string | wenn `found` | external | KG-Name + Nummer. |
| `parcel.geometry_ref` | string | wenn `found` | external | Referenz auf gespiegeltes PostGIS-Polygon (keine Roh-Geometrie im JSON; Export lizenzrechtlich kritisch, DATA-SOURCES §BEV). |
| `parcel.area_m2` | number | wenn `found` | engine | Aus Geometrie berechnete Fläche. |
| `parcel.area_tolerance_pct` | number | wenn `found` | static | Toleranzmarge (DKM ≠ Grundbuch), Default 3 %. |
| `parcel.grundstuecksnutzung` | string | optional | external | BEV-Nutzungsart (Bauf./Gärten/…). |
| `parcel.bev_stand` | ISO-8601 date | wenn `found` | external | Stand des BEV-Datensatzes (Cache-Datum). |
5. `widmung` — Flächenwidmung als Ampel
Quelle: NÖGIS Widmung, wöchentlich gespiegelt, `ST_Intersects`.
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `widmung.klasse` | enum `bauland`|`gruenland`|`industrie`|`sonder`|`gemischt`|`unbekannt` | ja | engine | Aggregierte Hauptklasse. |
| `widmung.detail` | string | optional | external | Original-Widmungstext NÖGIS. |
| `widmung.ampel` | enum `gruen`|`gelb`|`rot` | ja | engine | `gruen`=Bauland baureif, `gelb`=eingeschränkt/aufschließungsbedürftig, `rot`=Grünland/Bauverbot. |
| `widmung.ampel_begruendung` | string | ja | engine | Regelbasierter Kurztext (kein LLM), z. B. „Bauland-Wohngebiet, baureif". |
| `widmung.restriktionen` | string[] | ja | engine | Harte Einschränkungen aus Zusatz-Layern (NÖGIS Wasserrecht Schutz-/Schongebiet, Lärminfo-Klasse). Leer = keine. |
| `widmung.rechtsverbindlich` | boolean | ja | static | Immer `false` — nur Indikation, keine rechtsverbindliche Auskunft (DATA-SOURCES §NÖGIS). |
| `widmung.quelle_stand` | ISO-8601 date | ja | engine | Stand der Widmung (Mirror-Sync bzw. LAND_DATUM aus dem Live-Layer). |
| `widmung.source` | enum `dkm_mirror`|`wms` | optional | engine | Herkunft: dkm_mirror = exakte gespiegelte Geometrie (PostGIS); wms = NÖ-weiter Live-Fallback über RRU_WI_HUELLE (WMS-GetFeatureInfo), greift bei Mirror-Lücke/DB-Ausfall — gleiche Klassifikation, keine exakte Fläche. |
6. `risiko` — Naturgefahren, Altlasten, Klima
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `risiko.hochwasser.stufe` | enum `keine`|`niedrig`|`mittel`|`hoch`|`unbekannt` | ja | engine | Pfad B (NÖGIS) implementiert (lib/geo/hochwasser.ts): Stufe aus der häufigsten Abflusszone — HQ30/HQ100 → hoch, HQ300 → niedrig, keine → keine. (HORA-Pfad A bleibt Lizenz-blockiert.) |
| `risiko.hochwasser.hq_zone` | enum `HQ30`|`HQ100`|`HQ300`|null | ja | engine | Häufigste (ungünstigste) Abflusszone, in der der Punkt liegt (NÖ-OGD HYD_HW30/100/300ETC, WMS-GFI). null = keine kartierte Zone. |
| `risiko.hochwasser.jaehrlichkeiten` | number[] | ja | engine | Alle am Punkt vorhandenen Jährlichkeiten, aufsteigend (z. B. [30,100,300]). |
| `risiko.hochwasser.ampel` | enum `gruen`|`gelb`|`rot`|`unbekannt` | ja | engine | HQ30/HQ100 → rot; HQ300 → gelb; keine Zone → grün. |
| `risiko.hochwasser.hinweis` | string | ja | static | Wortgleich mit `disclaimer.bausteine.hochwasser_indikator`. |
| `risiko.hochwasser.quelle_stand` | ISO-8601 date | ja | engine | Abrufdatum (Live WMS-GFI gegen sdi.noe.gv.at). |
| `risiko.hora_pass.r2_key` | string | null | optional | external | R2-Key des HORA-Pass-PDF (ADR-0014). `null` wenn Layer aus = Lizenz nicht geklärt. |
| `risiko.hora_pass.gefahren` | object | ja | external | Map der 10 HORA-Gefahren → Stufe (`keine`/`niedrig`/`mittel`/`hoch`): `hochwasser`, `oberflaechenabfluss`, `lawine`, `erdbeben`, `rutschung`, `windspitzen`, `blitzdichte`, `hagel`, `schneelast`, `hitzeepisoden`. Aus Pfad-A-Parse; leeres Objekt wenn Pass nicht abrufbar. |
| `risiko.altlasten.intersect` | boolean | ja | engine | `ST_Intersects` BEV-Parzelle × ALSAG-Polygone. |
| `risiko.altlasten.flaeche` | string | null | wenn `intersect` | external | Bezeichnung der publizierten Altlast/Sanierungsfläche. |
| `risiko.altlasten.hinweis` | string | ja | static | Fix: „Nicht-Listung garantiert keine Schadstofffreiheit." (DATA-SOURCES §ALSAG, R-03). |
| `risiko.altlasten.hist_nutzung` | object | null | optional | engine | Visueller Indikator-Sub-Block (Issue #135). Keine algorithmische Klassifikation, kein LLM-Bilderverstehen — der Frontend-Layer zeigt nur die Karten. |
| `risiko.altlasten.hist_nutzung.zeitstaende[]` | array | ja | engine | Liste der historischen Zeitstände. Wien-Pilot: 3 (Luftbildplan 1938 / 1956 / Orthofoto heute, alle WMTS `mapsneu.wien.gv.at`). |
| `risiko.altlasten.hist_nutzung.zeitstaende[].slug` | enum `lb1938`|`lb1956`|`heute` | ja | engine | Stabile ID des Zeitstands. |
| `risiko.altlasten.hist_nutzung.zeitstaende[].jahr` | int | ja | engine | Aufnahmejahr. |
| `risiko.altlasten.hist_nutzung.zeitstaende[].quelle` | string | ja | engine | Klartext-Quelle (z. B. „data.wien.gv.at — Luftbildplan 1938"). |
| `risiko.altlasten.hist_nutzung.zeitstaende[].verfuegbar` | boolean | ja | engine | `true` innerhalb Wien-BBox, sonst `false`. |
| `risiko.altlasten.hist_nutzung.zeitstaende[].layer_ref` | string | null | ja | engine | MapLibre-Raster-Tile-URL (`{z}/{y}/{x}`); `null` wenn `verfuegbar=false`. |
| `risiko.altlasten.hist_nutzung.zeitstaende[].attribution` | string | ja | engine | Kurzattribution für Karten-Footer (CC BY 4.0). |
| `risiko.altlasten.hist_nutzung.hinweis` | string | ja | static | Fix: „Historische Karten und Luftbilder werden als visueller Indikator angezeigt. Keine Schadstoffaussage, keine algorithmische Bewertung. Bei Verdachtsmomenten ist eine fachliche Bodenuntersuchung erforderlich." |
| `risiko.altlasten.hist_nutzung.coverage_hinweis` | string | null | ja | engine | Innerhalb Coverage `null`, außerhalb Pilot-Beschränkungs-Hinweis. |
| `risiko.geogen.rutschung` | enum `keine`|`vorbegutachtung`|`erkundung`|`unbekannt` | ja | engine | NÖGIS Geogene Gefahren `ST_Intersects` (Hanglagen). |
| `risiko.radon` | object | null | optional | engine | Radon-Indikator-Block (Issue #132). `null` außerhalb Österreichs oder bei API-Ausfall (→ `degraded.radon`). Gemeinde-Ebene, keine Messung. |
| `risiko.radon.potenzialklasse` | 1 | 2 | 3 | null | ja | engine | ÖNRAP-Klassifikation (1=niedrig, 2=mittel, 3=hoch). |
| `risiko.radon.rnv_status` | enum `kein_schutz`|`vorsorge`|`schutz`|`unbekannt` | ja | engine | RnV-Kategorie nach BGBl. II Nr. 470/2020 (rechtlich verbindlich). |
| `risiko.radon.rnv_gebiet_raw` | string | null | ja | engine | Roh-Text aus der Quelle (audit-tauglich). |
| `risiko.radon.gemeinde` | string | null | ja | engine | Gemeinde-Name. |
| `risiko.radon.bezirk` | string | null | ja | engine | Politischer Bezirk. |
| `risiko.radon.gem_nr` | number | null | ja | engine | 5-stellige Gemeindekennzahl (Statistik Austria). |
| `risiko.radon.ampel` | enum `gruen`|`gelb`|`rot`|`unbekannt` | ja | engine | Engine-Mapping: Klasse 3 ODER Schutzgebiet → rot; Klasse 2 ODER Vorsorgegebiet → gelb; sonst grün. |
| `risiko.radon.hinweis` | string | ja | static | Wortgleich mit `disclaimer.bausteine.radon_indikator_nicht_messung`. |
| `risiko.radon.quelle_stand` | ISO-8601 date | ja | engine | Abrufdatum (Live-API gegen LFRZ). |
| `solar` | object | null | optional | engine | Standortgenaues Solarpotenzial (ADR-0037). `null` bei PVGIS-Ausfall → PV-Szenario nutzt die Konfig-Pauschale. |
| `solar.spez_ertrag_kwh_pro_kwp` | number | ja | external | PVGIS spez. Jahresertrag [kWh/kWp] bei optimaler Ausrichtung (AC); Engine-Override der Pauschale `1140`. |
| `solar.einstrahlung_kwh_m2` / `solar.optimale_neigung_grad` / `solar.optimaler_azimut_grad` | number | ja | external | Einstrahlung [kWh/m²] + von PVGIS bestimmte optimale Neigung/Azimut (0 = Süd). |
| `solar.quelle` / `solar.quelle_stand` | string | ja | external | Strahlungs-DB (z. B. `PVGIS-SARAH3`) + Abrufdatum. |
| `solar.google` | object | null | optional (Premium) | external | Dachgenaue Google-Anreicherung (Building Insights). DC-Werte, Live-Overlay — vor Persistenz entfernt (`stripGoogleSolar`, ToS R-07); auf Permalinks abwesend. `null` = keine Coverage / Free-Tier. |
| `solar.google.max_kwp` / `.max_panels` / `.roof_segments` | number | wenn `google` | external | Größte berechnete Belegung: kWp, Panel-Anzahl, Dachflächen. |
| `solar.google.max_yearly_kwh_dc` / `.implied_spez_kwh_pro_kwp_dc` | number | wenn `google` | external | Jahresertrag (DC) + abgeleiteter spez. Ertrag (DC, ≠ PVGIS-AC). |
| `solar.google.imagery_quality` / `.imagery_date` / `.region_code` | string | wenn `google` | external | Luftbild-Qualität (HIGH/MEDIUM/BASE) + Datum + ISO-Regioncode. |
| `erreichbarkeit` | object | null | optional | engine | Erreichbarkeits-Score (#140, ADR-0036). Pro POI-Klasse der nächste kuratierte POI + Distanz + Fahrzeit. Env-gated: ohne `OSRM_BASE_URL` Luftlinien-Schätzung (`quelle: "luftlinie"`, Phase 2.0); mit gesetztem Backend echte Auto-Fahrzeit via OSRM `/table` (`quelle: "osrm"`, Phase 2.5), fail-open zurück auf Luftlinie bei Fehler/Timeout. |
| `erreichbarkeit.eintraege[]` | array | ja | engine | 4 Einträge (autobahn, bahnhof, spital, schule), je mit Pfad zum nächsten kuratierten POI. |
| `erreichbarkeit.eintraege[].klasse` | enum `autobahn`|`bahnhof`|`spital`|`schule` | ja | engine | POI-Kategorie. |
| `erreichbarkeit.eintraege[].label` | string | ja | static | Anzeige-Label (DE). |
| `erreichbarkeit.eintraege[].poi_id` | string | null | ja | engine | Stabile POI-ID aus `lib/maps/poi-at.ts`. |
| `erreichbarkeit.eintraege[].poi_name` | string | null | ja | engine | Klartext-Name des nächsten POI. |
| `erreichbarkeit.eintraege[].distanz_km` | number | null | ja | engine | Distanz in km (1 Nachkommastelle). Bei `quelle: "osrm"` Straßendistanz, sonst Luftlinie. |
| `erreichbarkeit.eintraege[].fahrzeit_min` | number | null | ja | engine | Fahrzeit in Minuten. Bei `quelle: "osrm"` echte Routing-Fahrzeit, sonst Schätzung (Luftlinie × 1.35 / 50 km/h). |
| `erreichbarkeit.eintraege[].quelle` | enum `luftlinie`|`osrm`|`ors` | ja | engine | Methode: `luftlinie` ohne Routing-Backend (Default), `osrm` mit gesetztem `OSRM_BASE_URL`. Pro Klasse kann einzeln auf `luftlinie` zurückfallen, wenn OSRM den POI nicht erreicht. |
| `erreichbarkeit.hinweis` | string | ja | static | Pflicht-Hinweis: „Indikator-Schätzung … keine echte Routing-Fahrzeit". |
| `erreichbarkeit.coverage_hinweis` | string | ja | static | Hinweis auf kuratiertes POI-Set (Pilot-Phase). |
| `breitband` | object | null | optional | engine | Breitband-Versorgungs-Indikator (RTR/Breitbandatlas, Festnetz). `null` außerhalb versorgter Zellen/Österreichs oder bei API-Ausfall (→ `degraded.breitband`). Summe aller Infrastruktur-Anbieter, keine Anschlusszusage. Live via WMTS-GetFeatureInfo (REST-API per 30.06.2025 eingestellt). |
| `breitband.max_download_mbit` | number | null | ja | engine | Max. Download-Rate der 100×100-m-Rasterzelle (Mbit/s; `0` = „< 1"). |
| `breitband.klasse` | string | null | ja | engine | Klassen-Freitext der Quelle (z. B. „≥ 1 Gbit/s"). |
| `breitband.ampel` | enum `gruen`|`gelb`|`rot`|`unbekannt` | ja | engine | Engine-Mapping: ≥ 100 → grün; ≥ 30 → gelb; < 30 → rot; kein Wert → unbekannt. |
| `breitband.hinweis` | string | ja | static | Wortgleich mit `disclaimer.bausteine.breitband_indikator`. |
| `breitband.quelle_stand` | ISO-8601 date | ja | engine | Abrufdatum (Live WMTS-GFI gegen `breitbandatlas.gv.at`). |
| `laerm` | object | null | optional | engine | Umgebungslärm-Indikator (strategische Lärmkarten END 2002/49/EG, laerminfo.at). `null` nur bei API-Ausfall (→ `degraded.laerm`); „keine Zone" ist ein gültiges Ergebnis (Ampel grün). Nur Hauptquellen Straße + Schiene oberhalb der EU-Meldeschwellen, kein lokaler Lärm, berechnet nicht gemessen. Live via WMS-GetFeatureInfo. |
| `laerm.strasse_lden_db` | number | null | ja | engine | Untergrenze des Straßen-Lden-Bandes in dB (z. B. 65 für 65–69); `null` = keine Zone. |
| `laerm.strasse_lden_klasse` | string | null | ja | engine | Roh-Klassenbezeichner der Quelle (z. B. „Lden6569"). |
| `laerm.strasse_lnight_db` | number | null | ja | engine | Untergrenze des Straßen-Lnight-Bandes (Nacht). |
| `laerm.strasse_lnight_klasse` | string | null | ja | engine | Roh-Klassenbezeichner Straße Nacht. |
| `laerm.schiene_lden_db` | number | null | ja | engine | Untergrenze des Schienen-Lden-Bandes. |
| `laerm.schiene_lden_klasse` | string | null | ja | engine | Roh-Klassenbezeichner Schiene Tag-Abend-Nacht. |
| `laerm.schiene_lnight_db` | number | null | ja | engine | Untergrenze des Schienen-Lnight-Bandes (Nacht). |
| `laerm.schiene_lnight_klasse` | string | null | ja | engine | Roh-Klassenbezeichner Schiene Nacht. |
| `laerm.max_lden_db` | number | null | ja | engine | Höchste Lden-Untergrenze über Straße + Schiene (Headline); `null` = keine Zone. |
| `laerm.ampel` | enum `gruen`|`gelb`|`rot`|`unbekannt` | ja | engine | Engine-Mapping: Lden ≥ 70 ODER Lnight ≥ 60 → rot; sonst kartierte Zone → gelb; keine Zone → grün. |
| `laerm.hinweis` | string | ja | static | Wortgleich mit `disclaimer.bausteine.laerm_indikator`. |
| `laerm.quelle_stand` | ISO-8601 date | ja | engine | Abrufdatum (Live WMS-GFI gegen `inspire.lfrz.gv.at`). |
| `oev` | object | null | optional | engine | ÖV-Güteklassen-Indikator (ÖROK/BMIMI-Modell, #101 Phase 2). Nur PostGIS-Mirror (Pilot-bbox, jährliche Datei-Lieferung über AustriaTech — kein amtlicher WMS/WFS): außerhalb der Spiegelung `null` (→ `degraded.oev`, reason `ausserhalb_pilot_spiegelung`). „Keine Güteklasse" am Punkt ist ein gültiges Ergebnis (Ampel rot). |
| `oev.gueteklasse` | string | null | ja | engine | Güteklasse `A` (beste) … `G`; `null` = keine Klasse am Punkt. Klassen-Polygone überlappen — die beste Klasse gewinnt. |
| `oev.ampel` | enum `gruen`|`gelb`|`rot` | ja | engine | Deterministisch (`classifyOev`): A–C → grün, D–G → gelb, keine Klasse → rot. Schwellen sind Magnus-Review-Punkt (Briefing). |
| `oev.ampel_begruendung` | string | ja | engine | Klartext-Begründung der Ampel (deterministisch, kein LLM). |
| `oev.hinweis` | string | ja | static | Wortgleich mit `disclaimer.bausteine.oev_indikator`. |
| `oev.quelle_stand` | ISO-8601 date | null | ja | engine | Fahrplan-Referenzdatum der Lieferung (aus der Spiegelung; nie das Report-Datum). |
| `oev.source` | const `mirror` | ja | engine | Herkunfts-Transparenz (analog `parcel.source`). |
| `schutzgebiete` | object | null | optional | engine | Schutzgebiete-Indikator (NÖ-OGD Natur- + Wasserschutz). null außerhalb NÖ oder bei API-Ausfall (→ `degraded.schutzgebiete`). „Kein Treffer" ist ein gültiges Ergebnis (grün). Keine Rechtsauskunft. Live via WMS-GetFeatureInfo. |
| `schutzgebiete.treffer[]` | array | ja | engine | Alle zutreffenden Designationen (überlappende möglich). |
| `schutzgebiete.treffer[].kategorie` | string | ja | engine | Stabile ID (z. B. naturschutzgebiet, europaschutz_ffh, landschaftsschutzgebiet, nationalpark, naturpark, wasserschutzgebiet, wasserschongebiet). |
| `schutzgebiete.treffer[].label` | string | ja | engine | Anzeige-Label (DE). |
| `schutzgebiete.treffer[].name` | string | null | ja | engine | Gebietsname aus der Quelle. |
| `schutzgebiete.treffer[].schwere` | enum `gelb`|`rot` | ja | engine | rot = streng (Naturschutz-/Europaschutz-/Nationalpark-/Wasserschutzgebiet); gelb = milder (Landschaftsschutz/Naturpark/Wasserschongebiet). |
| `schutzgebiete.ampel` | enum `gruen`|`gelb`|`rot`|`unbekannt` | ja | engine | Ungünstigste Schwere über alle Treffer; grün wenn keiner. |
| `schutzgebiete.hinweis` | string | ja | static | Wortgleich mit `disclaimer.bausteine.schutzgebiete_indikator`. |
| `schutzgebiete.quelle_stand` | ISO-8601 date | ja | engine | Abrufdatum (Live WMS-GFI gegen sdi.noe.gv.at). |
| `denkmal` | object | null | optional | engine | Denkmalschutz (BDA-Denkmalverzeichnis §3 DMSG, NÖ). null, wenn keine Parzelle (KG+GstNr) aufgelöst wurde. Parzellenscharfer Match gegen vorgenerierte Liste (kein Netz/DB). Keine Rechtsauskunft; Ensembles/Umgebungsschutz nicht erfasst. |
| `denkmal.gelistet` | boolean | ja | engine | Liegt das Grundstück (KG-Code + GstNr) direkt im Denkmalverzeichnis? |
| `denkmal.objekte[]` | array | ja | engine | Auf der Parzelle gelistete Objekte (titel, schutzart). |
| `denkmal.gemeinde` | string | null | ja | engine | Gemeinde (aus KG-Code abgeleitet). |
| `denkmal.gemeinde_count` | number | null | ja | engine | Anzahl denkmalgeschützter Objekte in der Gemeinde (Kontext). |
| `denkmal.ampel` | enum `gruen`|`gelb`|`rot`|`unbekannt` | ja | engine | gelistet → rot; sonst grün. |
| `denkmal.hinweis` | string | ja | static | Wortgleich mit `disclaimer.bausteine.denkmal_indikator`. |
| `denkmal.quelle_stand` | ISO-8601 date | ja | engine | Abrufdatum (Lookup gegen die committete BDA-Liste). |
| `wlv` | object | null | optional | engine | Gefahrenzonenplan Wildbach & Lawine (WLV/BML, LFRZ WMS-GFI). null bei API-Ausfall (→ `degraded.wlv`); „keine Zone" ist ein gültiges Ergebnis (grün). Nur Gemeinden mit Plan (alpiner Raum). Keine rechtsverbindliche Zonen-Auskunft. |
| `wlv.zonen[]` | string[] | ja | engine | Distinkte Objektart-Werte am Punkt (z. B. „Rote Zone Wildbach", „Summenzone - Gelbe Zone Wildbach", „Raumrelevanter Bereich"). |
| `wlv.ampel` | enum `gruen`|`gelb`|`rot` | ja | engine | Rote Zone → rot; sonst Treffer → gelb; keine Zone → grün. |
| `wlv.gemeinde` | string | null | ja | engine | Hauptgemeindename des Gefahrenzonenplans. |
| `wlv.revisionsjahr` | number | null | ja | engine | Revisionsjahr des Plans (Daten-Vintage). |
| `wlv.hinweis` | string | ja | static | Wortgleich mit `disclaimer.bausteine.wlv_indikator`. |
| `wlv.quelle_stand` | ISO-8601 date | ja | engine | Abrufdatum (Live WMS-GFI gegen inspire.lfrz.gv.at/000901). |
| `risiko.klima.jahresmittel_temp_c` | number | ja | engine | GeoSphere Jahresmittel-Temperatur (Redis-Cache pro 1×1-km-Zelle / Live). |
| `risiko.klima.jahresniederschlag_mm` | number | ja | engine | GeoSphere Jahressumme Niederschlag. |
| `risiko.klima.sonnenstunden_jahr` | number | ja | engine | GeoSphere/Strahlung — Basis PV-Szenario. |
| `risiko.klima.referenzjahr` | int | ja | engine | Aggregations-Jahr (z. B. 2024). |
| `risiko.klima.quelle_stand` | ISO-8601 date | ja | engine | Cache-/Sync-Datum. |
7. `bestand` — Gebäude-Footprint
Quelle: Microsoft Building Footprints (Bulk, statisch) + optional Copernicus Versiegelung / Sentinel-2 NDVI (Phase 2).
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `bestand.bebaut` | boolean | ja | engine | Ob ein Footprint die Parzelle schneidet. |
| `bestand.gebaeude_anzahl` | int | ja | engine | Anzahl geschnittener Footprints. |
| `bestand.footprint_flaeche_m2` | number | ja | engine | Summe Footprint-Fläche auf der Parzelle. |
| `bestand.versiegelungsgrad_pct` | number | null | optional | engine | Copernicus (Phase 2), sonst `null`. |
| `bestand.ndvi_index` | number | null | optional | engine | Sentinel-2 „überwuchert?" (Phase 2), sonst `null`. |
| `bestand.footprint_stand` | ISO-8601 date | ja | engine | Datensatz-Stand (statischer Bulk). |
8. `grundbuch` — nur Premium (ADR-0016)
`null` für `free`/`standard`. Nur befüllt im `premium`-Pfad nach explizitem User-Trigger („Eigentumsverhältnisse abfragen") oder durch Magnus' Beratungsvorbereitung. Alle Personen pseudonymisiert — Klarnamen nie im Report-JSON, nie an LLM (ADR-0016 §Pseudonymisierung).
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `grundbuch.bezirksgericht` | string | ja | external | BG aus JustizOnline-Auszug. |
| `grundbuch.katastralgemeinde` | string | ja | external | KG-Code + Name. |
| `grundbuch.einlagezahl` | string | ja | external | EZ. |
| `grundbuch.as_of` | ISO-8601 datetime | ja | external | Stichtag des Auszugs (max. 90 Tage Cache, ADR-0016). |
| `grundbuch.eigentum.art` | string | ja | external | z. B. „Schlichtes Miteigentum". |
| `grundbuch.eigentum.personen[]` | object[] | ja | engine | `{ anteil, name:"[PSEUDONYMISIERT]", wohnort_grob }` — `wohnort_grob` nur Land/Bundesland-Granularität. |
| `grundbuch.eigentum.aktivierungs_komplexitaet` | enum `gering`|`mittel`|`hoch` | ja | engine | Aus B-Blatt abgeleitet. |
| `grundbuch.belastungen[]` | object[] | ja | engine | `{ art, berechtigter:"[PSEUDONYMISIERT]", impact }` — C-Blatt. |
| `grundbuch.dienstbarkeiten_zugunsten[]` | object[] | ja | external | C-Blatt, GSt-Referenzen (keine Personen). |
| `grundbuch.dienstbarkeiten_zulasten[]` | object[] | ja | external | C-Blatt. |
| `grundbuch.score.aktivierungs_faehigkeit` | enum `einfach`|`mittel`|`schwierig`|`sehr_schwierig` | ja | engine | Deterministische Score-Tabelle (ADR-0016, Magnus-kalibrierte Gewichte). Kein LLM. |
| `grundbuch.score.punktwert` | number | ja | engine | Roh-Score (Bandbreiten: +5..+3 einfach … <−3 sehr_schwierig). |
| `grundbuch.score.kommentar` | string | ja | engine | Regelbasierter Kurztext (z. B. realistischer Vorbereitungszeitraum). |
| `grundbuch.original_r2_key` | string | ja | external | R2-Key des Original-PDF (Zugriff nur Auftraggeber, separater Auth-Check). |
| `grundbuch.disclaimer` | string | ja | static | „Stand: {as_of}. Spätere Änderungen können bestehen. Verbindliche Auskunft nur via aktuellem Auszug." |
Offene Frage / Annahme: ADR-0016 fragt, ob eine eigene Rolle „Auftraggeber-mit-Namenseinsicht" nötig ist. Annahme dieses Specs: Klarnamen werden NIE in den Report-JSON-Contract aufgenommen; ein separater, nicht hier spezifizierter Endpoint liefert das Original-PDF an den authentifizierten Auftraggeber. Der Report bleibt damit durchgehend pseudonym → für Magnus' Workshop zu bestätigen.
9. `scenarios[]` — 3 bis 5 Nutzungsszenarien
Mindestens 3, maximal 5 Elemente (PRODUCT §User-Flow Punkt 3). Die Ampel-Bewertung und der gesamte `cost`-Block sind deterministisch (ADR-0015). Der `cost`-Block übernimmt das Schema aus ADR-0015 exakt und unverändert (`scenario`, `cost`, `cost_components`, `methodology`, `as_of_date`, `disclaimer`).
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `scenario_id` | string | ja | engine | Stabiler Slug, z. B. `pv-freiflaeche`. |
| `nutzungsklasse` | enum `wohnen`|`gewerbe`|`pv_anlage`|`co_working`|`lager`|`tourismus` | ja | engine | Nutzungsklasse (ADR-0015 Säule 3). |
| `titel` | string | ja | engine | Anzeige-Titel (Konfig-Template, kein LLM). |
| `ampel` | enum `gruen`|`gelb`|`rot` | ja | engine | Eignung: harte Ausschlusskriterien (rot) / weiche Faktoren (gelb/grün). |
| `ampel_faktoren` | object[] | ja | engine | `{ faktor, bewertung:"+/-/0", quelle_ref }` — nachvollziehbare Regel-Treffer (Widmung, Fläche, Altlast sowie die Risiko-Layer Hochwasser HQ30/HQ100, Denkmalschutz, strenge Schutzgebiete → je -, stufen grün auf gelb). |
| `ausschluss_grund` | string | null | wenn `rot` | engine | Harte Mindestanforderung verletzt (z. B. Mindestfläche, Schutzgebiet). |
| `kennzahlen` | object | optional | engine | Szenario-spezifische deterministische Kennzahlen. PV-Beispiel: `{ "kwp": 20, "ertrag_kwh_jahr": 22800, "ertrag_eur_jahr": 6100, "strompreis_ct_kwh": 26.8 }`. Alle aus Engine, nie LLM. |
| `cost` | object | ja | engine | Exakt ADR-0015-Schema: `{ low, expected, high, unit:"EUR", bandwidth_pct }`. |
| `cost_components` | object[] | ja | engine | Exakt ADR-0015: `[{ label, amount, source }]` — `source` zitiert Tabelle/Faktor. |
| `methodology` | string | ja | static | Exakt ADR-0015: „ÖNORM B 1802-1:2022 + Heft 3/2025 + BPI Q-aktuell". |
| `as_of_date` | ISO-8601 date | ja | engine | ADR-0015 — Cost-Stichtag (Config-Stand). |
| `disclaimer` | string | ja | static | ADR-0015 — „Grob-Kostenschätzung, ersetzt keine Detailkalkulation durch Sachverständigen/Planer". |
| `foerder_trigger` | string[] | ja | engine | IDs förderfähiger Bausteine → join auf `foerderungen[]` (z. B. `kpc-flaechenrecycling`, `kpc-waermepumpe`). |
| `narrativ` | string | nur `standard`+ | llm | 1–3 Absätze Erklärung. Darf nur Zahlen aus `cost`/`kennzahlen` 1:1 zitieren. Im `free`-Tier weggelassen oder gekürzt (§15). |
Offene Frage / Annahme: ADR-0015 listet Nutzungsklassen Wohnen/Gewerbe/PV/Co-Working/Lager/Tourismus, die exakten harten Mindestanforderungen + Cost-Bausteine pro Klasse liefert Magnus' POC- Woche-3-Workshop. Dieser Spec fixiert nur Struktur, nicht die Werte → Workshop-Input erforderlich.
10. `foerderungen[]` — RAG-Matching
Quelle: Förder-RAG (KPC/LEADER/KMU.DIGITAL/aws-Richtlinien als PDFs in pgvector). Das Matching (welche Förderung passt) ist `classification`-LLM (gpt-4o-mini/gemini-flash) mit striktem Tool-Output — Beträge/Sätze sind NICHT generiert, sondern aus dem kuratierten Förder-Katalog (Konfig) gezogen.
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `foerder_id` | string | ja | engine | Stabiler Slug, referenziert von `scenarios[].foerder_trigger`. |
| `programm` | string | ja | static | z. B. „KPC Flächenrecycling". |
| `traeger` | string | ja | static | KPC / LEADER / KMU.DIGITAL / aws. |
| `max_betrag_eur` | number | null | ja | static | Aus Förder-Katalog (z. B. 60000). Nie LLM. |
| `max_satz_pct` | number | null | ja | static | z. B. 75. |
| `bedingung` | string | ja | static | Förderbedingung aus Katalog. |
| `match_score` | number | ja | engine | RAG-Relevanz 0..1. |
| `match_begruendung` | string | nur `standard`+ | llm | Warum passend — nur qualitativ, keine neuen Zahlen. |
| `quelle` | string | ja | static | Richtlinien-Dokument + Stand (quartalsweise validiert). |
11. `sources[]` — Attribution-Pflicht (POC-Kriterium)
Pro tatsächlich verwendetem Layer genau ein Eintrag. Erfüllt das POC-Akzeptanzkriterium „Quellen-Block … Datenquelle + Lizenz + Stand" und die Attribution-Pflicht aus DATA-SOURCES §Attribution-Maintenance.
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `source_id` | string | ja | static | z. B. `bev`, `noegis-widmung`, `alsag`, `geosphere`, `ms-footprints`, `hora-pass`, `grundbuch`. |
| `name` | string | ja | static | Anzeigename. |
| `lizenz` | string | ja | static | z. B. „CC BY 4.0", „ODbL", „Standardnutzungsrechte". |
| `attribution_text` | string | ja | static | Exakt anzuzeigender String (z. B. „Kartendaten © basemap.at, CC BY 4.0"). |
| `stand` | ISO-8601 date | ja | engine | Letzter erfolgreicher Sync/Abruf dieses Layers. |
| `verwendet_fuer` | string[] | ja | engine | Welche Report-Sektionen den Layer nutzen. |
Ein Report muss ≥ 4 `sources` (BEV, NÖGIS-Widmung, GeoSphere, ALSAG) enthalten, sonst gilt das POC-Mindest-Datenlayer-Kriterium als verfehlt → `confidence.poc_min_layers_met = false`.
12. `confidence` — Unsicherheitshinweise
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `confidence.overall` | enum `hoch`|`mittel`|`niedrig` | ja | engine | Aggregierte Konfidenz (fehlende Layer / Cache-Alter / `degraded`). |
| `confidence.notes` | string[] | ja | engine | Konkrete Hinweise (z. B. „Klima-Aggregat 5 Tage alt", „HORA-Layer aus — Lizenz nicht geklärt"). |
| `confidence.poc_min_layers_met` | boolean | ja | engine | ≥ 4 Pflicht-Datenlayer sichtbar verknüpft (POC-Kriterium A). |
| `confidence.cost_bandwidth_default_pct` | number | ja | static | Standard-Unsicherheit Cost (±20 %, ADR-0015 Säule 1). |
13. `disclaimer` — Haftung & Opt-in
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `disclaimer.text` | string | ja | static | Voller Haftungsausschluss-Text (ADR-0007/POC-Kriterium B). |
| `disclaimer.opt_in_required` | boolean | ja | static | Immer `true` — Checkbox vor Report-Anzeige (POC-Kriterium B). |
| `disclaimer.opt_in_recorded_at` | ISO-8601 datetime | null | ja | engine | Wann der User die Checkbox bestätigt hat (Audit). `null` = noch nicht bestätigt → Report bleibt verdeckt. |
| `disclaimer.bausteine` | object | ja | static | Quellen-spezifische Pflicht-Hinweise: `widmung_nicht_rechtsverbindlich`, `alsag_keine_freiheitsgarantie`, `bev_keine_vermessung`, `cost_keine_sachverstaendigenbewertung`, `radon_indikator_nicht_messung` (nur wenn `risiko.radon` gesetzt), `breitband_indikator` (nur wenn `breitband` gesetzt), `laerm_indikator` (nur wenn `laerm` gesetzt), `oev_indikator` (nur wenn `oev` gesetzt), `hochwasser_indikator` (nur wenn `risiko.hochwasser` gesetzt), `schutzgebiete_indikator` (nur wenn `schutzgebiete` gesetzt), `denkmal_indikator` (nur wenn `denkmal` gesetzt), `wlv_indikator` (nur wenn `wlv` gesetzt), `grundbuch_stichtag` (nur Premium). |
14. `narrative` — die einzigen reinen LLM-Blöcke
| Feld | Typ | Pflicht | Herkunft | Beschreibung |
|---|---|---|---|---|
| `narrative.summary` | string | nur `standard`+ | llm | Executive-Summary (3–5 Sätze). Keine Zahl, die nicht in `engine`-Feldern steht. |
| `narrative.empfehlung` | string | nur `premium`/`planer` | llm | Handlungsempfehlung (Opus 4.7). |
| `narrative.modell` | string | ja | engine | Welches Modell den Narrativ erzeugt hat (`anthropic/claude-sonnet-4.6` etc.). Audit/Cost. |
| `narrative.guard_passed` | boolean | ja | engine | Zahlen-Konsistenz-Guard bestanden (ADR-0015). `false` → Fallback-Template wurde gerendert. |
Im `free`-Tier ist `narrative.summary` ein gekürzter Template-Satz (kein LLM-Call → Cost-Cap, POC LLM-Sub-Ziel < $0,10/Report), `narrative.modell = "template"`.
15. Free/anonym vs. Premium 490 € — Sektions-Matrix
| Sektion | `free` (anonym) | `standard` (registriert) | `premium` (490 €) | `planer` (Abo) |
|---|---|---|---|---|
| `meta`, `parcel`, `widmung` | ✅ | ✅ | ✅ | ✅ |
| `risiko` (Klima/ALSAG/Hochwasser-Stufe) | ✅ Kurz | ✅ Voll | ✅ Voll | ✅ Voll |
| `risiko.hora_pass.r2_key` (PDF-Beilage) | ❌ | ❌ | ✅ | ✅ |
| `bestand` | ✅ Basis | ✅ | ✅ + Versiegelung/NDVI | ✅ + Versiegelung/NDVI |
| `scenarios[]` | 3, ohne `narrativ` | 3–5 + `narrativ` | 3–5 + `narrativ` + Empfehlung | 3–5, tabellarisch |
| `scenarios[].cost_components` | nur Summe | ✅ Detail | ✅ Detail | ✅ Detail |
| `foerderungen[]` | Top 1, ohne Begründung | ✅ + Begründung | ✅ + Begründung | ✅ |
| `grundbuch` | `null` | `null` | ✅ (nach Trigger) | `null` (es sei denn explizit) |
| `narrative.summary` | Template | LLM (Sonnet) | LLM (Opus) | LLM (Sonnet) |
| `narrative.empfehlung` | ❌ | ❌ | ✅ (Opus) | ❌ |
| `sources`, `confidence`, `disclaimer` | ✅ | ✅ | ✅ | ✅ |
Free-Tier zusätzlich: Rate-Limit 10 Reports/Tag/IP (POC-Kriterium B). Tier-Bestimmung erfolgt server-seitig aus Auth-Kontext (ADR-0006), nie client-seitig — Feld-Stripping passiert im Backend, bevor das JSON den Server verlässt.
16. PDF-Mapping (ADR-0013)
| Tier | PDF-Pfad | Tool | Sektionen im PDF |
|---|---|---|---|
| `free` / `standard` | Standard-Report-PDF | `@react-pdf/renderer` | `meta`, Karte (pre-rendered PNG), `parcel`, `widmung`, `risiko` (kompakt), `bestand`, `scenarios` (Karten-Layout, Cost-Summe), Top-`foerderungen`, `sources`, `disclaimer`. Map als `(parcel_id, zoom, layer_preset)`-PNG-Cache. |
| `planer` | Planer-Export-PDF | `@react-pdf/renderer` | Wie Standard, aber tabellen-lastig, alle `cost_components`, weniger Narrativ, hohe Volumina. |
| `premium` (490 €) | Beratungs-Report-PDF | Puppeteer (Fluid Compute) | Alle Sektionen inkl. `grundbuch`, `narrative.empfehlung`, volle `cost_components`, interaktiv gerenderte MapLibre-Karte. HORA-Pass-PDF (`risiko.hora_pass.r2_key`) als letzte 2 Seiten angehängt (ADR-0014 Variante α — Attribution via BMLUK-Footer automatisch erfüllt). |
Felder mit Herkunft `external`/`engine` werden in beide Templates 1:1 übernommen; `llm`-Felder nur wenn `narrative.guard_passed = true`, sonst Template-Fallback.
17. Vollständiges Beispiel-JSON (fiktive NÖ-Adresse, `premium`)
Fiktive Liegenschaft, Pilotregion NÖ. Enthält das POC-PV-Szenario mit konkreter Euro-Zahl.
```json { "schema_version": "1.0.0", "report_id": "9f2c1e7a-0b44-4d21-9c3e-7a1b6f0e2d55", "meta": { "address": { "raw": "Hauptstraße 12, 2230 Gänserndorf", "normalized": "Hauptstraße 12, 2230 Gänserndorf, Niederösterreich, AT" }, "coordinate": { "lat": 48.3417, "lon": 16.7192, "source": "bev" }, "region": { "slug": "at-noe", "name": "Niederösterreich" }, "report_tier": "premium", "org_id": 42, "requested_by": "user", "as_of": "2026-05-16", "generated_at": "2026-05-16T09:14:02Z", "engine_version": "0.3.0", "config_version": "bodenwerte@2025.4+herstellungskosten@heft3-2025+bpi@2025Q2", "degraded": [] }, "parcel": { "found": true, "grundstuecksnummer": "1247/3", "katastralgemeinde": "08221 Gänserndorf", "geometry_ref": "postgis:parcels/at-noe/08221/1247-3", "area_m2": 1840, "area_tolerance_pct": 3, "grundstuecksnutzung": "Baufläche begrünt", "bev_stand": "2026-04-28" }, "widmung": { "klasse": "bauland", "detail": "Bauland-Wohngebiet (BW)", "ampel": "gruen", "ampel_begruendung": "Bauland-Wohngebiet, baureif, keine harten Restriktionen", "restriktionen": [], "rechtsverbindlich": false, "quelle_stand": "2026-05-12" }, "risiko": { "hochwasser": { "stufe": "niedrig", "hq_zone": null }, "hora_pass": { "r2_key": "hora-pass/48.3417-16.7192.pdf", "gefahren": { "hochwasser": "niedrig", "oberflaechenabfluss": "niedrig", "lawine": "keine", "erdbeben": "mittel", "rutschung": "keine", "windspitzen": "mittel", "blitzdichte": "niedrig", "hagel": "mittel", "schneelast": "niedrig", "hitzeepisoden": "mittel" } }, "altlasten": { "intersect": false, "flaeche": null, "hinweis": "Nicht-Listung garantiert keine Schadstofffreiheit." }, "geogen": { "rutschung": "keine" }, "klima": { "jahresmittel_temp_c": 10.4, "jahresniederschlag_mm": 548, "sonnenstunden_jahr": 1995, "referenzjahr": 2024, "quelle_stand": "2026-05-11" } }, "bestand": { "bebaut": true, "gebaeude_anzahl": 1, "footprint_flaeche_m2": 142, "versiegelungsgrad_pct": 31, "ndvi_index": 0.42, "footprint_stand": "2025-09-01" }, "grundbuch": { "bezirksgericht": "Gänserndorf", "katastralgemeinde": "08221 Gänserndorf", "einlagezahl": "612", "as_of": "2026-04-02T07:51:10Z", "eigentum": { "art": "Schlichtes Miteigentum", "personen": [ { "anteil": "1/2", "name": "[PSEUDONYMISIERT]", "wohnort_grob": "AT, NÖ" }, { "anteil": "1/2", "name": "[PSEUDONYMISIERT]", "wohnort_grob": "AT, Wien" } ], "aktivierungs_komplexitaet": "mittel" }, "belastungen": [ { "art": "Hypothek", "berechtigter": "[PSEUDONYMISIERT]", "impact": "lösbar mit Refinanzierung" } ], "dienstbarkeiten_zugunsten": [], "dienstbarkeiten_zulasten": [ { "art": "Wegerecht", "ueber": "GSt 1247/3", "zugunsten": "GSt 1247/4" } ], "score": { "aktivierungs_faehigkeit": "mittel", "punktwert": 1, "kommentar": "Zwei Miteigentümer, eine lösbare Hypothek. Aktivierung mit überschaubarem Koordinationsaufwand realistisch, Vorbereitung 3–6 Monate." }, "original_r2_key": "grundbuch/3f9a...c1.pdf", "disclaimer": "Stand: 2026-04-02. Spätere Änderungen können bestehen. Verbindliche Auskunft nur via aktuellem Auszug." }, "scenarios": [ { "scenario_id": "pv-freiflaeche", "nutzungsklasse": "pv_anlage", "titel": "PV-Freiflächen-Anlage 20 kWp", "ampel": "gruen", "ampel_faktoren": [ { "faktor": "Sonnenstunden", "bewertung": "+", "quelle_ref": "geosphere" }, { "faktor": "Keine Beschattung Bestand", "bewertung": "+", "quelle_ref": "ms-footprints" }, { "faktor": "Widmung Bauland", "bewertung": "0", "quelle_ref": "noegis-widmung" } ], "ausschluss_grund": null, "kennzahlen": { "kwp": 20, "ertrag_kwh_jahr": 22800, "ertrag_eur_jahr": 6100, "strompreis_ct_kwh": 26.8 }, "cost": { "low": 26000, "expected": 30000, "high": 34500, "unit": "EUR", "bandwidth_pct": 20 }, "cost_components": [ { "label": "PV-Module + Wechselrichter (20 kWp)", "amount": 26000, "source": "Konfig: PV 1.300 EUR/kWp installiert (ADR-0015 Säule 3)" }, { "label": "Montage Freifläche + Unterkonstruktion", "amount": 3000, "source": "Konfig: Freiflächen-Aufschlag" }, { "label": "Netzanschluss + Zählertausch", "amount": 1000, "source": "Konfig: Standard-Netzanschluss NÖ" } ], "methodology": "ÖNORM B 1802-1:2022 + Heft 3/2025 + BPI Q-aktuell", "as_of_date": "2026-05-14", "disclaimer": "Grob-Kostenschätzung, ersetzt keine Detailkalkulation durch Sachverständigen/Planer", "foerder_trigger": ["kpc-pv"], "narrativ": "Die Parzelle eignet sich mit rund 1.995 Sonnenstunden im Jahr gut für eine PV-Freiflächen-Anlage. Eine 20-kWp-Anlage liefert rechnerisch etwa 22.800 kWh pro Jahr, was bei aktuellem Strompreis rund 6.100 EUR jährlichem Ertrag entspricht. Die Investitionskosten liegen bei etwa 30.000 EUR (±20 %)." }, { "scenario_id": "wohnen-sanierung", "nutzungsklasse": "wohnen", "titel": "Sanierung Bestandsgebäude für Wohnnutzung", "ampel": "gelb", "ampel_faktoren": [ { "faktor": "Widmung Bauland-Wohngebiet", "bewertung": "+", "quelle_ref": "noegis-widmung" }, { "faktor": "Bestand vorhanden", "bewertung": "+", "quelle_ref": "ms-footprints" }, { "faktor": "Erdbeben mittel (HORA)", "bewertung": "-", "quelle_ref": "hora-pass" } ], "ausschluss_grund": null, "kennzahlen": { "wohnnutzflaeche_m2": 110 }, "cost": { "low": 232000, "expected": 290000, "high": 348000, "unit": "EUR", "bandwidth_pct": 20 }, "cost_components": [ { "label": "Gebäude-Bestand-Sanierung", "amount": 255000, "source": "Heft 3/2025 NÖ gehoben 3.200 EUR/m² × 110 m² × 0.72 (Sanierungs-Faktor)" }, { "label": "Heizungstausch (KPC-förderfähig)", "amount": 25000, "source": "Konfig: HVAC-Wärmepumpe Standard" }, { "label": "Außenanlagen-Aufschlag", "amount": 10000, "source": "Heft 3/2025: ~2 % Aufschlag (einfach)" } ], "methodology": "ÖNORM B 1802-1:2022 + Heft 3/2025 + BPI Q-aktuell", "as_of_date": "2026-05-14", "disclaimer": "Grob-Kostenschätzung, ersetzt keine Detailkalkulation durch Sachverständigen/Planer", "foerder_trigger": ["kpc-waermepumpe", "kpc-flaechenrecycling"], "narrativ": "Das Bestandsgebäude lässt sich für Wohnnutzung sanieren. Der Sanierungsaufwand wird auf rund 290.000 EUR geschätzt (±20 %), basierend auf dem Heft-3/2025-Wert für NÖ gehobene Ausstattung und einem Sanierungs-Faktor. Der Heizungstausch ist KPC-förderfähig." }, { "scenario_id": "lager-logistik", "nutzungsklasse": "lager", "titel": "Lager-/Logistiknutzung", "ampel": "rot", "ampel_faktoren": [ { "faktor": "Widmung Wohngebiet schließt Lager aus", "bewertung": "-", "quelle_ref": "noegis-widmung" } ], "ausschluss_grund": "Harte Ausschlussregel: Widmung Bauland-Wohngebiet lässt gewerbliche Lagernutzung nicht zu.", "kennzahlen": {}, "cost": { "low": 0, "expected": 0, "high": 0, "unit": "EUR", "bandwidth_pct": 0 }, "cost_components": [], "methodology": "ÖNORM B 1802-1:2022 + Heft 3/2025 + BPI Q-aktuell", "as_of_date": "2026-05-14", "disclaimer": "Grob-Kostenschätzung, ersetzt keine Detailkalkulation durch Sachverständigen/Planer", "foerder_trigger": [], "narrativ": "Eine Lager-/Logistiknutzung ist an diesem Standort aufgrund der Widmung Bauland-Wohngebiet rechtlich ausgeschlossen." } ], "foerderungen": [ { "foerder_id": "kpc-pv", "programm": "KPC Photovoltaik", "traeger": "KPC", "max_betrag_eur": null, "max_satz_pct": 30, "bedingung": "Investitionszuschuss für Photovoltaik-Anlagen, Antrag vor Umsetzung", "match_score": 0.88, "match_begruendung": "Das PV-Freiflächen-Szenario fällt in den förderfähigen Anlagentyp der KPC-Photovoltaik-Richtlinie.", "quelle": "KPC-Richtlinie PV, Stand Q1/2026 (quartalsweise validiert)" }, { "foerder_id": "kpc-flaechenrecycling", "programm": "KPC Flächenrecycling", "traeger": "KPC", "max_betrag_eur": 60000, "max_satz_pct": 75, "bedingung": "Bis 75 % bzw. max. 60.000 EUR für Konzept, 50.000 EUR Detailuntersuchung", "match_score": 0.71, "match_begruendung": "Reaktivierung einer untergenutzten Baufläche — passt zum Flächenrecycling-Zweck.", "quelle": "KPC-Richtlinie Flächenrecycling, Stand Q1/2026" } ], "sources": [ { "source_id": "bev", "name": "BEV Kataster / INSPIRE", "lizenz": "Standardnutzungsrechte", "attribution_text": "Katasterdaten © BEV", "stand": "2026-04-28", "verwendet_fuer": ["parcel"] }, { "source_id": "noegis-widmung", "name": "NÖGIS Flächenwidmung", "lizenz": "CC BY 4.0", "attribution_text": "Widmungsdaten © Land NÖ (NÖGIS), CC BY 4.0", "stand": "2026-05-12", "verwendet_fuer": ["widmung", "scenarios"] }, { "source_id": "geosphere", "name": "GeoSphere Austria", "lizenz": "CC BY 4.0", "attribution_text": "Klimadaten © GeoSphere Austria, CC BY 4.0", "stand": "2026-05-11", "verwendet_fuer": ["risiko"] }, { "source_id": "alsag", "name": "ALSAG / Altlastenportal", "lizenz": "OGD", "attribution_text": "Altlastendaten © Umweltbundesamt", "stand": "2026-05-01", "verwendet_fuer": ["risiko"] }, { "source_id": "ms-footprints", "name": "Microsoft Building Footprints", "lizenz": "ODbL", "attribution_text": "Gebäudedaten © Microsoft, ODbL", "stand": "2025-09-01", "verwendet_fuer": ["bestand", "scenarios"] }, { "source_id": "hora-pass", "name": "HORA-Pass (BMLUK)", "lizenz": "Information für die Öffentlichkeit", "attribution_text": "Naturgefahren-Indikation: HORA-Pass, hora.gv.at, BMLUK", "stand": "2026-05-15", "verwendet_fuer": ["risiko"] }, { "source_id": "grundbuch", "name": "Grundbuch (JustizOnline)", "lizenz": "Bundesabgabenordnung", "attribution_text": "Grundbuchauszug via JustizOnline, Stichtag 2026-04-02", "stand": "2026-04-02", "verwendet_fuer": ["grundbuch"] } ], "confidence": { "overall": "hoch", "notes": ["Klima-Aggregat 5 Tage alt (innerhalb Cache-Toleranz)"], "poc_min_layers_met": true, "cost_bandwidth_default_pct": 20 }, "disclaimer": { "text": "Dieser Report ist eine unverbindliche Ersteinschätzung des Nutzungs-Potenzials. Er ersetzt keine rechtsverbindliche Vermessung, keine Sachverständigenbewertung und keine Detailkalkulation. Alle Werte sind Grob-Schätzungen mit angegebener Bandbreite.", "opt_in_required": true, "opt_in_recorded_at": "2026-05-16T09:13:55Z", "bausteine": { "widmung_nicht_rechtsverbindlich": "Widmungsangaben sind nur Indikation, keine rechtsverbindliche Auskunft der Gemeinde.", "alsag_keine_freiheitsgarantie": "Nicht-Listung im Altlastenkataster garantiert keine Schadstofffreiheit.", "bev_keine_vermessung": "Errechnete Flächen/Szenarien ersetzen keine rechtsverbindliche Vermessung.", "cost_keine_sachverstaendigenbewertung": "Grob-Kostenschätzung nach ÖNORM B 1802-1, keine Sachverständigenbewertung im Sinne der GewO.", "grundbuch_stichtag": "Grundbuch-Daten Stand 2026-04-02. Spätere Änderungen können bestehen." } }, "narrative": { "summary": "Die Liegenschaft in 2230 Gänserndorf liegt in Bauland-Wohngebiet ohne harte Restriktionen. Drei Nutzungsszenarien wurden geprüft: eine PV-Freiflächen-Anlage (empfohlen, ca. 6.100 EUR Jahresertrag), eine Wohn-Sanierung des Bestands (rund 290.000 EUR) sowie eine Lagernutzung (widmungsrechtlich ausgeschlossen). Die Eigentumsverhältnisse (zwei Miteigentümer) erlauben eine Aktivierung mit überschaubarem Koordinationsaufwand.", "empfehlung": "Kurzfristig größter Hebel: PV-Freiflächen-Anlage mit KPC-Photovoltaik-Förderung als niederschwelliger Einstieg, parallel Klärung der Hypothek für ein späteres Sanierungsszenario.", "modell": "anthropic/claude-opus-4.7", "guard_passed": true } } ```
18. Offene Fragen für Magnus' Domain-Workshop
- Harte Mindestanforderungen + Cost-Bausteine pro Nutzungsklasse (Wohnen/Gewerbe/PV/Co-Working/Lager/Tourismus) — dieser Spec fixiert nur die Struktur (ADR-0015 Säule 3, POC Woche 3).
- Ampel-Schwellen: Welche weichen Faktoren kippen `gruen`→`gelb`, welche sind harte `rot`-Ausschlüsse je Klasse?
- Grundbuch-Klarnamen-Auth: Bestätigung der Annahme in §8 — Klarnamen nie im Report-JSON, separater Auftraggeber-Endpoint (ADR-0016 offene Frage).
- Förder-Katalog-Pflege: Welche Programme im MVP-Katalog, wer validiert quartalsweise die Beträge/Sätze (DATA-SOURCES §Förder).
- PV-Referenz-Annahmen: kWp→kWh-Faktor und Strompreis für die POC-Euro-Zahl — Engine-Konstante oder regionsabhängig?