// Domain model for the F∆I Recl∆Im client. // // These types are the lingua franca between the (eventually // gRPC-fed) repository and the UI. Names mirror the Studie's // §5.3 data model. /// Evidence tier of the lowest-tier ingredient in a value. /// Per Studie §6.7: "Regel der niedrigsten Stufe" — any composed /// figure carries the tier of its weakest input. enum EvidenceTier { /// Official / peer-reviewed: NKR, DESTATIS, ifo, ZEW. t1, /// Verbands-Erhebung: DIHK, BDI, ZDH, IHK. t2, /// Eigene strukturierte Erhebung (Phase 2/3). t3, /// Qualitatives Signal — pain indicator only, never magnitude. t4, } extension EvidenceTierLabel on EvidenceTier { /// Kompakt-Code für enge Layouts. String get short => switch (this) { EvidenceTier.t1 => 'T1', EvidenceTier.t2 => 'T2', EvidenceTier.t3 => 'T3', EvidenceTier.t4 => 'T4', }; /// Ein-Wort-Bedeutung, immer neben dem T-Code zu zeigen. String get headline => switch (this) { EvidenceTier.t1 => 'amtlich', EvidenceTier.t2 => 'Verband', EvidenceTier.t3 => 'Erhebung', EvidenceTier.t4 => 'Signal', }; /// Lange Form für Listen, Tooltips, Legend-Texte. String get description => switch (this) { EvidenceTier.t1 => 'amtlich oder peer-reviewed (NKR, DESTATIS, ifo, ZEW)', EvidenceTier.t2 => 'Verbands-Erhebung (DIHK, IHK, ZDH, BDI)', EvidenceTier.t3 => 'eigene strukturierte Erhebung (Phase 2/3)', EvidenceTier.t4 => 'qualitatives Signal — nur Hinweis, keine Magnitude', }; } /// Geltungsbereich — the geographic/legal scope a norm belongs to. /// Nested by widening reach: Berlin ⊂ Deutschland ⊂ EU ⊂ Welt. /// Every norm carries one so the UI can flag, at a glance, *which* /// reform lever a figure speaks to (Bezirk/Senat, Bund, Brüssel, /// or völkerrechtlich) — see Studie §5.3. enum Jurisdiction { /// Landesrecht Berlin (BauO Bln, ASOG, …) — Senat/Bezirk lever. berlin, /// Bundesrecht (GewO, MiLoG, KassenSichV) — NKR/Bundestag lever. deutschland, /// EU-Recht (DSGVO, REFIT) — Reform-Hebel liegt in Brüssel. eu, /// Völker-/Welthandelsrecht (WTO, OECD, UN) — international. international, } extension JurisdictionLabel on Jurisdiction { /// Kompakt-Code für Chips und enge Layouts. String get short => switch (this) { Jurisdiction.berlin => 'BE', Jurisdiction.deutschland => 'DE', Jurisdiction.eu => 'EU', Jurisdiction.international => 'INT', }; /// Voll ausgeschriebener Name. String get label => switch (this) { Jurisdiction.berlin => 'Berlin', Jurisdiction.deutschland => 'Deutschland', Jurisdiction.eu => 'EU', Jurisdiction.international => 'International', }; /// Rechtsebene — für Tooltip und Legend. String get level => switch (this) { Jurisdiction.berlin => 'Landesrecht Berlin', Jurisdiction.deutschland => 'Bundesrecht', Jurisdiction.eu => 'Recht der Europäischen Union', Jurisdiction.international => 'Völker-/Welthandelsrecht', }; /// Wo der Reform-Hebel ansetzt — die handlungsleitende Aussage. String get reformLever => switch (this) { Jurisdiction.berlin => 'Reform-Hebel: Senat / Bezirk (Land Berlin)', Jurisdiction.deutschland => 'Reform-Hebel: Bundestag / NKR (Bund)', Jurisdiction.eu => 'Reform-Hebel: Brüssel — DE-Streichung allein wirkt nicht', Jurisdiction.international => 'Reform-Hebel: völkerrechtlich (WTO / OECD / UN)', }; /// Ableitung aus der ELI/CELEX-Kennung, wenn kein Feld gesetzt /// ist (Hub-Live-Modus liefert die ELI, nicht immer den Scope). /// eli/eu/… → eu /// eli/land/be/… → berlin /// eli/bund/… → deutschland /// eli/int|un|wto/… → international static Jurisdiction fromEli(String eli) { final e = eli.toLowerCase(); if (e.contains('/eu/') || e.startsWith('eli/eu') || e.contains('celex')) { return Jurisdiction.eu; } if (e.contains('/land/be') || e.contains('/land/berlin') || e.contains('/be/')) { return Jurisdiction.berlin; } if (e.contains('/int/') || e.contains('/un/') || e.contains('/wto/') || e.contains('/oecd/')) { return Jurisdiction.international; } return Jurisdiction.deutschland; } /// Parse a serialized scope string from a hub flow bag, falling /// back to ELI-derivation when absent or unrecognized. static Jurisdiction parse(String? raw, {required String eli}) { switch (raw?.trim().toLowerCase()) { case 'berlin' || 'be' || 'land-berlin': return Jurisdiction.berlin; case 'deutschland' || 'de' || 'bund' || 'bundesrecht': return Jurisdiction.deutschland; case 'eu' || 'europa' || 'union': return Jurisdiction.eu; case 'international' || 'int' || 'welt' || 'global': return Jurisdiction.international; default: return fromEli(eli); } } } /// Aktualität einer Norm-Fassung relativ zu unserer Datenbasis. /// Quelle der Wahrheit: `Norm.sourceSha256` gegen die aktuell /// veröffentlichte Quelle + `standDate`. Surfaced als Badge, damit /// eine veraltete Auswertung sofort erkennbar ist (Studie §5.3). enum Freshness { /// Unsere Fassung == aktuell veröffentlichter Stand. current, /// Upstream geändert/aufgehoben — unsere Auswertung ist veraltet. superseded, /// Noch nicht gegen die Quelle abgeglichen. unknown, } extension FreshnessLabel on Freshness { String get label => switch (this) { Freshness.current => 'aktuell', Freshness.superseded => 'geändert', Freshness.unknown => 'ungeprüft', }; String get description => switch (this) { Freshness.current => 'Fassung entspricht dem aktuell veröffentlichten Stand.', Freshness.superseded => 'Quelle wurde upstream geändert — Auswertung veraltet, ' 'Neubewertung nötig.', Freshness.unknown => 'Aktualität nicht gegen die Quelle abgeglichen.', }; /// Stale-Zustände bekommen ein sichtbares Badge; `current` nicht. bool get needsBadge => this != Freshness.current; } /// A citable source — never an LLM, always a public document or /// dataset reference. UI surfaces these next to every figure. class Source { const Source({ required this.label, required this.url, required this.tier, this.note, }); final String label; final String url; final EvidenceTier tier; final String? note; } /// One atomic obligation (Pflicht) extracted from a norm. /// Tuple shape matches Studie §6.3. class Duty { const Duty({ required this.modality, required this.addressee, required this.action, required this.frequency, required this.sourceNorm, this.authority, this.consequence, }); final String modality; // OBLIGATION | PROHIBITION | … final String addressee; final String action; final String frequency; final String sourceNorm; final String? authority; final String? consequence; } /// One Absatz/paragraph of the actual normative text. class NormParagraph { const NormParagraph({required this.number, required this.text}); /// Source numbering as it appears, e.g. "(1)", "(2a)". Empty /// when the source has no Absatz numbering. final String number; final String text; } /// One norm version — a versioned legal text identified by its /// (synthetic for now) ELI/CELEX. class Norm { const Norm({ required this.eli, required this.title, required this.jurabk, required this.paragraph, required this.jurisdiction, required this.standDate, required this.sourceUrl, required this.sourceSha256, this.freshness = Freshness.current, this.supersededNote, this.paragraphs = const [], }); final String eli; final String title; final String jurabk; final String paragraph; /// Geltungsbereich — Berlin / Deutschland / EU / International. /// Surfaced as a scope badge wherever the norm appears. final Jurisdiction jurisdiction; final DateTime standDate; final String sourceUrl; final String sourceSha256; /// Aktualität dieser Fassung gegenüber der Quelle. final Freshness freshness; /// Optionaler Klartext-Hinweis, *was* sich geändert hat (z. B. /// "BEG IV hat §17 zum 1.1.2026 entschärft"). Nur gesetzt, wenn /// freshness != current. final String? supersededNote; /// The actual paragraphs of the norm text, normalised by /// `text.akoma-normalize` in live mode. In demo mode the /// fixture quotes the public source verbatim under §5 UrhG /// (amtliche Werke). final List paragraphs; } /// A heatmap point: one norm with its computed scores. /// Axes mirror §6 in the study. class Evaluation { const Evaluation({ required this.norm, required this.skmEurPerYear, required this.benefitScore, required this.affectedCount, required this.frustScore, required this.tierLowest, required this.duties, required this.sources, required this.auditEventId, this.notes, this.componentSummary = const [], this.reviewedBy, this.reviewedAt, }); final Norm norm; /// Yearly bureaucracy cost across the addressee population. /// €/year, computed via SKM (`P × F × T × h`). final double skmEurPerYear; /// 0–5, composite of seven dimensions /// (safety / market / legal-certainty / EU-harmony / …). final double benefitScore; /// Number of addressees (KMU, Bürger, etc.). UI uses this for /// heatmap point size. final int affectedCount; /// 0–10, composite of readability + Verweistiefe + Akteurs- /// pluralität + Norm-Volatilität. final double frustScore; /// Lowest tier among all ingredients of any displayed figure. final EvidenceTier tierLowest; final List duties; final List sources; /// Pointer into the Ch∆In audit log (SQLite event_log row). final String auditEventId; final String? notes; /// Compact lines for at-a-glance reading on list views. /// One line per SKM-Komponente (P / F / T / h) plus optional /// extras — each carries its tier-tag inline. Empty list ⇒ /// fall back to the notes field for the list view. final List componentSummary; /// Jurist who confirmed this evaluation (the `by` of the hub /// approval). Null ⇒ not yet reviewed. Jurist review is OPTIONAL: /// the figure stands on its own; a review only *confirms* it. final String? reviewedBy; /// When the jurist signed off. Null ⇒ not reviewed. final DateTime? reviewedAt; /// True once a trusted jurist has confirmed this evaluation. bool get isReviewed => reviewedBy != null; }