feat(ui): scope, freshness and jurist-review badges + live empty-state
Adds three dark-first chip badges in the TierBadge pattern: - Jurisdiction (Berlin/DE/EU/International) — derived from the ELI, surfaced on the norms list, detail header and heatmap chips/hover. - Freshness (aktuell/geaendert/ungeprueft) — flags norms amended upstream (standDate + sourceSha256); renders only when stale. - Jurist review — marks evaluations a trusted jurist has confirmed (optional; the figure stands on its own). Models gain Jurisdiction/Freshness enums, Norm.freshness/supersededNote and Evaluation.reviewedBy/reviewedAt; HubRepository parses them from the flow bag (ELI fallback). HeatmapPage shows a real empty-state (hub-down vs no-evaluations) instead of a blank plot, and connect() no longer throws on a failed probe. Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
9dfa418e92
commit
aabff2b140
11 changed files with 604 additions and 26 deletions
|
|
@ -51,6 +51,138 @@ extension EvidenceTierLabel on EvidenceTier {
|
|||
};
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
|
@ -107,9 +239,12 @@ class Norm {
|
|||
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 [],
|
||||
});
|
||||
|
||||
|
|
@ -117,10 +252,23 @@ class Norm {
|
|||
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
|
||||
|
|
@ -143,6 +291,8 @@ class Evaluation {
|
|||
required this.auditEventId,
|
||||
this.notes,
|
||||
this.componentSummary = const [],
|
||||
this.reviewedBy,
|
||||
this.reviewedAt,
|
||||
});
|
||||
|
||||
final Norm norm;
|
||||
|
|
@ -179,4 +329,15 @@ class Evaluation {
|
|||
/// extras — each carries its tier-tag inline. Empty list ⇒
|
||||
/// fall back to the notes field for the list view.
|
||||
final List<String> 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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue