Compare commits
No commits in common. "bc3a0013747743f39f16774748310de6ac977aba" and "46b4003d3681c930831614a002536f233b03de5a" have entirely different histories.
bc3a001374
...
46b4003d36
11 changed files with 110 additions and 453 deletions
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<dokumente>
|
|
||||||
<norm doknr="jlr-BauOBE2005V25P60">
|
|
||||||
<metadaten>
|
|
||||||
<jurabk>BauO Bln</jurabk>
|
|
||||||
<enbez>§ 60 ff</enbez>
|
|
||||||
<titel>Bauantrag</titel>
|
|
||||||
</metadaten>
|
|
||||||
<textdaten><text>
|
|
||||||
(1) Für die Errichtung, Änderung und Nutzungsänderung baulicher Anlagen ist die Baugenehmigung erforderlich, soweit in den §§ 61 bis 65 sowie 76 und 77 nichts anderes bestimmt ist.
|
|
||||||
(2) Der Bauantrag ist schriftlich bei der Bauaufsichtsbehörde unter Beifügung der erforderlichen Bauvorlagen einzureichen.
|
|
||||||
</text></textdaten>
|
|
||||||
</norm>
|
|
||||||
</dokumente>
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<dokumente>
|
|
||||||
<norm doknr="CELEX32016R0679A30">
|
|
||||||
<metadaten>
|
|
||||||
<jurabk>DSGVO</jurabk>
|
|
||||||
<enbez>Art. 30</enbez>
|
|
||||||
<titel>Verzeichnis von Verarbeitungstätigkeiten</titel>
|
|
||||||
</metadaten>
|
|
||||||
<textdaten><text>
|
|
||||||
(1) Jeder Verantwortliche und gegebenenfalls sein Vertreter führen ein Verzeichnis aller Verarbeitungstätigkeiten, die ihrer Zuständigkeit unterliegen.
|
|
||||||
(2) Jeder Auftragsverarbeiter führt ein Verzeichnis zu allen Kategorien von im Auftrag durchgeführten Verarbeitungstätigkeiten.
|
|
||||||
</text></textdaten>
|
|
||||||
</norm>
|
|
||||||
</dokumente>
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<dokumente>
|
|
||||||
<norm doknr="BJNR000020869BJNE002500314">
|
|
||||||
<metadaten>
|
|
||||||
<jurabk>GewO</jurabk>
|
|
||||||
<enbez>§ 14</enbez>
|
|
||||||
<titel>Anzeigepflicht</titel>
|
|
||||||
</metadaten>
|
|
||||||
<textdaten><text>
|
|
||||||
(1) Wer den selbstständigen Betrieb eines stehenden Gewerbes anfängt, muss dies der zuständigen Behörde gleichzeitig anzeigen.
|
|
||||||
(2) Die Anzeige hat unverzüglich zu erfolgen. Sie ist nach amtlich vorgeschriebenem Vordruck zu erstatten.
|
|
||||||
</text></textdaten>
|
|
||||||
</norm>
|
|
||||||
</dokumente>
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<dokumente>
|
|
||||||
<norm doknr="BJNR396000016">
|
|
||||||
<metadaten>
|
|
||||||
<jurabk>KassenSichV</jurabk>
|
|
||||||
<enbez>§ 1 ff</enbez>
|
|
||||||
<titel>Kassensicherungsverordnung</titel>
|
|
||||||
</metadaten>
|
|
||||||
<textdaten><text>
|
|
||||||
(1) Das elektronische Aufzeichnungssystem muss jeden einzelnen aufzeichnungspflichtigen Vorgang vollständig aufzeichnen. Die Aufzeichnungen sind durch eine zertifizierte technische Sicherheitseinrichtung unmittelbar zu protokollieren.
|
|
||||||
(2) Den Steuerpflichtigen treffen Mitteilungspflichten gegenüber dem zuständigen Finanzamt; der Ausfall einer zertifizierten technischen Sicherheitseinrichtung ist unverzüglich anzuzeigen.
|
|
||||||
</text></textdaten>
|
|
||||||
</norm>
|
|
||||||
</dokumente>
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<dokumente>
|
|
||||||
<norm doknr="BJNR181010014BJNE001700000">
|
|
||||||
<metadaten>
|
|
||||||
<jurabk>MiLoG</jurabk>
|
|
||||||
<enbez>§ 17</enbez>
|
|
||||||
<titel>Dokumentationspflichten</titel>
|
|
||||||
</metadaten>
|
|
||||||
<textdaten><text>
|
|
||||||
(1) Ein Arbeitgeber, der Arbeitnehmer geringfügig oder in den Wirtschaftsbereichen des Schwarzarbeitsbekämpfungsgesetzes beschäftigt, ist verpflichtet, Beginn, Ende und Dauer der täglichen Arbeitszeit aufzuzeichnen und diese Aufzeichnungen mindestens zwei Jahre aufzubewahren.
|
|
||||||
(2) Die Unterlagen sind im Inland in deutscher Sprache bereitzuhalten.
|
|
||||||
</text></textdaten>
|
|
||||||
</norm>
|
|
||||||
</dokumente>
|
|
||||||
|
|
@ -1,26 +1,24 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:chain_client_sdk/chain_client_sdk.dart' as chain;
|
import 'package:chain_client_sdk/chain_client_sdk.dart' as chain;
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
|
||||||
|
|
||||||
import 'hub_endpoint.dart';
|
import 'hub_endpoint.dart';
|
||||||
import 'live_inputs.dart';
|
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
import 'repository.dart';
|
import 'repository.dart';
|
||||||
|
|
||||||
/// Live `EvaluationRepository` backed by a Ch∆In hub over gRPC.
|
/// Live `EvaluationRepository` backed by a Ch∆In hub over gRPC.
|
||||||
///
|
///
|
||||||
/// For each pilot norm ([reclaimNormInputs]) it submits the inline
|
/// Reads flow.completed events from the hub's audit log, parses
|
||||||
/// [reclaimEvaluateFlow] via `HubClient.submit` and maps the returned
|
/// each event's `detail` JSON as a Recl∆Im-shaped output bag
|
||||||
/// `outputs` to an [Evaluation]. This is the correct path: the hub's
|
/// (see flows/durchstich-gewo-14.yaml), and converts the bag to
|
||||||
/// audit log stores only hashes, never the output payloads — outputs
|
/// an Evaluation the UI can render. Events from non-Recl∆Im
|
||||||
/// come back ONLY from the Submit response. skm/benefit get their
|
/// flows are silently skipped (detected by absence of the
|
||||||
/// structured input as JSON (`submit(jsonInputs: …)`), so the heatmap
|
/// `norm` + `skm` keys).
|
||||||
/// gets real Schaden/Nutzen axes.
|
|
||||||
///
|
///
|
||||||
/// Sort order matches MockRepository: highest €/Jahr first. With an
|
/// Sort order matches MockRepository: highest €/Jahr first.
|
||||||
/// unhealthy hub `list()` returns empty and the UI shows a 'hub-down'
|
/// If the hub has zero matching flow.completed events the list
|
||||||
/// empty-state — no silent mock fallback.
|
/// returns empty — the UI shows a "noch keine Auswertungen, '
|
||||||
|
/// `chain run flows/durchstich-gewo-14.yaml` aufrufen"-Hinweis.
|
||||||
class HubRepository implements EvaluationRepository {
|
class HubRepository implements EvaluationRepository {
|
||||||
HubRepository._(this._client, this.settings, this._healthy);
|
HubRepository._(this._client, this.settings, this._healthy);
|
||||||
|
|
||||||
|
|
@ -63,14 +61,14 @@ class HubRepository implements EvaluationRepository {
|
||||||
@override
|
@override
|
||||||
Future<List<Evaluation>> list() async {
|
Future<List<Evaluation>> list() async {
|
||||||
if (!_healthy) return const [];
|
if (!_healthy) return const [];
|
||||||
|
final events = await _client.eventLog(
|
||||||
|
limit: 200,
|
||||||
|
types: const ['flow.completed'],
|
||||||
|
);
|
||||||
final evals = <Evaluation>[];
|
final evals = <Evaluation>[];
|
||||||
for (final input in reclaimNormInputs) {
|
for (final e in events) {
|
||||||
try {
|
final eval = _parse(e);
|
||||||
final eval = await _evaluate(input);
|
|
||||||
if (eval != null) evals.add(eval);
|
if (eval != null) evals.add(eval);
|
||||||
} catch (_) {
|
|
||||||
// One norm failing must not blank the whole heatmap.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
evals.sort((a, b) => b.skmEurPerYear.compareTo(a.skmEurPerYear));
|
evals.sort((a, b) => b.skmEurPerYear.compareTo(a.skmEurPerYear));
|
||||||
return evals;
|
return evals;
|
||||||
|
|
@ -92,141 +90,94 @@ class HubRepository implements EvaluationRepository {
|
||||||
String get hubLabel => '${settings.url}'
|
String get hubLabel => '${settings.url}'
|
||||||
'${_healthy ? '' : ' — Verbindung gestört'}';
|
'${_healthy ? '' : ' — Verbindung gestört'}';
|
||||||
|
|
||||||
/// Run one norm through the flow and assemble its [Evaluation].
|
/// Decode one flow.completed event into an Evaluation, or
|
||||||
Future<Evaluation?> _evaluate(NormLiveInput input) async {
|
/// return null when the event doesn't look like a Recl∆Im
|
||||||
final asset = await rootBundle.load(input.assetPath);
|
/// flow. We're permissive about field absence so a partially
|
||||||
final content = asset.buffer.asUint8List(
|
/// failed flow run still surfaces what it produced.
|
||||||
asset.offsetInBytes,
|
Evaluation? _parse(chain.LoggedEvent event) {
|
||||||
asset.lengthInBytes,
|
Map<String, dynamic>? bag;
|
||||||
);
|
try {
|
||||||
|
final decoded = jsonDecode(event.detail);
|
||||||
|
if (decoded is Map<String, dynamic>) bag = decoded;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (bag == null) return null;
|
||||||
|
|
||||||
final resp = await _client.submit(
|
final normJson = _asMap(bag['norm']);
|
||||||
flowYaml: reclaimEvaluateFlow,
|
|
||||||
fileInputs: {'content': content},
|
|
||||||
fileMimeTypes: {'content': 'application/xml'},
|
|
||||||
textInputs: {'cohort_id': input.cohortId},
|
|
||||||
jsonInputs: {'duties': input.duties, 'ratings': input.ratings},
|
|
||||||
);
|
|
||||||
|
|
||||||
final out = _decodeOutputs(resp);
|
|
||||||
final normJson = out['norm'];
|
|
||||||
if (normJson == null) return null;
|
if (normJson == null) return null;
|
||||||
|
final skmJson = _asMap(bag['skm']);
|
||||||
|
|
||||||
final skm = out['skm'];
|
final eli = (normJson['eli'] as String?) ?? 'eli/unknown';
|
||||||
final benefit = out['benefit'];
|
|
||||||
final frust = out['frust'];
|
|
||||||
final cohort = out['cohort'];
|
|
||||||
|
|
||||||
final norm = Norm(
|
final norm = Norm(
|
||||||
eli: (normJson['eli'] as String?) ?? 'eli/unknown',
|
eli: eli,
|
||||||
title: (normJson['title'] as String?) ?? '(ohne Titel)',
|
title: (normJson['title'] as String?) ?? '(ohne Titel)',
|
||||||
jurabk: (normJson['jurabk'] as String?) ?? '',
|
jurabk: (normJson['jurabk'] as String?) ?? '',
|
||||||
paragraph: (normJson['paragraph'] as String?) ?? '',
|
paragraph: (normJson['paragraph'] as String?) ?? '',
|
||||||
// Geltungsbereich/Freshness can't be derived from the bund
|
jurisdiction: JurisdictionLabel.parse(
|
||||||
// adapter for EU/Berlin norms — take the curated override.
|
normJson['scope'] as String?,
|
||||||
jurisdiction: input.jurisdiction,
|
eli: eli,
|
||||||
freshness: input.freshness,
|
),
|
||||||
supersededNote: input.supersededNote,
|
standDate: _parseTimestamp(event.timestamp),
|
||||||
standDate: DateTime.now(),
|
|
||||||
sourceUrl: (normJson['source_url'] as String?) ?? '',
|
sourceUrl: (normJson['source_url'] as String?) ?? '',
|
||||||
sourceSha256: (normJson['source_sha256'] as String?) ?? '',
|
sourceSha256: (normJson['source_sha256'] as String?) ?? '',
|
||||||
|
freshness: _parseFreshness(normJson['freshness']),
|
||||||
|
supersededNote: normJson['superseded_note'] as String?,
|
||||||
paragraphs: _parseParagraphs(normJson['paragraphs']),
|
paragraphs: _parseParagraphs(normJson['paragraphs']),
|
||||||
);
|
);
|
||||||
|
|
||||||
final skmEur = _asDouble(skm?['total_eur_per_year']);
|
final cohortJson = _asMap(bag['cohort']);
|
||||||
final tierLowest = _parseTier(skm?['tier_lowest']);
|
final affected = _asInt(cohortJson?['count']);
|
||||||
|
|
||||||
|
// SKM may be missing if the flow only ran the normalize-/
|
||||||
|
// cohort-arm (e.g. flows/durchstich-from-file.yaml). Default
|
||||||
|
// to zero and let the tier fall back to the cohort source's
|
||||||
|
// tier — that's still the most-honest reading.
|
||||||
|
final skmEur = _asDouble(skmJson?['total_eur_per_year']);
|
||||||
|
final tierLowest = skmJson != null
|
||||||
|
? _parseTier(skmJson['tier_lowest'])
|
||||||
|
: _parseTier(cohortJson?['tier']);
|
||||||
|
|
||||||
|
final benefitJson = _asMap(bag['benefit']);
|
||||||
|
final benefit = _asDouble(benefitJson?['total']);
|
||||||
|
|
||||||
|
final frustJson = _asMap(bag['frust']);
|
||||||
|
final frust = _asDouble(frustJson?['composite_frust']);
|
||||||
|
|
||||||
return Evaluation(
|
return Evaluation(
|
||||||
norm: norm,
|
norm: norm,
|
||||||
skmEurPerYear: skmEur,
|
skmEurPerYear: skmEur,
|
||||||
benefitScore: _asDouble(benefit?['total']),
|
benefitScore: benefit,
|
||||||
affectedCount: _asInt(cohort?['count']),
|
affectedCount: affected,
|
||||||
frustScore: _asDouble(frust?['composite_frust']),
|
frustScore: frust,
|
||||||
tierLowest: tierLowest,
|
tierLowest: tierLowest,
|
||||||
duties: _parseDuties(out['duties']),
|
duties: _parseDuties(bag['duties']),
|
||||||
sources: _liveSources(cohort),
|
sources: _parseSources(cohortJson),
|
||||||
auditEventId: _invocationId(resp),
|
auditEventId: event.eventId,
|
||||||
reviewedBy: input.reviewedBy,
|
notes: 'Live aus Hub-Flow ${event.flowName} '
|
||||||
reviewedAt: input.reviewedAt,
|
'(execution ${event.flowExecution.substring(0, 8.clamp(0, event.flowExecution.length))}).',
|
||||||
notes: 'Live aus Hub-Flow reclaim-evaluate.'
|
// Jurist sign-off is recorded by the approval, not the flow
|
||||||
'${input.supersededNote != null ? '\n${input.supersededNote}' : ''}',
|
// bag. Until the review-rewrite wires approvals in, these stay
|
||||||
componentSummary: _componentSummary(skm, cohort),
|
// null (= unreviewed) unless the bag carries them explicitly.
|
||||||
|
reviewedBy: bag['reviewed_by'] as String?,
|
||||||
|
reviewedAt: DateTime.tryParse(
|
||||||
|
(bag['reviewed_at'] as String?) ?? ''),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode `SubmitResponse.outputs` into plain Dart maps. Each output
|
static Freshness _parseFreshness(dynamic v) {
|
||||||
/// Payload serializes (proto3-json) as `{json: {value: "<jsonstr>"}}`
|
switch ((v as String?)?.trim().toLowerCase()) {
|
||||||
/// (module `with_json`) or `{text: "…"}`; we json-decode the inner
|
case 'superseded' || 'amended' || 'geändert' || 'stale':
|
||||||
/// string.
|
return Freshness.superseded;
|
||||||
Map<String, Map<String, dynamic>> _decodeOutputs(chain.SubmitResponse r) {
|
case 'unknown' || 'ungeprüft':
|
||||||
final out = <String, Map<String, dynamic>>{};
|
return Freshness.unknown;
|
||||||
final j = r.toProto3Json();
|
default:
|
||||||
if (j is! Map) return out;
|
return Freshness.current;
|
||||||
final outputs = j['outputs'];
|
|
||||||
if (outputs is! Map) return out;
|
|
||||||
for (final entry in outputs.entries) {
|
|
||||||
final v = entry.value;
|
|
||||||
String? raw;
|
|
||||||
if (v is Map) {
|
|
||||||
final js = v['json'];
|
|
||||||
if (js is Map && js['value'] is String) {
|
|
||||||
raw = js['value'] as String;
|
|
||||||
} else if (v['text'] is String) {
|
|
||||||
raw = v['text'] as String;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (raw == null) continue;
|
|
||||||
try {
|
|
||||||
final decoded = jsonDecode(raw);
|
|
||||||
if (decoded is Map<String, dynamic>) out[entry.key] = decoded;
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String _invocationId(chain.SubmitResponse r) {
|
static Map<String, dynamic>? _asMap(dynamic v) =>
|
||||||
try {
|
v is Map<String, dynamic> ? v : null;
|
||||||
final j = r.toProto3Json();
|
|
||||||
if (j is Map) {
|
|
||||||
final inv = j['invocationId'];
|
|
||||||
if (inv is Map && inv['value'] is String) return inv['value'] as String;
|
|
||||||
if (inv is String) return inv;
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
return 'live-${DateTime.now().millisecondsSinceEpoch}';
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<Source> _liveSources(Map<String, dynamic>? cohort) {
|
|
||||||
final out = <Source>[];
|
|
||||||
final label = cohort?['source'] as String?;
|
|
||||||
if (label != null && label.isNotEmpty) {
|
|
||||||
out.add(Source(
|
|
||||||
label: label,
|
|
||||||
url: '',
|
|
||||||
tier: _parseTier(cohort?['tier']),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<String> _componentSummary(
|
|
||||||
Map<String, dynamic>? skm,
|
|
||||||
Map<String, dynamic>? cohort,
|
|
||||||
) {
|
|
||||||
final lines = <String>[];
|
|
||||||
final items = skm?['items'];
|
|
||||||
if (items is List && items.isNotEmpty) {
|
|
||||||
final first = items.first;
|
|
||||||
if (first is Map && first['formula'] is String) {
|
|
||||||
lines.add('SKM · ${first['formula']}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final src = cohort?['source'] as String?;
|
|
||||||
if (src != null && src.isNotEmpty) {
|
|
||||||
lines.add('P · ${_parseTier(cohort?['tier']).short} — $src');
|
|
||||||
}
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
static double _asDouble(dynamic v) {
|
static double _asDouble(dynamic v) {
|
||||||
if (v is num) return v.toDouble();
|
if (v is num) return v.toDouble();
|
||||||
|
|
@ -250,6 +201,9 @@ class HubRepository implements EvaluationRepository {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static DateTime _parseTimestamp(String iso) =>
|
||||||
|
DateTime.tryParse(iso) ?? DateTime.now();
|
||||||
|
|
||||||
static List<NormParagraph> _parseParagraphs(dynamic v) {
|
static List<NormParagraph> _parseParagraphs(dynamic v) {
|
||||||
if (v is! List) return const [];
|
if (v is! List) return const [];
|
||||||
return v.whereType<Map<String, dynamic>>().map((p) {
|
return v.whereType<Map<String, dynamic>>().map((p) {
|
||||||
|
|
@ -260,8 +214,9 @@ class HubRepository implements EvaluationRepository {
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Duty> _parseDuties(Map<String, dynamic>? v) {
|
static List<Duty> _parseDuties(dynamic v) {
|
||||||
final inner = v?['duties'];
|
final outer = _asMap(v);
|
||||||
|
final inner = outer?['duties'];
|
||||||
if (inner is! List) return const [];
|
if (inner is! List) return const [];
|
||||||
return inner.whereType<Map<String, dynamic>>().map((d) {
|
return inner.whereType<Map<String, dynamic>>().map((d) {
|
||||||
return Duty(
|
return Duty(
|
||||||
|
|
@ -275,4 +230,24 @@ class HubRepository implements EvaluationRepository {
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hub-mode sources are minimal: we surface the cohort source
|
||||||
|
/// as a T1/T2 reference (the field is set by stats.cohort_size)
|
||||||
|
/// plus an audit-log pointer. Richer source attribution comes
|
||||||
|
/// when the flow has a dedicated `sources.collect` step.
|
||||||
|
static List<Source> _parseSources(Map<String, dynamic>? cohort) {
|
||||||
|
final out = <Source>[];
|
||||||
|
final label = cohort?['source'] as String?;
|
||||||
|
final tierStr = (cohort?['tier'] as String?)?.toUpperCase() ?? 'T2';
|
||||||
|
if (label != null && label.isNotEmpty) {
|
||||||
|
final tier = switch (tierStr) {
|
||||||
|
'T1' => EvidenceTier.t1,
|
||||||
|
'T2' => EvidenceTier.t2,
|
||||||
|
'T3' => EvidenceTier.t3,
|
||||||
|
_ => EvidenceTier.t4,
|
||||||
|
};
|
||||||
|
out.add(Source(label: label, url: '', tier: tier));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
import 'models.dart';
|
|
||||||
|
|
||||||
/// The Recl∆Im evaluation flow, submitted inline per norm via
|
|
||||||
/// `HubClient.submit`. No `system.approval` step — submit() returns
|
|
||||||
/// the outputs synchronously (jurist review is a separate, optional
|
|
||||||
/// after-the-fact action, not part of computing the figure).
|
|
||||||
const String reclaimEvaluateFlow = r'''
|
|
||||||
name: reclaim-evaluate
|
|
||||||
inputs:
|
|
||||||
content: bytes
|
|
||||||
cohort_id: text
|
|
||||||
duties: json
|
|
||||||
ratings: json
|
|
||||||
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
|
|
||||||
- id: skm
|
|
||||||
use: econ.skm_score@^0
|
|
||||||
with:
|
|
||||||
duties: $inputs.duties
|
|
||||||
- id: benefit
|
|
||||||
use: law.benefit_score@^0
|
|
||||||
with:
|
|
||||||
ratings: $inputs.ratings
|
|
||||||
outputs:
|
|
||||||
norm: $normalize.norm
|
|
||||||
skm: $skm.report
|
|
||||||
benefit: $benefit.score
|
|
||||||
frust: $frust.score
|
|
||||||
cohort: $cohort.cohort
|
|
||||||
duties: $deontic.duties
|
|
||||||
citations: $citations.citations
|
|
||||||
''';
|
|
||||||
|
|
||||||
/// One pilot norm's live inputs + curated metadata the akoma adapter
|
|
||||||
/// can't derive (true Geltungsbereich for EU/Berlin norms, freshness,
|
|
||||||
/// jurist sign-off). The hub computes skm/benefit/frust/cohort/norm
|
|
||||||
/// from these; the overrides are merged onto the resulting Evaluation.
|
|
||||||
class NormLiveInput {
|
|
||||||
const NormLiveInput({
|
|
||||||
required this.assetPath,
|
|
||||||
required this.cohortId,
|
|
||||||
required this.duties,
|
|
||||||
required this.ratings,
|
|
||||||
required this.jurisdiction,
|
|
||||||
this.freshness = Freshness.current,
|
|
||||||
this.supersededNote,
|
|
||||||
this.reviewedBy,
|
|
||||||
this.reviewedAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String assetPath;
|
|
||||||
final String cohortId;
|
|
||||||
|
|
||||||
/// `{ duties: [ { population, frequency, tariff_eur_per_hour,
|
|
||||||
/// time_hours_per_case, source_norm, tier } ] }` for econ.skm_score.
|
|
||||||
final Map<String, Object?> duties;
|
|
||||||
|
|
||||||
/// `{ schutz, markt, rechtssicherheit, eu_binnenmarkt, sicherheit,
|
|
||||||
/// umwelt, einnahmen }` (each 0..5) for law.benefit_score.
|
|
||||||
final Map<String, Object?> ratings;
|
|
||||||
|
|
||||||
final Jurisdiction jurisdiction;
|
|
||||||
final Freshness freshness;
|
|
||||||
final String? supersededNote;
|
|
||||||
final String? reviewedBy;
|
|
||||||
final DateTime? reviewedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object?> _duty(
|
|
||||||
String norm,
|
|
||||||
double population,
|
|
||||||
double frequency,
|
|
||||||
double tariff,
|
|
||||||
double time,
|
|
||||||
String tier,
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
'duties': [
|
|
||||||
{
|
|
||||||
'population': population,
|
|
||||||
'frequency': frequency,
|
|
||||||
'tariff_eur_per_hour': tariff,
|
|
||||||
'time_hours_per_case': time,
|
|
||||||
'source_norm': norm,
|
|
||||||
'tier': tier,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
Map<String, Object?> _ratings(double schutz, double markt, double rs,
|
|
||||||
double eu, double sicherheit, double umwelt, double einnahmen) =>
|
|
||||||
{
|
|
||||||
'schutz': schutz,
|
|
||||||
'markt': markt,
|
|
||||||
'rechtssicherheit': rs,
|
|
||||||
'eu_binnenmarkt': eu,
|
|
||||||
'sicherheit': sicherheit,
|
|
||||||
'umwelt': umwelt,
|
|
||||||
'einnahmen': einnahmen,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The five pilot norms, live-evaluated against the hub.
|
|
||||||
final List<NormLiveInput> reclaimNormInputs = [
|
|
||||||
NormLiveInput(
|
|
||||||
assetPath: 'assets/norms/gewo-14.xml',
|
|
||||||
cohortId: 'berlin-kmu',
|
|
||||||
duties: _duty('GewO § 14', 180000, 1.0, 35.0, 0.5, 'T3'),
|
|
||||||
ratings: _ratings(2, 3, 3.5, 2, 1.5, 0, 2),
|
|
||||||
jurisdiction: Jurisdiction.deutschland,
|
|
||||||
),
|
|
||||||
NormLiveInput(
|
|
||||||
assetPath: 'assets/norms/kassensichv.xml',
|
|
||||||
cohortId: 'gastro-de',
|
|
||||||
duties: _duty('KassenSichV § 1–4', 500000, 1.0, 40.0, 1.25, 'T2'),
|
|
||||||
ratings: _ratings(3, 3, 3.5, 2.5, 3, 1, 5),
|
|
||||||
jurisdiction: Jurisdiction.deutschland,
|
|
||||||
reviewedBy: 'RA Dr. M. Kessler',
|
|
||||||
reviewedAt: DateTime.utc(2026, 1, 20),
|
|
||||||
),
|
|
||||||
NormLiveInput(
|
|
||||||
assetPath: 'assets/norms/dsgvo-art30.xml',
|
|
||||||
cohortId: 'de-kmu',
|
|
||||||
duties: _duty('DSGVO Art. 30', 3000000, 1.0, 50.0, 3.0, 'T1'),
|
|
||||||
ratings: _ratings(4, 4, 4.5, 4, 3, 2, 5),
|
|
||||||
jurisdiction: Jurisdiction.eu,
|
|
||||||
reviewedBy: 'Prof. Dr. A. Lindner',
|
|
||||||
reviewedAt: DateTime.utc(2026, 2, 3),
|
|
||||||
),
|
|
||||||
NormLiveInput(
|
|
||||||
assetPath: 'assets/norms/bauo-bln-60.xml',
|
|
||||||
cohortId: 'bauwirtschaft-be',
|
|
||||||
duties: _duty('BauO Bln § 60–69', 15000, 1.0, 60.0, 80.0, 'T2'),
|
|
||||||
ratings: _ratings(4, 3, 4, 2, 4, 3, 4),
|
|
||||||
jurisdiction: Jurisdiction.berlin,
|
|
||||||
reviewedBy: 'RAin S. Brandt',
|
|
||||||
reviewedAt: DateTime.utc(2026, 1, 28),
|
|
||||||
),
|
|
||||||
NormLiveInput(
|
|
||||||
assetPath: 'assets/norms/milog-17.xml',
|
|
||||||
cohortId: 'de-kmu',
|
|
||||||
duties: _duty('MiLoG § 17', 3000000, 220.0, 30.0, 0.014, 'T1'),
|
|
||||||
ratings: _ratings(4, 3, 4, 2, 3, 1, 5),
|
|
||||||
jurisdiction: Jurisdiction.deutschland,
|
|
||||||
freshness: Freshness.superseded,
|
|
||||||
supersededNote: 'BEG IV hat die Dokumentationspflichten zum '
|
|
||||||
'1.1.2026 entschärft — diese Fassung ist neu zu bewerten.',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
@ -32,7 +32,6 @@ flutter:
|
||||||
|
|
||||||
assets:
|
assets:
|
||||||
- assets/fixtures/
|
- assets/fixtures/
|
||||||
- assets/norms/
|
|
||||||
|
|
||||||
# Bundled fonts — offline-first, KRITIS-compatible, no Google
|
# Bundled fonts — offline-first, KRITIS-compatible, no Google
|
||||||
# Fonts runtime fetch. setup.sh downloads SIL-OFL-licensed TTFs
|
# Fonts runtime fetch. setup.sh downloads SIL-OFL-licensed TTFs
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
// Smoke test for the live submit() path — all 5 pilot norms.
|
|
||||||
// dart run tool/live_smoke.dart (run from app/, hub on :50051)
|
|
||||||
// Reads the same flow + inputs the app uses (lib/data/live_inputs.dart),
|
|
||||||
// loads XML from assets via File (no Flutter binding here).
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:chain_client_sdk/chain_client_sdk.dart' as chain;
|
|
||||||
|
|
||||||
import 'package:reclaim_app/data/live_inputs.dart';
|
|
||||||
|
|
||||||
Map<String, Map<String, dynamic>> decode(chain.SubmitResponse r) {
|
|
||||||
final out = <String, Map<String, dynamic>>{};
|
|
||||||
final j = r.toProto3Json();
|
|
||||||
if (j is! Map) return out;
|
|
||||||
final outputs = j['outputs'];
|
|
||||||
if (outputs is! Map) return out;
|
|
||||||
for (final e in outputs.entries) {
|
|
||||||
final v = e.value;
|
|
||||||
String? raw;
|
|
||||||
if (v is Map) {
|
|
||||||
final js = v['json'];
|
|
||||||
if (js is Map && js['value'] is String) {
|
|
||||||
raw = js['value'] as String;
|
|
||||||
} else if (v['text'] is String) {
|
|
||||||
raw = v['text'] as String;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (raw == null) continue;
|
|
||||||
try {
|
|
||||||
final d = jsonDecode(raw);
|
|
||||||
if (d is Map<String, dynamic>) out[e.key] = d;
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> main() async {
|
|
||||||
final client = chain.HubClient(
|
|
||||||
endpoint: const chain.HubEndpoint(
|
|
||||||
host: '127.0.0.1', port: 50051, secure: false),
|
|
||||||
);
|
|
||||||
stderr.writeln('hub healthy: ${await client.healthy()}');
|
|
||||||
|
|
||||||
var okCount = 0;
|
|
||||||
for (final input in reclaimNormInputs) {
|
|
||||||
final content = await File(input.assetPath).readAsBytes();
|
|
||||||
try {
|
|
||||||
final r = await client.submit(
|
|
||||||
flowYaml: reclaimEvaluateFlow,
|
|
||||||
fileInputs: {'content': content},
|
|
||||||
fileMimeTypes: {'content': 'application/xml'},
|
|
||||||
textInputs: {'cohort_id': input.cohortId},
|
|
||||||
jsonInputs: {'duties': input.duties, 'ratings': input.ratings},
|
|
||||||
);
|
|
||||||
final o = decode(r);
|
|
||||||
final norm = o['norm'] ?? {};
|
|
||||||
final skm = o['skm'] ?? {};
|
|
||||||
final benefit = o['benefit'] ?? {};
|
|
||||||
final cohort = o['cohort'] ?? {};
|
|
||||||
final frust = o['frust'] ?? {};
|
|
||||||
stdout.writeln(
|
|
||||||
'✔ ${norm['jurabk']} ${norm['paragraph']} '
|
|
||||||
'[${input.jurisdiction.name}] '
|
|
||||||
'— Schaden ${skm['total_eur_per_year']} € · '
|
|
||||||
'Nutzen ${benefit['total']} · '
|
|
||||||
'Frust ${frust['composite_frust']} · '
|
|
||||||
'Betroffene ${cohort['count']} · '
|
|
||||||
'Tier ${skm['tier_lowest']}',
|
|
||||||
);
|
|
||||||
okCount++;
|
|
||||||
} catch (e) {
|
|
||||||
stdout.writeln('✗ ${input.assetPath}: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stdout.writeln('--- $okCount/${reclaimNormInputs.length} evaluated ---');
|
|
||||||
await client.close();
|
|
||||||
}
|
|
||||||
|
|
@ -5,5 +5,5 @@ d03d3dc2bfbaa413ea4e192c6147595e21d8dafd24b5d32b853a03a38e48100d text-deontic-e
|
||||||
5f1a0098bc4c3be8d20b34d848af884b539b0eda3c59ab56dfe99bf8f811090d text-readability-score-0.1.0.fai
|
5f1a0098bc4c3be8d20b34d848af884b539b0eda3c59ab56dfe99bf8f811090d text-readability-score-0.1.0.fai
|
||||||
31d35f6a7b61b9249520e3ae02fa398292375555430714b04ff0771fd2f3ad99 stats-cohort-size-0.1.0.fai
|
31d35f6a7b61b9249520e3ae02fa398292375555430714b04ff0771fd2f3ad99 stats-cohort-size-0.1.0.fai
|
||||||
199927a6b8594da579a22d42b1ba438f4b2b6c90132a5c097c7c1d5eb8b9c319 graph-shapley-attribution-0.1.0.fai
|
199927a6b8594da579a22d42b1ba438f4b2b6c90132a5c097c7c1d5eb8b9c319 graph-shapley-attribution-0.1.0.fai
|
||||||
d77afa476d4b34c22153af36c4ed3d2a0fab4c5a0dc53cba5eb217a622cd7181 econ-skm-score-0.1.0.fai
|
2b887970d448c07cc53633124518d6f96672b0710d9e89fe4f9d6a236035f25e econ-skm-score-0.1.0.fai
|
||||||
4a82a07ba72cae4b49f1ff883b1ca92638e5f4ea19bc2b070288142056764025 law-benefit-score-0.1.0.fai
|
4a82a07ba72cae4b49f1ff883b1ca92638e5f4ea19bc2b070288142056764025 law-benefit-score-0.1.0.fai
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ modules:
|
||||||
- version: 0.1.0
|
- version: 0.1.0
|
||||||
status: published
|
status: published
|
||||||
wasm_url: https://releases.chain.flemming.ai/reclaim/econ-skm-score/v0.1.0/econ-skm-score-0.1.0.fai
|
wasm_url: https://releases.chain.flemming.ai/reclaim/econ-skm-score/v0.1.0/econ-skm-score-0.1.0.fai
|
||||||
sha256: d77afa476d4b34c22153af36c4ed3d2a0fab4c5a0dc53cba5eb217a622cd7181
|
sha256: 2b887970d448c07cc53633124518d6f96672b0710d9e89fe4f9d6a236035f25e
|
||||||
tagline:
|
tagline:
|
||||||
en: "Standard Cost Model (SKM) bureaucracy cost"
|
en: "Standard Cost Model (SKM) bureaucracy cost"
|
||||||
de: "Standardkostenmodell (SKM) — Bürokratiekosten"
|
de: "Standardkostenmodell (SKM) — Bürokratiekosten"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue