ADR-0070: Release-gebundener Preiskanal für T04 und ehrliche Granularität

  • Status: Accepted
  • Datum: 2026-09-17
  • Owner: Andreas (Tech)
  • Issue: #575 (Slice 1), Parent #493
  • Adressiert Risiken: R-01/R-02 (Datenrechte, Quellenstand), R-06 (Scheingenauigkeit)
  • Präzisiert: ADR-0069 §1 (Scope)
  • Baut auf: ADR-0043, ADR-0051, Muster Erdbeben-Publisher (#427)

Kontext

ADR-0069 hat T04 Immobilienmarkt-Signal als drittes öffentliches Werkzeug gewählt und als Bedingung gesetzt, dass Slice 1 die Preisquellen zuerst in den Source-Release-Vertrag überführt. Die Faktenlage beim Start:

  • Sechs Preisquellen werden per Cron in price_facts (Migration 0012) idempotent überschrieben. Die Tabelle kennt weder Release noch Run-Key; ein öffentliches Werkzeug könnte daraus keinen unveränderlichen, rechte- und frischegebundenen Stand lesen.
  • statat, eurostat, oenb stehen in source_registry als legacy_unverified mit rights_status = unknown; es gibt keine source-rights-Dateien, keinen Distribution-Key, keine Consumer-Bindung.
  • Granularität: Statistik Austria (OGD_hpi2015_HPI_15_1) hat nur die Zeitreihe als Dimension, Eurostat prc_hpi_q nur Länder, die OeNB nur Österreich und Wien. Es gibt keine Bundesland-Werte. ADR-0069 §1 hatte „im gewählten Bundesland“ versprochen; das war nicht belegt.

Entscheidung

  1. Scope-Korrektur zu ADR-0069: T04 zeigt Österreich gesamt (Statistik Austria HPI, Eurostat HPI), Wien und Österreich (OeNB Fundamentalpreisindikator) sowie EU-Aggregate als Vergleich. Kein Bundesland-Umschalter. Die Granularitätsgrenze steht in Rechte-Review, Coverage-Evidenz, Hilfetext und Werkzeug.
  2. Drei Quellen, ein Vertrag: lib/data-sources/prices-release-contract.mjs definiert für statat, eurostat, oenb Distribution-Key, Pflicht- Indikatoren, Pflicht-Regionen, Wertebereich und die fünf Pflichtprüfungen rights-attribution, artifact-schema, nationwide-coverage, null-and-range, freshness. Fähigkeit index_lookup, Kanal public, Coverage-Key country:AT. Eurostat trägt zusätzlich einen Regionsfilter (releaseRegions: Österreich, EU- und Euroraum-Aggregate): nur diese Zeilen bilden Kopie, Manifest und Evidenz; einzelne andere Länder bleiben in der operativen Tabelle. Beleg aus dem ersten Produktionslauf am 18. September 2026: die Türkei-Reihe (Index 1885 bei Basis 2015) riss die globale Wertgrenze, obwohl Österreich und EU im Rahmen lagen; der erste Kandidat f7f9563f… blieb deshalb korrekt unqualifiziert.
  3. Unveränderliche Release-Kopie statt Arbeitstabelle: Migration 0054 legt price_release_facts (PK je Release und Grain) und die Sicht price_facts_current an, die nur Zeilen des aktiven, qualifizierten public_display-Releases liefert. price_facts bleibt die operative Tabelle der Crons; das Werkzeug liest sie nie.
  4. Gebundener Ingest bezeugt sein Manifest: ingest() in scripts/lib/prices-db.mjs nimmt release entgegen, setzt Run-Key und Release-ID ab dem Start (source_release_assert_run_source), kopiert die Fakten je Release und schreibt den SHA-256 über die sortierten kanonischen Zeilen in metadata.manifest_sha256 plus quality_summary. Der Publisher übernimmt diesen Hash unverändert.
  5. Ein generischer Publisher: scripts/publish-prices-release.mjs --source <slug> folgt der erzwungenen Sequenz Kandidat → gebundener Ingest → Evidenz → Qualify → CAS-Activate. Kandidat entsteht mit freshness_basis = retrieved_at (der Quellenstand ist erst nach dem Ingest bekannt) und wird nach dem Ingest auf source_vintage plus fresh_until (Periodenende + maxStalenessDays) vervollständigt. Eine fehlgeschlagene Pflichtprüfung lässt Kandidat und Evidenz stehen und qualifiziert nicht. OeNB wird aus einer manuell beschafften ZIP-Datei publiziert (#87).
  6. Matrix und Registry: Die Lanes der drei Quellen tragen die Tool-ID market-signal; das Matrix-Tupel wird auf drei Werkzeuge erweitert (Version 2026-09-17.market-signal-price-lanes-v1). Der Registry-Eintrag erhält coverageContract: machine, bleibt aber planned, bis ein Release aktiv ist. Site-Check ignoriert Preis-Lanes weiterhin (er verlangt site-check in toolIds).
  7. Rechte-Review bleibt menschlich: Die drei Review-Dateien sind Entwürfe mit belegten Primärquellen (Statistik Austria OGD-Deskriptor CC BY 4.0, OeNB-Nutzungsbedingungen 2.2 CC BY 4.0, Eurostat Copyright-Hinweis). Die Registry-Übernahme per apply-source-rights-review.mjs --apply --ack <sha> und die Produktionsaktivierung erfolgen erst nach ausdrücklicher Bestätigung.

Alternativen, die erwogen wurden

AlternativeProContraWarum verworfen
price_facts um source_release_id erweitern und Crons überschreiben lassenkleinste Migrationjeder Cron-Lauf entkoppelt Zeilen wieder vom Release; Sicht leert sich unkontrolliertverletzt ADR-0051 (unveränderlicher Stand)
Crons komplett auf release-gebundene Läufe umstellenein Pfadjeder Quartalslauf würde automatisch qualifizieren und aktivierenAktivierung muss bewusst bleiben (ADR-0043)
Nur Statistik Austria binden, Rest späterschnellerT04 braucht OeNB-Wien und EU-Vergleich für ein sinnvolles Ergebnisdrei Quellen, ein Vertrag ist kaum teurer
Bundesland-Werte aus anderer Quelle nachbeschaffenerfüllt ADR-0069 wörtlichkeine freie Quelle mit Bundesland-HPI bekannt; lizenzpflichtige Transaktionsdaten (ADR-0016)ehrliche Korrektur statt Scheingenauigkeit

Konsequenzen

Positiv

  • Erste Migration von legacyUnverified-Quellen in den Release-Vertrag; das Muster ist für die übrigen 35 Katalogquellen wiederverwendbar.
  • Das Werkzeug kann nie einen Stand zeigen, der nicht qualifiziert, aktiv und frisch ist.
  • Granularitätsgrenze ist in jeder Schicht dieselbe.

Negativ / Trade-offs

  • Zwei Datenhaltungen für Preise (operativ und released); Speicher vernachlässigbar (< 10.000 Zeilen je Release).
  • OeNB-Releases bleiben Handarbeit je Quartal.
  • Ein Freshness-Fenster von 200–230 Tagen ist großzügig, weil Statistik Austria und OeNB mit Verzögerung veröffentlichen; ein stale Release blockiert die Matrixzelle, ersetzt sich aber nicht selbst.

Folge-Entscheidungen

  • Slice 2 (#575): Werkzeugseite und GET /api/tools/immobilienmarkt, lesend über price_facts_current. Pflicht aus dem Eurostat-Review (accessDateInAttribution): die sichtbare Attribution muss das Abrufdatum (retrieved_at des aktiven Releases) enthalten.
  • Slice 3: MET-01-Instrumentierung, Release-Evidenz, Tag-90-Bericht.
  • Runbook: docs/PRICES-RELEASE-RUNBOOK.md.

Referenzen

  • lib/data-sources/prices-release-contract.mjs, scripts/lib/prices-release-publisher.mjs, scripts/publish-prices-release.mjs, scripts/lib/prices-db.mjs
  • lib/db/migrations/0054_price_release_facts.sql
  • config/source-rights/{statat,eurostat,oenb}.public-display.json, docs/evidence/source-rights-{statat,eurostat,oenb}-2026-09-17.md
  • Tests: tests/unit/scripts-lib/prices-release-publisher.test.mjs, tests/unit/scripts-lib/prices-db-release.test.mjs, tests/integration/prices-publisher-postgres.e2e.mjs