Compare commits

...

2 commits

Author SHA1 Message Date
bc3a001374 chore(store): refresh econ-skm-score bundle hash after float fix
Signed-off-by: flemming-it <sf@flemming.it>
2026-06-19 02:39:15 +02:00
425a0e5790 feat(live): real live mode via HubClient.submit → outputs
HubRepository.list() now runs the inline reclaim-evaluate flow per
pilot norm via submit() and maps the returned outputs to Evaluation
— the correct path, since the hub audit log stores only hashes, not
outputs. skm/benefit get their structured input as JSON (submit
jsonInputs), so the live heatmap gets real Schaden/Nutzen axes.

- live_inputs.dart: the inline eval flow + the 5 pilot norms' inputs
  (XML asset, cohort_id, skm duties, benefit ratings) plus curated
  overrides the bund adapter can't derive (true Geltungsbereich for
  the EU/Berlin norms, freshness, jurist sign-off).
- assets/norms/*.xml: 5 gesetze-im-internet-style source documents.
- tool/live_smoke.dart: dev smoke test (5/5 evaluate against a live
  hub: Schaden 3M–450M €, Nutzen 2.0–3.8, EU/Berlin scope correct).

Needs econ.skm_score's float-population fix (chain-modules commit
80f6620).

Signed-off-by: flemming-it <sf@flemming.it>
2026-06-19 02:39:15 +02:00
11 changed files with 453 additions and 110 deletions

View file

@ -0,0 +1,14 @@
<?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>

View file

@ -0,0 +1,14 @@
<?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>

View file

@ -0,0 +1,14 @@
<?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>

View file

@ -0,0 +1,14 @@
<?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>

View file

@ -0,0 +1,14 @@
<?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>

View file

@ -1,24 +1,26 @@
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 ChIn hub over gRPC. /// Live `EvaluationRepository` backed by a ChIn hub over gRPC.
/// ///
/// Reads flow.completed events from the hub's audit log, parses /// For each pilot norm ([reclaimNormInputs]) it submits the inline
/// each event's `detail` JSON as a Recl∆Im-shaped output bag /// [reclaimEvaluateFlow] via `HubClient.submit` and maps the returned
/// (see flows/durchstich-gewo-14.yaml), and converts the bag to /// `outputs` to an [Evaluation]. This is the correct path: the hub's
/// an Evaluation the UI can render. Events from non-ReclIm /// audit log stores only hashes, never the output payloads outputs
/// flows are silently skipped (detected by absence of the /// come back ONLY from the Submit response. skm/benefit get their
/// `norm` + `skm` keys). /// structured input as JSON (`submit(jsonInputs: )`), so the heatmap
/// gets real Schaden/Nutzen axes.
/// ///
/// Sort order matches MockRepository: highest /Jahr first. /// Sort order matches MockRepository: highest /Jahr first. With an
/// If the hub has zero matching flow.completed events the list /// unhealthy hub `list()` returns empty and the UI shows a 'hub-down'
/// returns empty the UI shows a "noch keine Auswertungen, ' /// empty-state no silent mock fallback.
/// `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);
@ -61,14 +63,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 e in events) { for (final input in reclaimNormInputs) {
final eval = _parse(e); try {
if (eval != null) evals.add(eval); final eval = await _evaluate(input);
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;
@ -90,94 +92,141 @@ class HubRepository implements EvaluationRepository {
String get hubLabel => '${settings.url}' String get hubLabel => '${settings.url}'
'${_healthy ? '' : ' — Verbindung gestört'}'; '${_healthy ? '' : ' — Verbindung gestört'}';
/// Decode one flow.completed event into an Evaluation, or /// Run one norm through the flow and assemble its [Evaluation].
/// return null when the event doesn't look like a Recl∆Im Future<Evaluation?> _evaluate(NormLiveInput input) async {
/// flow. We're permissive about field absence so a partially final asset = await rootBundle.load(input.assetPath);
/// failed flow run still surfaces what it produced. final content = asset.buffer.asUint8List(
Evaluation? _parse(chain.LoggedEvent event) { asset.offsetInBytes,
Map<String, dynamic>? bag; asset.lengthInBytes,
try { );
final decoded = jsonDecode(event.detail);
if (decoded is Map<String, dynamic>) bag = decoded;
} catch (_) {
return null;
}
if (bag == null) return null;
final normJson = _asMap(bag['norm']); final resp = 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 out = _decodeOutputs(resp);
final normJson = out['norm'];
if (normJson == null) return null; if (normJson == null) return null;
final skmJson = _asMap(bag['skm']);
final eli = (normJson['eli'] as String?) ?? 'eli/unknown'; final skm = out['skm'];
final benefit = out['benefit'];
final frust = out['frust'];
final cohort = out['cohort'];
final norm = Norm( final norm = Norm(
eli: eli, eli: (normJson['eli'] as String?) ?? 'eli/unknown',
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?) ?? '',
jurisdiction: JurisdictionLabel.parse( // Geltungsbereich/Freshness can't be derived from the bund
normJson['scope'] as String?, // adapter for EU/Berlin norms take the curated override.
eli: eli, jurisdiction: input.jurisdiction,
), freshness: input.freshness,
standDate: _parseTimestamp(event.timestamp), supersededNote: input.supersededNote,
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 cohortJson = _asMap(bag['cohort']); final skmEur = _asDouble(skm?['total_eur_per_year']);
final affected = _asInt(cohortJson?['count']); final tierLowest = _parseTier(skm?['tier_lowest']);
// 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: benefit, benefitScore: _asDouble(benefit?['total']),
affectedCount: affected, affectedCount: _asInt(cohort?['count']),
frustScore: frust, frustScore: _asDouble(frust?['composite_frust']),
tierLowest: tierLowest, tierLowest: tierLowest,
duties: _parseDuties(bag['duties']), duties: _parseDuties(out['duties']),
sources: _parseSources(cohortJson), sources: _liveSources(cohort),
auditEventId: event.eventId, auditEventId: _invocationId(resp),
notes: 'Live aus Hub-Flow ${event.flowName} ' reviewedBy: input.reviewedBy,
'(execution ${event.flowExecution.substring(0, 8.clamp(0, event.flowExecution.length))}).', reviewedAt: input.reviewedAt,
// Jurist sign-off is recorded by the approval, not the flow notes: 'Live aus Hub-Flow reclaim-evaluate.'
// bag. Until the review-rewrite wires approvals in, these stay '${input.supersededNote != null ? '\n${input.supersededNote}' : ''}',
// null (= unreviewed) unless the bag carries them explicitly. componentSummary: _componentSummary(skm, cohort),
reviewedBy: bag['reviewed_by'] as String?,
reviewedAt: DateTime.tryParse(
(bag['reviewed_at'] as String?) ?? ''),
); );
} }
static Freshness _parseFreshness(dynamic v) { /// Decode `SubmitResponse.outputs` into plain Dart maps. Each output
switch ((v as String?)?.trim().toLowerCase()) { /// Payload serializes (proto3-json) as `{json: {value: "<jsonstr>"}}`
case 'superseded' || 'amended' || 'geändert' || 'stale': /// (module `with_json`) or `{text: ""}`; we json-decode the inner
return Freshness.superseded; /// string.
case 'unknown' || 'ungeprüft': Map<String, Map<String, dynamic>> _decodeOutputs(chain.SubmitResponse r) {
return Freshness.unknown; final out = <String, Map<String, dynamic>>{};
default: final j = r.toProto3Json();
return Freshness.current; if (j is! Map) return out;
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 Map<String, dynamic>? _asMap(dynamic v) => static String _invocationId(chain.SubmitResponse r) {
v is Map<String, dynamic> ? v : null; try {
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();
@ -201,9 +250,6 @@ 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) {
@ -214,9 +260,8 @@ class HubRepository implements EvaluationRepository {
}).toList(); }).toList();
} }
static List<Duty> _parseDuties(dynamic v) { static List<Duty> _parseDuties(Map<String, dynamic>? v) {
final outer = _asMap(v); final inner = v?['duties'];
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(
@ -230,24 +275,4 @@ 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;
}
} }

View file

@ -0,0 +1,169 @@
import 'models.dart';
/// The ReclIm 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 § 14', 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 § 6069', 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.',
),
];

View file

@ -32,6 +32,7 @@ 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

78
app/tool/live_smoke.dart Normal file
View file

@ -0,0 +1,78 @@
// 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();
}

View file

@ -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
2b887970d448c07cc53633124518d6f96672b0710d9e89fe4f9d6a236035f25e econ-skm-score-0.1.0.fai d77afa476d4b34c22153af36c4ed3d2a0fab4c5a0dc53cba5eb217a622cd7181 econ-skm-score-0.1.0.fai
4a82a07ba72cae4b49f1ff883b1ca92638e5f4ea19bc2b070288142056764025 law-benefit-score-0.1.0.fai 4a82a07ba72cae4b49f1ff883b1ca92638e5f4ea19bc2b070288142056764025 law-benefit-score-0.1.0.fai

View file

@ -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: 2b887970d448c07cc53633124518d6f96672b0710d9e89fe4f9d6a236035f25e sha256: d77afa476d4b34c22153af36c4ed3d2a0fab4c5a0dc53cba5eb217a622cd7181
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"