From 136d3b48c619c0e668243ebe7adfdc40f8dd73b1 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Fri, 19 Jun 2026 02:25:38 +0200 Subject: [PATCH 1/5] perf(ui): stop the nav-rail brand pulse to fix web scroll jank The DeltaMark ran an endless 9s breathing animation (60fps repaint) inside the persistent nav rail, so it repainted on every scroll frame and stuttered scrolling on Flutter web. Make the rail mark static (animated: false) and isolate the animated variant (landing page) behind a RepaintBoundary. Signed-off-by: flemming-it --- app/lib/pages/shell_page.dart | 6 +++++- app/lib/widgets/delta_mark.dart | 35 ++++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/lib/pages/shell_page.dart b/app/lib/pages/shell_page.dart index 2f7b51a..01aa278 100644 --- a/app/lib/pages/shell_page.dart +++ b/app/lib/pages/shell_page.dart @@ -50,7 +50,11 @@ class _ShellPageState extends State { ), child: Column( children: [ - const DeltaMark(size: 36), + // Static in the persistent nav rail: a 60fps pulse + // here repaints every scroll frame and stutters web + // scrolling. The breathing mark stays on the + // landing page (which isn't scrolled). + const DeltaMark(size: 36, animated: false), const SizedBox(height: ReclaimSpace.sm), const BrandWordmark(size: 18), Text( diff --git a/app/lib/widgets/delta_mark.dart b/app/lib/widgets/delta_mark.dart index fd921cb..6ed7398 100644 --- a/app/lib/widgets/delta_mark.dart +++ b/app/lib/widgets/delta_mark.dart @@ -46,17 +46,30 @@ class _DeltaMarkState extends State @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller, - builder: (context, _) { - final t = _controller.value; - // Breathe: scale 1 → 1.28 → 1 over the cycle. - final pulse = 1.0 + 0.28 * (1 - (math.cos(t * 2 * math.pi).abs())); - return CustomPaint( - size: Size.square(widget.size), - painter: _DeltaPainter(pulse: widget.animated ? pulse : 1.0), - ); - }, + // Static mark: no controller listening, paint once. + if (!widget.animated) { + return CustomPaint( + size: Size.square(widget.size), + painter: _DeltaPainter(pulse: 1.0), + ); + } + // Animated: isolate the per-frame repaint in its own layer so + // the breathing pulse never invalidates surrounding content + // (web scroll stays smooth). + return RepaintBoundary( + child: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + final t = _controller.value; + // Breathe: scale 1 → 1.28 → 1 over the cycle. + final pulse = + 1.0 + 0.28 * (1 - (math.cos(t * 2 * math.pi).abs())); + return CustomPaint( + size: Size.square(widget.size), + painter: _DeltaPainter(pulse: pulse), + ); + }, + ), ); } } From 9dfa418e923f3a4e55fd106df20a552ae9bca1b5 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Fri, 19 Jun 2026 02:25:39 +0200 Subject: [PATCH 2/5] fix(ui): never hang on a blank frame when the hub is unreachable main() awaited the hub health probe before runApp(); a gRPC-web error out of HubRepository.connect propagated and left the app on a white page. Guard the settings+probe in main() with a MockRepository fallback so the first frame always paints. Signed-off-by: flemming-it --- app/lib/main.dart | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/lib/main.dart b/app/lib/main.dart index 83e9b52..038c14c 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -9,8 +9,22 @@ import 'theme/reclaim_theme.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - final settings = await HubSettings.load(); - final repository = await _pickRepository(settings); + // Acquiring settings + probing the hub must never prevent the + // first frame. Any failure here falls back to demo data so the + // app always paints instead of hanging on a blank page. + HubSettings settings; + EvaluationRepository repository; + try { + settings = await HubSettings.load(); + repository = await _pickRepository(settings); + } catch (_) { + settings = const HubSettings( + host: HubSettings.defaultHost, + port: HubSettings.defaultPort, + secure: false, + ); + repository = const MockRepository(); + } runApp(ReclaimApp(repository: repository, settings: settings)); } From aabff2b1408ea519d362a8bce8457b8014c82a57 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Fri, 19 Jun 2026 02:25:39 +0200 Subject: [PATCH 3/5] feat(ui): scope, freshness and jurist-review badges + live empty-state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/lib/data/fixtures.dart | 15 +++ app/lib/data/hub_repository.dart | 45 ++++++- app/lib/data/models.dart | 161 ++++++++++++++++++++++++ app/lib/pages/heatmap_page.dart | 126 ++++++++++++++++--- app/lib/pages/norm_detail_page.dart | 25 +++- app/lib/pages/norms_list_page.dart | 18 ++- app/lib/theme/reclaim_tokens.dart | 17 +++ app/lib/widgets/freshness_badge.dart | 74 +++++++++++ app/lib/widgets/heatmap_grid.dart | 3 +- app/lib/widgets/jurisdiction_badge.dart | 69 ++++++++++ app/lib/widgets/review_badge.dart | 77 ++++++++++++ 11 files changed, 604 insertions(+), 26 deletions(-) create mode 100644 app/lib/widgets/freshness_badge.dart create mode 100644 app/lib/widgets/jurisdiction_badge.dart create mode 100644 app/lib/widgets/review_badge.dart diff --git a/app/lib/data/fixtures.dart b/app/lib/data/fixtures.dart index abf5685..1fc2af0 100644 --- a/app/lib/data/fixtures.dart +++ b/app/lib/data/fixtures.dart @@ -26,6 +26,7 @@ class Fixtures { title: 'Anzeigepflicht', jurabk: 'GewO', paragraph: '§ 14', + jurisdiction: Jurisdiction.deutschland, standDate: DateTime.utc(2025, 12, 1), sourceUrl: 'https://www.gesetze-im-internet.de/gewo/__14.html', sourceSha256: 'demo-${'a' * 60}', @@ -104,6 +105,7 @@ class Fixtures { title: 'Kassensicherungsverordnung', jurabk: 'KassenSichV', paragraph: 'gesamt', + jurisdiction: Jurisdiction.deutschland, standDate: DateTime.utc(2025, 11, 15), sourceUrl: 'https://www.gesetze-im-internet.de/kassensichv/index.html', @@ -177,6 +179,8 @@ class Fixtures { ), ], auditEventId: 'demo-evt-kassensichv', + reviewedBy: 'RA Dr. M. Kessler', + reviewedAt: DateTime.utc(2026, 1, 20), notes: 'Komponenten-Tier: Initial-Erfüllungsaufwand wurde 2016 im NKR-Stellungnahme-' 'Prozess zum Kassengesetz dokumentiert (T1 für Größenordnung). ' @@ -197,6 +201,7 @@ class Fixtures { title: 'Verzeichnis von Verarbeitungstätigkeiten', jurabk: 'DSGVO', paragraph: 'Art. 30', + jurisdiction: Jurisdiction.eu, standDate: DateTime.utc(2025, 10, 1), sourceUrl: 'https://eur-lex.europa.eu/eli/reg/2016/679', sourceSha256: 'demo-${'c' * 60}', @@ -277,6 +282,8 @@ class Fixtures { ), ], auditEventId: 'demo-evt-dsgvo-30', + reviewedBy: 'Prof. Dr. A. Lindner', + reviewedAt: DateTime.utc(2026, 2, 3), notes: 'EU-determiniert: Reform-Vorschlag muss Brüssel adressieren, nicht ' 'DE-Streichung. Komponenten-Tier: P (Fallzahl Verantwortliche) ' @@ -299,6 +306,7 @@ class Fixtures { title: 'Bauantrag', jurabk: 'BauO Bln', paragraph: '§ 60 ff', + jurisdiction: Jurisdiction.berlin, standDate: DateTime.utc(2025, 9, 1), sourceUrl: 'https://gesetze.berlin.de/bsbe/document/jlr-BauOBE2005V25P60', @@ -363,6 +371,8 @@ class Fixtures { ), ], auditEventId: 'demo-evt-berlinbauo-60', + reviewedBy: 'RAin S. Brandt', + reviewedAt: DateTime.utc(2026, 1, 28), notes: 'Berlin-spezifisch, daher kein NKR-Erfüllungsaufwand auf Bundesebene. ' 'P aus Berliner Bauaufsichts-Statistik (T2 Handwerkskammer Berlin); ' @@ -385,9 +395,14 @@ class Fixtures { title: 'Dokumentationspflichten', jurabk: 'MiLoG', paragraph: '§ 17', + jurisdiction: Jurisdiction.deutschland, standDate: DateTime.utc(2025, 11, 1), sourceUrl: 'https://www.gesetze-im-internet.de/milog/__17.html', sourceSha256: 'demo-${'e' * 60}', + freshness: Freshness.superseded, + supersededNote: 'BEG IV hat die Dokumentationspflichten zum ' + '1.1.2026 entschärft — diese Auswertung bildet die Fassung ' + 'davor ab und ist neu zu bewerten.', paragraphs: const [ NormParagraph( number: '(1)', diff --git a/app/lib/data/hub_repository.dart b/app/lib/data/hub_repository.dart index 2f1676e..768bd71 100644 --- a/app/lib/data/hub_repository.dart +++ b/app/lib/data/hub_repository.dart @@ -36,10 +36,21 @@ class HubRepository implements EvaluationRepository { endpoint: endpoint, authToken: settings.authToken, ); - final ok = await client.healthy().timeout( - const Duration(seconds: 4), - onTimeout: () => false, - ); + // The probe must NEVER throw out of connect(): main() awaits + // this before runApp(), so any uncaught error here leaves the + // app on a blank page. A timeout maps to false; so does any + // transport error (e.g. a gRPC-web call against a plain-gRPC + // hub, or a refused connection) — the app then comes up in + // 'hub-down' state and shows the empty-state hint. + bool ok; + try { + ok = await client.healthy().timeout( + const Duration(seconds: 4), + onTimeout: () => false, + ); + } catch (_) { + ok = false; + } return HubRepository._(client, settings, ok); } @@ -97,14 +108,21 @@ class HubRepository implements EvaluationRepository { if (normJson == null) return null; final skmJson = _asMap(bag['skm']); + final eli = (normJson['eli'] as String?) ?? 'eli/unknown'; final norm = Norm( - eli: (normJson['eli'] as String?) ?? 'eli/unknown', + eli: eli, title: (normJson['title'] as String?) ?? '(ohne Titel)', jurabk: (normJson['jurabk'] as String?) ?? '', paragraph: (normJson['paragraph'] as String?) ?? '', + jurisdiction: JurisdictionLabel.parse( + normJson['scope'] as String?, + eli: eli, + ), standDate: _parseTimestamp(event.timestamp), sourceUrl: (normJson['source_url'] as String?) ?? '', sourceSha256: (normJson['source_sha256'] as String?) ?? '', + freshness: _parseFreshness(normJson['freshness']), + supersededNote: normJson['superseded_note'] as String?, paragraphs: _parseParagraphs(normJson['paragraphs']), ); @@ -138,9 +156,26 @@ class HubRepository implements EvaluationRepository { auditEventId: event.eventId, notes: 'Live aus Hub-Flow ${event.flowName} ' '(execution ${event.flowExecution.substring(0, 8.clamp(0, event.flowExecution.length))}).', + // Jurist sign-off is recorded by the approval, not the flow + // bag. Until the review-rewrite wires approvals in, these stay + // null (= unreviewed) unless the bag carries them explicitly. + reviewedBy: bag['reviewed_by'] as String?, + reviewedAt: DateTime.tryParse( + (bag['reviewed_at'] as String?) ?? ''), ); } + static Freshness _parseFreshness(dynamic v) { + switch ((v as String?)?.trim().toLowerCase()) { + case 'superseded' || 'amended' || 'geändert' || 'stale': + return Freshness.superseded; + case 'unknown' || 'ungeprüft': + return Freshness.unknown; + default: + return Freshness.current; + } + } + static Map? _asMap(dynamic v) => v is Map ? v : null; diff --git a/app/lib/data/models.dart b/app/lib/data/models.dart index d75f950..9a6ae17 100644 --- a/app/lib/data/models.dart +++ b/app/lib/data/models.dart @@ -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 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; } diff --git a/app/lib/pages/heatmap_page.dart b/app/lib/pages/heatmap_page.dart index f5832ee..3e1200c 100644 --- a/app/lib/pages/heatmap_page.dart +++ b/app/lib/pages/heatmap_page.dart @@ -5,6 +5,7 @@ import '../data/repository.dart'; import '../theme/reclaim_tokens.dart'; import '../widgets/mode_banner.dart'; import '../widgets/heatmap_grid.dart'; +import '../widgets/jurisdiction_badge.dart'; import '../widgets/reclaim_card.dart'; import '../widgets/tier_badge.dart'; import 'norm_detail_page.dart'; @@ -59,26 +60,29 @@ class _HeatmapPageState extends State { ), const SizedBox(height: ReclaimSpace.lg), Expanded( - child: ReclaimCard( - accent: true, - expand: true, - child: HeatmapGrid( - evaluations: items, - onTap: (e) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => NormDetailPage( - repository: widget.repository, - evaluation: e, - ), + child: items.isEmpty + ? _EmptyHubState(repository: widget.repository) + : ReclaimCard( + accent: true, + expand: true, + child: HeatmapGrid( + evaluations: items, + onTap: (e) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => NormDetailPage( + repository: widget.repository, + evaluation: e, + ), + ), + ); + }, ), - ); - }, - ), - ), + ), ), - const SizedBox(height: ReclaimSpace.md), - const _LegendCard(), + if (items.isNotEmpty) ...[ + const SizedBox(height: ReclaimSpace.md), + const _LegendCard(), const SizedBox(height: ReclaimSpace.lg), Wrap( spacing: ReclaimSpace.md, @@ -104,6 +108,11 @@ class _HeatmapPageState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ + JurisdictionBadge( + jurisdiction: e.norm.jurisdiction, + compact: true), + const SizedBox( + width: ReclaimSpace.xs), TierBadge( tier: e.tierLowest, compact: true), const SizedBox( @@ -131,6 +140,7 @@ class _HeatmapPageState extends State { ), ], ), + ], ], ); }, @@ -196,6 +206,8 @@ class _LegendCardState extends State<_LegendCard> { symbol: '●', text: 'Betroffenheit'), _MiniLabel( symbol: '◐', text: 'Evidenz-Stufe'), + _MiniLabel( + symbol: '⌖', text: 'Geltungsbereich'), ], ), ), @@ -272,6 +284,21 @@ class _LegendCardState extends State<_LegendCard> { 'SKM-Zahl T4. Petrol → warm = stark → ' 'schwach.', ), + SizedBox(height: ReclaimSpace.sm), + _LegendRow( + symbol: '⌖', + axis: 'Chip-Symbol', + label: 'Geltungsbereich', + description: + 'Jede Norm trägt ein Scope-Chip — ' + 'BE Berlin (Landesrecht, Senat/Bezirk), ' + 'DE Deutschland (Bundesrecht, NKR/' + 'Bundestag), EU (Reform-Hebel in Brüssel), ' + 'INT International (WTO/OECD/UN). Es zeigt ' + 'an, auf welcher Ebene der Reform-Hebel ' + 'ansetzt: eine EU-Norm lässt sich nicht ' + 'durch nationale Streichung entlasten.', + ), ], ), ) @@ -285,6 +312,69 @@ class _LegendCardState extends State<_LegendCard> { } } +/// Shown in place of the heatmap when the repository returns zero +/// evaluations — almost always live-hub mode against a hub that +/// has no Recl∆Im `flow.completed` events yet (or is unreachable). +/// Tells the reader exactly how to get data on screen instead of +/// leaving a blank plot. +class _EmptyHubState extends StatelessWidget { + const _EmptyHubState({required this.repository}); + + final EvaluationRepository repository; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + final down = repository.mode == 'hub-down'; + return ReclaimCard( + accent: true, + expand: true, + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 460), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + down ? Icons.cloud_off_outlined : Icons.inbox_outlined, + size: 40, + color: ReclaimColors.mute, + ), + const SizedBox(height: ReclaimSpace.md), + Text( + down + ? 'Hub nicht erreichbar' + : 'Noch keine Auswertungen im Hub', + style: t.headlineSmall, + ), + const SizedBox(height: ReclaimSpace.sm), + Text( + down + ? 'Verbunden mit ${repository.hubLabel}. Läuft ' + '`chain serve`? Sonst im Hub-Reiter den ' + 'Demo-Modus aktivieren — dann zeigt die ' + 'Heatmap das Fixture-Korpus.' + : 'Der Hub liefert keine flow.completed-Events. ' + 'Einen Recl∆Im-Flow laufen lassen, z. B.\n' + ' chain run flows/durchstich-from-file.yaml ' + '--input content=@norm.xml --input ' + 'cohort_id=berlin-kmu\n' + 'und im Hub/Studio freigeben — oder im ' + 'Hub-Reiter den Demo-Modus aktivieren.', + style: t.bodyLarge?.copyWith( + color: ReclaimColors.mute, + height: 1.4, + ), + ), + ], + ), + ), + ), + ); + } +} + class _MiniLabel extends StatelessWidget { const _MiniLabel({required this.symbol, required this.text}); diff --git a/app/lib/pages/norm_detail_page.dart b/app/lib/pages/norm_detail_page.dart index c0ff4ca..c77b628 100644 --- a/app/lib/pages/norm_detail_page.dart +++ b/app/lib/pages/norm_detail_page.dart @@ -5,8 +5,11 @@ import '../data/repository.dart'; import '../theme/reclaim_tokens.dart'; import '../widgets/mode_banner.dart'; import '../widgets/evidence_sidebar.dart'; +import '../widgets/freshness_badge.dart'; +import '../widgets/jurisdiction_badge.dart'; import '../widgets/norm_text_card.dart'; import '../widgets/reclaim_card.dart'; +import '../widgets/review_badge.dart'; import '../widgets/tier_badge.dart'; class NormDetailPage extends StatelessWidget { @@ -63,7 +66,27 @@ class NormDetailPage extends StatelessWidget { ], ), ), - TierBadge(tier: e.tierLowest), + Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + JurisdictionBadge( + jurisdiction: e.norm.jurisdiction), + const SizedBox(height: ReclaimSpace.sm), + TierBadge(tier: e.tierLowest), + if (e.norm.freshness.needsBadge) ...[ + const SizedBox( + height: ReclaimSpace.sm), + FreshnessBadge( + freshness: e.norm.freshness, + note: e.norm.supersededNote, + ), + ], + const SizedBox(height: ReclaimSpace.sm), + ReviewBadge( + evaluation: e, showUnreviewed: true), + ], + ), ], ), const SizedBox(height: ReclaimSpace.lg), diff --git a/app/lib/pages/norms_list_page.dart b/app/lib/pages/norms_list_page.dart index 2d1ff3e..468dd8d 100644 --- a/app/lib/pages/norms_list_page.dart +++ b/app/lib/pages/norms_list_page.dart @@ -4,7 +4,10 @@ import '../data/models.dart'; import '../data/repository.dart'; import '../theme/reclaim_tokens.dart'; import '../widgets/mode_banner.dart'; +import '../widgets/freshness_badge.dart'; +import '../widgets/jurisdiction_badge.dart'; import '../widgets/reclaim_card.dart'; +import '../widgets/review_badge.dart'; import '../widgets/tier_badge.dart'; import 'norm_detail_page.dart'; @@ -108,7 +111,20 @@ class _NormRow extends StatelessWidget { ), ), const SizedBox(height: ReclaimSpace.sm), - TierBadge(tier: evaluation.tierLowest), + Wrap( + spacing: ReclaimSpace.sm, + runSpacing: ReclaimSpace.xs, + children: [ + JurisdictionBadge( + jurisdiction: evaluation.norm.jurisdiction), + TierBadge(tier: evaluation.tierLowest), + FreshnessBadge( + freshness: evaluation.norm.freshness, + note: evaluation.norm.supersededNote, + ), + ReviewBadge(evaluation: evaluation), + ], + ), ], ), ), diff --git a/app/lib/theme/reclaim_tokens.dart b/app/lib/theme/reclaim_tokens.dart index 654bde6..555900d 100644 --- a/app/lib/theme/reclaim_tokens.dart +++ b/app/lib/theme/reclaim_tokens.dart @@ -36,6 +36,23 @@ class ReclaimColors { static const tierT2 = Color(0xFF6CA29A); // verband static const tierT3 = Color(0xFFB0AC8E); // own survey static const tierT4 = Color(0xFFA86F5C); // qualitative signal only + + // Scope badges — Geltungsbereich of a norm. Hues widen from a + // warm-local Berlin to a cool-global teal, distinct from the + // tier palette so the two chips never read as the same thing. + // Tuned for dark-first legibility on the ink canvas (bright + // enough to read as chip text/border over inkRaised). + static const scopeBerlin = Color(0xFFD0685C); // Land Berlin (brick) + static const scopeDeutschland = Color(0xFFC79A3C); // Bund (gold) + static const scopeEu = Color(0xFF5B8DD6); // EU (union blue) + static const scopeInternational = Color(0xFF4FB89E); // global (teal) + + // Freshness — is our norm version current vs. the published source. + static const freshSuperseded = Color(0xFFE0973A); // amended upstream + static const freshUnknown = Color(0xFF9AA0A6); // not reconciled + + // Jurist review — an evaluation confirmed (signed off) by a jurist. + static const reviewConfirmed = Color(0xFF6FBE7A); // confirm green } /// Spacing scale — 4/8/12/16/24/32/48 multiples, matching diff --git a/app/lib/widgets/freshness_badge.dart b/app/lib/widgets/freshness_badge.dart new file mode 100644 index 0000000..0c938ee --- /dev/null +++ b/app/lib/widgets/freshness_badge.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +import '../data/models.dart'; +import '../theme/reclaim_tokens.dart'; + +/// Freshness badge — flags when a norm version is stale relative to +/// the published source (amended upstream) or hasn't been reconciled. +/// By design it renders NOTHING for [Freshness.current] (the common +/// case shouldn't add noise) unless [alwaysShow] is set. Same chip +/// shape as the scope/tier badges. +class FreshnessBadge extends StatelessWidget { + const FreshnessBadge({ + super.key, + required this.freshness, + this.note, + this.compact = false, + this.alwaysShow = false, + }); + + final Freshness freshness; + + /// Optional plain-text hint of what changed (from Norm.supersededNote). + final String? note; + final bool compact; + final bool alwaysShow; + + @override + Widget build(BuildContext context) { + if (!freshness.needsBadge && !alwaysShow) { + return const SizedBox.shrink(); + } + final t = Theme.of(context).textTheme; + final color = switch (freshness) { + Freshness.current => ReclaimColors.signal, + Freshness.superseded => ReclaimColors.freshSuperseded, + Freshness.unknown => ReclaimColors.freshUnknown, + }; + final icon = switch (freshness) { + Freshness.current => Icons.check_circle_outline, + Freshness.superseded => Icons.history_toggle_off, + Freshness.unknown => Icons.help_outline, + }; + final label = compact ? freshness.label : 'Stand: ${freshness.label}'; + return Tooltip( + message: '${freshness.description}' + '${note != null && note!.isNotEmpty ? '\n$note' : ''}', + child: Container( + padding: EdgeInsets.symmetric( + horizontal: compact ? 6 : ReclaimSpace.sm, + vertical: compact ? 2 : 4, + ), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.18), + border: Border.all(color: color.withValues(alpha: 0.55)), + borderRadius: const BorderRadius.all(ReclaimRadius.sm), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: compact ? 12 : 14, color: color), + const SizedBox(width: 6), + Text( + label, + style: (compact ? t.labelSmall : t.labelLarge)?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/widgets/heatmap_grid.dart b/app/lib/widgets/heatmap_grid.dart index 3abb1a8..75c6d30 100644 --- a/app/lib/widgets/heatmap_grid.dart +++ b/app/lib/widgets/heatmap_grid.dart @@ -267,7 +267,8 @@ class _HeatmapPainter extends CustomPainter { ); _drawText( canvas, - 'Tier: ${e.tierLowest.short} Klick → Detail', + 'Bereich: ${e.norm.jurisdiction.short} ' + 'Tier: ${e.tierLowest.short} Klick → Detail', box.topLeft + const Offset(10, 44), const TextStyle( color: mute, diff --git a/app/lib/widgets/jurisdiction_badge.dart b/app/lib/widgets/jurisdiction_badge.dart new file mode 100644 index 0000000..88cdb8e --- /dev/null +++ b/app/lib/widgets/jurisdiction_badge.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +import '../data/models.dart'; +import '../theme/reclaim_tokens.dart'; + +/// Scope badge — surfaces a norm's Geltungsbereich (Berlin / +/// Deutschland / EU / International) wherever the norm appears, so +/// the reader sees at a glance which reform lever a figure speaks +/// to. Visually mirrors [TierBadge]: a scope glyph + code in a +/// tinted chip, here with a Material icon instead of a dot. +class JurisdictionBadge extends StatelessWidget { + const JurisdictionBadge({ + super.key, + required this.jurisdiction, + this.compact = false, + }); + + final Jurisdiction jurisdiction; + final bool compact; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + final color = switch (jurisdiction) { + Jurisdiction.berlin => ReclaimColors.scopeBerlin, + Jurisdiction.deutschland => ReclaimColors.scopeDeutschland, + Jurisdiction.eu => ReclaimColors.scopeEu, + Jurisdiction.international => ReclaimColors.scopeInternational, + }; + final icon = switch (jurisdiction) { + Jurisdiction.berlin => Icons.location_city_outlined, + Jurisdiction.deutschland => Icons.flag_outlined, + Jurisdiction.eu => Icons.star_outline_rounded, + Jurisdiction.international => Icons.public, + }; + final label = compact + ? jurisdiction.short + : '${jurisdiction.short} · ${jurisdiction.label}'; + return Tooltip( + message: 'Geltungsbereich: ${jurisdiction.label} ' + '(${jurisdiction.level}).\n${jurisdiction.reformLever}.', + child: Container( + padding: EdgeInsets.symmetric( + horizontal: compact ? 6 : ReclaimSpace.sm, + vertical: compact ? 2 : 4, + ), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.18), + border: Border.all(color: color.withValues(alpha: 0.55)), + borderRadius: const BorderRadius.all(ReclaimRadius.sm), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: compact ? 12 : 14, color: color), + const SizedBox(width: 6), + Text( + label, + style: (compact ? t.labelSmall : t.labelLarge)?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/widgets/review_badge.dart b/app/lib/widgets/review_badge.dart new file mode 100644 index 0000000..cfb03d6 --- /dev/null +++ b/app/lib/widgets/review_badge.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +import '../data/models.dart'; +import '../theme/reclaim_tokens.dart'; + +/// Review badge — marks an evaluation a trusted jurist has confirmed +/// (signed off via the hub approval). Jurist review is OPTIONAL: an +/// evaluation stands on its own; this only adds the "bestätigt" mark. +/// Renders a confirm-green check when reviewed; renders the muted +/// "ungeprüft" state only when [showUnreviewed] is set (otherwise +/// nothing, to keep unreviewed rows clean). +class ReviewBadge extends StatelessWidget { + const ReviewBadge({ + super.key, + required this.evaluation, + this.compact = false, + this.showUnreviewed = false, + }); + + final Evaluation evaluation; + final bool compact; + final bool showUnreviewed; + + @override + Widget build(BuildContext context) { + final reviewed = evaluation.isReviewed; + if (!reviewed && !showUnreviewed) return const SizedBox.shrink(); + + final t = Theme.of(context).textTheme; + final color = + reviewed ? ReclaimColors.reviewConfirmed : ReclaimColors.mute; + final icon = reviewed + ? Icons.verified_outlined + : Icons.radio_button_unchecked; + final label = reviewed + ? (compact ? 'geprüft' : 'Jurist-geprüft') + : (compact ? 'offen' : 'ungeprüft'); + final tip = reviewed + ? 'Bestätigt von ${evaluation.reviewedBy}' + '${evaluation.reviewedAt != null ? ' am ${_d(evaluation.reviewedAt!)}' : ''}.\n' + 'Jurist-Review ist optional — es bestätigt die Auswertung, ' + 'ist aber keine Voraussetzung.' + : 'Noch von keinem Juristen bestätigt. Die Auswertung gilt ' + 'trotzdem (Review ist optional).'; + return Tooltip( + message: tip, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: compact ? 6 : ReclaimSpace.sm, + vertical: compact ? 2 : 4, + ), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.18), + border: Border.all(color: color.withValues(alpha: 0.55)), + borderRadius: const BorderRadius.all(ReclaimRadius.sm), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: compact ? 12 : 14, color: color), + const SizedBox(width: 6), + Text( + label, + style: (compact ? t.labelSmall : t.labelLarge)?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } + + static String _d(DateTime d) => + '${d.day.toString().padLeft(2, '0')}.${d.month.toString().padLeft(2, '0')}.${d.year}'; +} From a022f3100a227a33ea97c35e3c8d676f4c5ad6f1 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Fri, 19 Jun 2026 02:25:39 +0200 Subject: [PATCH 4/5] feat(flow): runnable durchstich-full + jurist approval prompt Earlier durchstich-full never parsed (inline maps in with:). Rewrite to the runnable step set (normalize/deontic/citations/cohort/frust); skm/benefit stay documented-out pending the SDK json-input path. Give the system.approval step a clear prompt (Studio shows it as the approval-card heading). Signed-off-by: flemming-it --- flows/durchstich-full.yaml | 92 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 flows/durchstich-full.yaml diff --git a/flows/durchstich-full.yaml b/flows/durchstich-full.yaml new file mode 100644 index 0000000..8b86747 --- /dev/null +++ b/flows/durchstich-full.yaml @@ -0,0 +1,92 @@ +# Vertikaler Durchstich, Vollversion (so weit heute lauffähig). +# +# Erweitert durchstich-from-file um die Analyse-Module, deren +# Eingaben sich aus vorhergehenden Step-Outputs speisen lassen +# (alle als $ref) — Pflichten, Zitationsgraph, Lesbarkeit/Frust. +# Das flow.completed-Event trägt damit norm + duties + citations +# + frust + cohort, also die Form, die HubRepository.list() in der +# Recl∆Im-App in Evaluation-Objekte übersetzt. +# +# NICHT enthalten: econ.skm_score und law.benefit_score. Beide +# Module lesen ihren Tupel-/Ratings-Input über `require_json`, +# d.h. sie erwarten ein Payload::Json. In einem Flow lässt sich +# ein Json-Wert aber nur aus einem vorhergehenden Step-Output +# beziehen — `chain run --input name=value` erzeugt Text, +# `--input name=@file` erzeugt Bytes, nie Json, und ein Inline- +# Map-Literal in `with:` lehnt der Flow-Parser ab ("expected a +# string"). Es gibt heute also keinen Weg, skm/benefit aus CLI- +# Inputs zu treiben. Siehe Handoff an chain (json-CLI-Input). +# Sobald der Plattform-Fix steht, die skm-/benefit-Steps unten +# einkommentieren (Form: `with: { duties: $inputs.duties }` mit +# `inputs: { duties: json }`). +# +# Aufruf: +# chain run flows/durchstich-full.yaml \ +# --input content=@/tmp/gewo-14-mini.xml \ +# --input cohort_id=berlin-kmu +# (pausiert am system.approval-Gate — danach via Hub/Studio +# freigeben, dann wird das flow.completed-Event geschrieben.) + +name: durchstich-full + +inputs: + content: bytes + cohort_id: text + +steps: + - id: normalize + use: text.akoma_normalize@^0 + with: + content: $inputs.content + mime: "application/xml" + + - id: deontic + use: text.deontic_extract@^0 + with: + norm: $normalize.norm + + - id: citations + use: graph.citation_extract@^0 + with: + norm: $normalize.norm + + - id: cohort + use: stats.cohort_size@^0 + with: + cohort_id: $inputs.cohort_id + + - id: frust + use: text.readability_score@^0 + with: + norm: $normalize.norm + graph: $citations.citations + + # Pending platform json-CLI-input (Handoff → chain). Form, die + # nach dem Fix gilt — `inputs:` oben um `duties: json` / + # `ratings: json` ergänzen und mit --input duties=… speisen: + # - id: skm + # use: econ.skm_score@^0 + # with: + # duties: $inputs.duties + # - id: benefit + # use: law.benefit_score@^0 + # with: + # ratings: $inputs.ratings + + - id: review + use: system.approval@^0 + with: + title: "Recl∆Im — Review GewO §14 (Live)" + prompt: >- + Juristische Freigabe: Stimmen die extrahierten Pflichten, die + Zitationen und die Norm-Wiedergabe mit dem geltenden Gesetzestext + überein? Quellen siehe Payload. Bei Freigabe gilt die Auswertung + als juristisch bestätigt. + payload: $normalize.norm + +outputs: + norm: $normalize.norm + frust: $frust.score + cohort: $cohort.cohort + duties: $deontic.duties + citations: $citations.citations From 46b4003d3681c930831614a002536f233b03de5a Mon Sep 17 00:00:00 2001 From: flemming-it Date: Fri, 19 Jun 2026 02:25:39 +0200 Subject: [PATCH 5/5] =?UTF-8?q?chore(store):=20Recl=E2=88=86Im=20module=20?= =?UTF-8?q?store=20index=20+=20staged=20bundles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit store/store.yaml lists all 8 modules (status published, DE+EN tagline/description, category, tags, license, repo) with real .fai bundle sha256 from chain pack. Bundles are unsigned for now per chain (require_signatures off; FAI_SIGNING_KEY untouched; Recl∆Im key + per-store pinning later) and staged under store/bundles (gitignored). Only mirror hosting remains. Signed-off-by: flemming-it --- .gitignore | 5 ++ store/.gitignore | 1 + store/SHA256.local.txt | 9 +++ store/SHA256.txt | 9 +++ store/store.yaml | 167 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 store/.gitignore create mode 100644 store/SHA256.local.txt create mode 100644 store/SHA256.txt create mode 100644 store/store.yaml diff --git a/.gitignore b/.gitignore index 5caee98..012dd08 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ Thumbs.db # Local-only scratch /scratch/ /.local/ +.playwright-mcp/ +*.log +reclaim-landing.png +app/build/ +store/bundles/ diff --git a/store/.gitignore b/store/.gitignore new file mode 100644 index 0000000..6ee5de4 --- /dev/null +++ b/store/.gitignore @@ -0,0 +1 @@ +bundles/ diff --git a/store/SHA256.local.txt b/store/SHA256.local.txt new file mode 100644 index 0000000..4d4be3a --- /dev/null +++ b/store/SHA256.local.txt @@ -0,0 +1,9 @@ +# Local module.wasm sha256 (cross-check; canonical sha must come from the hosted .fai bundle) +0fd01a7aa7ca2a47aa27bea7b30f8b8fe2ffc551728694c5dfbadb75bc37e91d text-akoma-normalize +722da24ebc239015af344b66a438097ef607b965badb43f91e43c4b1a9fdcc19 text-deontic-extract +3f4976db72bd7410e83b0694de3e91c09b8829d3d73fa48ddf806c170cc5c602 graph-citation-extract +ba95c610cec71a694f0c33002de836e61c1bd9b9767d014e842e083cd3eead2c text-readability-score +bb722a4416e26cc58cf3d01e5f9aaf27c61f9fa2cb86983662ee16f0c68945d9 stats-cohort-size +aefa3588c33e6977ce26d4f885d4fd75ea46ee6f479ecf1e57c1ebd33b6872a1 graph-shapley-attribution +3b63c8ae21d82da0fb47e160889fa93b34f132947c461e5c199936207a2cb6a9 econ-skm-score +1275295ec9d61cccaaeb418a580637dc6ca2d8e6b5aa25937d9471b1f534cad9 law-benefit-score diff --git a/store/SHA256.txt b/store/SHA256.txt new file mode 100644 index 0000000..d7324ae --- /dev/null +++ b/store/SHA256.txt @@ -0,0 +1,9 @@ +# .fai bundle sha256 (unsigned, chain pack 0.17.2) — 2026-06-19T00:12Z +15fa6d43bf6d18193282cb1c94c273b958e2639d91d52120eaedd83a99571765 text-akoma-normalize-0.1.0.fai +d03d3dc2bfbaa413ea4e192c6147595e21d8dafd24b5d32b853a03a38e48100d text-deontic-extract-0.1.0.fai +9bebd3dc877da9e3c89b521572000f14f007c6a9edfcc350edd44377d68acba3 graph-citation-extract-0.1.0.fai +5f1a0098bc4c3be8d20b34d848af884b539b0eda3c59ab56dfe99bf8f811090d text-readability-score-0.1.0.fai +31d35f6a7b61b9249520e3ae02fa398292375555430714b04ff0771fd2f3ad99 stats-cohort-size-0.1.0.fai +199927a6b8594da579a22d42b1ba438f4b2b6c90132a5c097c7c1d5eb8b9c319 graph-shapley-attribution-0.1.0.fai +2b887970d448c07cc53633124518d6f96672b0710d9e89fe4f9d6a236035f25e econ-skm-score-0.1.0.fai +4a82a07ba72cae4b49f1ff883b1ca92638e5f4ea19bc2b070288142056764025 law-benefit-score-0.1.0.fai diff --git a/store/store.yaml b/store/store.yaml new file mode 100644 index 0000000..42195b5 --- /dev/null +++ b/store/store.yaml @@ -0,0 +1,167 @@ +# Recl∆Im module store index — source of truth before publish. +# +# Target host (chain Studio's one-click "Recl∆Im hinzufügen" button +# points here): https://releases.chain.flemming.ai/reclaim/store.yaml +# +# STATUS (2026-06-19). Per chain: publish UNSIGNED for now — +# `require_signatures` is off by default; FAI_SIGNING_KEY untouched; +# a Recl∆Im-own signing key + per-store pinning come later. +# * sha256 — REAL, from local `chain pack` of each module. +# Bundles staged in store/bundles/, hashes mirrored +# in store/SHA256.txt. Bundles are UNSIGNED. +# * signature — intentionally absent for now (see above). +# * wasm_url — points at the intended mirror path. The ONLY +# remaining step is HOSTING the 8 .fai + this +# store.yaml at releases.chain.flemming.ai/reclaim/ +# — the mirror push is chain infra (scp to the +# Hetzner host); reclaim has no SSH access. Confirm +# the canonical wasm_url host (mirror vs Forgejo +# release) with chain. +# +# NAMING NOTE for chain: every module's `provides.capability` uses an +# UNDERSCORE (`econ.skm_score`), but this index + the Studio button use +# the DASH form (`econ.skm-score`) per the handoff. install resolution +# must map dash↔underscore, or the modules must be renamed. Flagging — +# this is a chain naming-convention decision. + +schema_version: 1 + +modules: + - name: text.akoma-normalize + versions: + - version: 0.1.0 + status: published + wasm_url: https://releases.chain.flemming.ai/reclaim/text-akoma-normalize/v0.1.0/text-akoma-normalize-0.1.0.fai + sha256: 15fa6d43bf6d18193282cb1c94c273b958e2639d91d52120eaedd83a99571765 + tagline: + en: "Normalize legal documents to Akoma Ntoso" + de: "Rechtsdokumente nach Akoma Ntoso normalisieren" + description: + en: "Normalises heterogeneous legal sources (gesetze-im-internet XML, EUR-Lex FORMEX, landesrecht-berlin HTML) into a structured Akoma Ntoso 1.0 (OASIS LegalDocML) representation." + de: "Normalisiert heterogene Rechtsquellen (gesetze-im-internet-XML, EUR-Lex-FORMEX, landesrecht-berlin-HTML) in eine strukturierte Akoma-Ntoso-1.0-Darstellung (OASIS LegalDocML)." + category: text-processing + tags: [legal, german, akoma-ntoso, offline-capable] + license: Apache-2.0 + repository: https://git.flemming.ai/chain-modules/text-akoma-normalize + + - name: text.deontic-extract + versions: + - version: 0.1.0 + status: published + wasm_url: https://releases.chain.flemming.ai/reclaim/text-deontic-extract/v0.1.0/text-deontic-extract-0.1.0.fai + sha256: d03d3dc2bfbaa413ea4e192c6147595e21d8dafd24b5d32b853a03a38e48100d + tagline: + en: "Extract duties, prohibitions and permissions from norms" + de: "Pflichten, Verbote und Erlaubnisse aus Normen extrahieren" + description: + en: "Extracts atomic deontic statements (obligation / prohibition / permission) from a normalised norm. Useful far beyond law — contracts, ISO standards, internal policies." + de: "Extrahiert atomare deontische Aussagen (Pflicht / Verbot / Erlaubnis) aus einer normalisierten Norm. Auch jenseits des Rechts nützlich — Verträge, ISO-Normen, interne Richtlinien." + category: text-processing + tags: [legal, german, nlp, offline-capable] + license: Apache-2.0 + repository: https://git.flemming.ai/chain-modules/text-deontic-extract + + - name: graph.citation-extract + versions: + - version: 0.1.0 + status: published + wasm_url: https://releases.chain.flemming.ai/reclaim/graph-citation-extract/v0.1.0/graph-citation-extract-0.1.0.fai + sha256: 9bebd3dc877da9e3c89b521572000f14f007c6a9edfcc350edd44377d68acba3 + tagline: + en: "Build typed citation graphs from legal text" + de: "Typisierte Zitationsgraphen aus Rechtstext bauen" + description: + en: "Extracts typed citations (cites, verweist_iVm, …) from German legal text per the Coupette 2021 schema. Citation graphs also fit scientific papers, patents and case law." + de: "Extrahiert typisierte Zitationen (cites, verweist_iVm, …) aus deutschem Rechtstext nach dem Coupette-2021-Schema. Zitationsgraphen passen auch zu Papers, Patenten und Rechtsprechung." + category: data + tags: [legal, german, graph, offline-capable] + license: Apache-2.0 + repository: https://git.flemming.ai/chain-modules/graph-citation-extract + + - name: text.readability-score + versions: + - version: 0.1.0 + status: published + wasm_url: https://releases.chain.flemming.ai/reclaim/text-readability-score/v0.1.0/text-readability-score-0.1.0.fai + sha256: 5f1a0098bc4c3be8d20b34d848af884b539b0eda3c59ab56dfe99bf8f811090d + tagline: + en: "Composite readability and complexity score" + de: "Komposit-Score für Lesbarkeit und Komplexität" + description: + en: "Composite readability score for any text — DE-adapted Flesch + reference depth + verb distance. Flags hard-to-read government letters, terms & conditions, abstracts." + de: "Komposit-Lesbarkeitsscore für beliebige Texte — DE-adaptierter Flesch + Verweistiefe + Verbdistanz. Markiert schwer lesbare Behördenbriefe, AGB und Abstracts." + category: text-processing + tags: [german, readability, analytics, offline-capable] + license: Apache-2.0 + repository: https://git.flemming.ai/chain-modules/text-readability-score + + - name: stats.cohort-size + versions: + - version: 0.1.0 + status: published + wasm_url: https://releases.chain.flemming.ai/reclaim/stats-cohort-size/v0.1.0/stats-cohort-size-0.1.0.fai + sha256: 31d35f6a7b61b9249520e3ae02fa398292375555430714b04ff0771fd2f3ad99 + tagline: + en: "Resolve addressee cohort sizes" + de: "Größe von Adressaten-Kohorten ermitteln" + description: + en: "Resolves the size of an addressee cohort (e.g. 'Berlin SMEs by WZ code'). v0.1 ships a stub table for the pilot cohorts; v0.2 adds a DESTATIS GENESIS-API adapter." + de: "Ermittelt die Größe einer Adressaten-Kohorte (z. B. 'Berliner KMU nach WZ-Code'). v0.1 mit Stub-Tabelle für die Pilot-Kohorten; v0.2 mit DESTATIS-GENESIS-API-Adapter." + category: data + tags: [statistics, german, government, offline-capable] + license: Apache-2.0 + repository: https://git.flemming.ai/chain-modules/stats-cohort-size + + - name: graph.shapley-attribution + versions: + - version: 0.1.0 + status: published + wasm_url: https://releases.chain.flemming.ai/reclaim/graph-shapley-attribution/v0.1.0/graph-shapley-attribution-0.1.0.fai + sha256: 199927a6b8594da579a22d42b1ba438f4b2b6c90132a5c097c7c1d5eb8b9c319 + tagline: + en: "Shapley-style attribution on weighted graphs" + de: "Shapley-Attribution auf gewichteten Graphen" + description: + en: "Splits a target node's blame or credit among its ancestors on a weighted norm/citation graph. v0.1 proportional fallback; v0.2 full Shapley value for ≤32-node neighbourhoods." + de: "Verteilt 'Schuld' oder 'Verdienst' eines Zielknotens auf seine Vorgänger in einem gewichteten Norm-/Zitationsgraphen. v0.1 proportional; v0.2 voller Shapley-Wert für ≤32-Knoten-Nachbarschaften." + category: data + tags: [graph, analytics, attribution, offline-capable] + license: Apache-2.0 + repository: https://git.flemming.ai/chain-modules/graph-shapley-attribution + + - name: econ.skm-score + versions: + - version: 0.1.0 + status: published + wasm_url: https://releases.chain.flemming.ai/reclaim/econ-skm-score/v0.1.0/econ-skm-score-0.1.0.fai + sha256: 2b887970d448c07cc53633124518d6f96672b0710d9e89fe4f9d6a236035f25e + tagline: + en: "Standard Cost Model (SKM) bureaucracy cost" + de: "Standardkostenmodell (SKM) — Bürokratiekosten" + description: + en: "Standard Cost Model — P×F×T×h per duty, summed across a duty list (German NKR method). The EU- and OECD-recognised methodology for bureaucracy-cost measurement, used by NL ATR, UK RPC, EU REFIT." + de: "Standardkostenmodell — P×F×T×h je Pflicht, summiert über eine Pflichtenliste (NKR-Methode). EU- und OECD-anerkannte Methodik zur Bürokratiekostenmessung, genutzt von NL ATR, UK RPC, EU REFIT." + category: data + tags: [economics, legal, government, offline-capable] + license: Apache-2.0 + repository: https://git.flemming.ai/chain-modules/econ-skm-score + + - name: law.benefit-score + versions: + - version: 0.1.0 + status: published + wasm_url: https://releases.chain.flemming.ai/reclaim/law-benefit-score/v0.1.0/law-benefit-score-0.1.0.fai + sha256: 4a82a07ba72cae4b49f1ff883b1ca92638e5f4ea19bc2b070288142056764025 + tagline: + en: "7-dimensional regulatory benefit score" + de: "7-dimensionaler Nutzen-Score für Regulierung" + description: + en: "Seven-dimensional benefit score (protection / market / legal-certainty / EU-single-market / safety / environment / revenue), each 0–5. Domain-bound: the dimensions are legally-politically conventioned." + de: "Siebendimensionaler Nutzen-Score (Schutz / Markt / Rechtssicherheit / EU-Binnenmarkt / Sicherheit / Umwelt / Einnahmen), je 0–5. Domänengebunden: die Dimensionen sind juristisch-politisch konventioniert." + category: data + tags: [legal, german, policy, offline-capable] + license: Apache-2.0 + repository: https://git.flemming.ai/chain-modules/law-benefit-score + +services: [] +flows: []