From c94b55e5399e2106372712d2b2b877b3e56256bb Mon Sep 17 00:00:00 2001 From: flemming-it Date: Thu, 18 Jun 2026 11:25:47 +0200 Subject: [PATCH] feat: initial law-benefit-score v0.1.0 (law.benefit_score@0.1.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven-dimensional benefit score aggregator (Studie §3.2): schutz · markt · rechtssicherheit · eu_binnenmarkt sicherheit · umwelt · einnahmen Each dimension is 0..5. Output carries the mean total (also 0..5) plus the dispersion (standard deviation across the seven dimensions), so the UI can flag "high mean but split panel" cases where a juristisch-politisch konventionierte Dimension dominates and others disagree. Per-dimension sources can be passed via the optional _sources sub-object so the audit log preserves the "hier kommt jede 5 her"-Beleg. This is the one genuinely domain-bound module: the seven dimensions are juristisch-politisch konventioniert and won't generalise to other domains. Lives in the chain-modules-law org once the org is created on Forgejo; until then the remote URL is set accordingly. v0.1.0 input shape is a flat ratings object so the surrounding flow can pre-compute the scores from any source (LLM proposal + Juristen-review, NKR-Stellungnahme, Beirats-Konsens). Pure in-WASM, zero filesystem, zero network. Reserved for next versions: - 0.2: per-dimension Juristen-vote panel (inter-rater κ output) - 0.3: LLM-proposed ratings + RAG-Quellen integration via the surrounding flow (this module stays deterministic) Signed-off-by: flemming-it --- .gitignore | 3 + Cargo.lock | 443 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 36 ++++ module.yaml | 44 +++++ rust-toolchain.toml | 4 + src/lib.rs | 118 ++++++++++++ 6 files changed, 648 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 module.yaml create mode 100644 rust-toolchain.toml create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e66a2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target/ +**/*.rs.bk +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7b97af8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,443 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chain-module-sdk" +version = "0.1.2" +source = "git+https://git.flemming.ai/fai/chain-module-sdk-rust.git?branch=main#98e98e9371f7409560a1ef08bc0923d9a2506449" +dependencies = [ + "chain-module-sdk-macros", + "serde", + "serde_json", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "chain-module-sdk-macros" +version = "0.1.2" +source = "git+https://git.flemming.ai/fai/chain-module-sdk-rust.git?branch=main#98e98e9371f7409560a1ef08bc0923d9a2506449" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "law_benefit_score" +version = "0.1.0" +dependencies = [ + "chain-module-sdk", + "serde", + "serde_json", +] + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-encoder" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e913f9242315ca39eff82aee0e19ee7a372155717ff0eb082c741e435ce25ed1" +dependencies = [ + "leb128", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185dfcd27fa5db2e6a23906b54c28199935f71d9a27a1a27b3a88d6fee2afae7" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25" +dependencies = [ + "ahash", + "bitflags", + "hashbrown 0.14.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2b3e15cd6068f233926e7d8c7c588b2ec4fb7cc7bf3824115e7c7e2a8485a3" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b632a5a0fa2409489bd49c9e6d99fcc61bb3d4ce9d1907d44662e75a28c71172" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7947d0131c7c9da3f01dfde0ab8bd4c4cf3c5bd49b6dba0ae640f1fa752572ea" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4329de4186ee30e2ef30a0533f9b3c123c019a237a7c82d692807bf1b3ee2697" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177fb7ee1484d113b4792cc480b1ba57664bbc951b42a4beebe573502135b1fc" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b505603761ed400c90ed30261f44a768317348e49f1864e82ecdc3b2744e5627" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae2a7999ed18efe59be8de2db9cb2b7f84d88b27818c79353dfc53131840fe1a" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7087708 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "law_benefit_score" +version = "0.1.0" +edition = "2024" +authors = ["Dr. Stefan Flemming "] +license = "Apache-2.0" +publish = false +description = "F∆I module — 7-dimensional benefit score per Studie §3.2" +repository = "https://git.flemming.ws/chain-modules-law/law-benefit-score" +rust-version = "1.85" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +chain-module-sdk = { git = "https://git.flemming.ai/fai/chain-module-sdk-rust.git", branch = "main" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[profile.release] +opt-level = "s" +lto = true +codegen-units = 1 +strip = "symbols" + +[lints.rust] +unsafe_op_in_unsafe_fn = "warn" + +[lints.clippy] +unwrap_used = "deny" +expect_used = "deny" +panic = "deny" +print_stdout = "deny" +print_stderr = "deny" + +[workspace] diff --git a/module.yaml b/module.yaml new file mode 100644 index 0000000..d0e0fc5 --- /dev/null +++ b/module.yaml @@ -0,0 +1,44 @@ +schema_version: 3 +provider: chain +name: law-benefit-score +version: 0.1.0 + +# 7-dimensional benefit score: Schutz/Markt/Rechtssicherheit/ +# EU-Binnenmarkt/Sicherheit/Umwelt/Einnahmen — each 0..5. +# +# This is the ONE genuinely domain-bound module — the seven +# dimensions are juristisch-politisch konventioniert. It lives +# in `chain-modules-law/` once the org exists. +# +# v0.1.0 ships a simple uniform-input scorer: the flow supplies +# expert ratings; this module aggregates + normalises. v0.2 +# integrates with `llm.chat` proposals + Juristen-review. +provides: + - capability: law.benefit_score + version: 0.1.0 + +inputs: + ratings: + type: json + description: + en: | + { schutz, markt, rechtssicherheit, eu_binnenmarkt, + sicherheit, umwelt, einnahmen } — each 0..5. + Source field per dimension is recommended for the audit + log; pass `_sources: {dimension: source_label}` to surface + in the output report. + de: | + Bewertungen pro Dimension (0..5) + optional _sources. + +outputs: + score: + type: json + description: + en: | + { dimensions, total: 0..5, dispersion, + sources: {dimension: source_label?} }. + de: | + { dimensions, total: 0..5, dispersion, + sources: {dimension: source_label?} }. + +permissions: [] diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..32add0c --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.86" +targets = ["wasm32-wasip2"] +components = ["rustfmt", "clippy"] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bfce361 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,118 @@ +//! `law.benefit_score` — aggregate seven juristisch-politisch +//! konventionierte benefit dimensions to a single 0..5 score +//! with dispersion (= standard deviation across dimensions). +//! +//! The dimensions follow Studie §3.2: +//! - Schutz (Gesundheit / Sicherheit) +//! - Markt (Marktordnung, Wettbewerb) +//! - Rechtssicherheit +//! - EU-Binnenmarkt +//! - Sicherheit (öffentliche) +//! - Umwelt +//! - Einnahmen (fiskalisch) + +#![allow(clippy::result_large_err)] + +use chain_module_sdk::prelude::*; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(Debug, Deserialize, Default)] +#[serde(default)] +struct Ratings { + schutz: f32, + markt: f32, + rechtssicherheit: f32, + eu_binnenmarkt: f32, + sicherheit: f32, + umwelt: f32, + einnahmen: f32, + #[serde(rename = "_sources")] + sources: BTreeMap, +} + +#[derive(Debug, Serialize)] +struct Score { + dimensions: BTreeMap<&'static str, f32>, + total: f32, + dispersion: f32, + sources: BTreeMap, +} + +#[fai_module] +pub fn invoke(_ctx: Context, inputs: Inputs) -> Result { + let r: Ratings = inputs + .require_json("ratings") + .map_err(|e| ModuleError::invalid_input(format!("ratings shape: {e}")))?; + + let values: [(&'static str, f32); 7] = [ + ("schutz", clamp05(r.schutz)), + ("markt", clamp05(r.markt)), + ("rechtssicherheit", clamp05(r.rechtssicherheit)), + ("eu_binnenmarkt", clamp05(r.eu_binnenmarkt)), + ("sicherheit", clamp05(r.sicherheit)), + ("umwelt", clamp05(r.umwelt)), + ("einnahmen", clamp05(r.einnahmen)), + ]; + + let mean = values.iter().map(|(_, v)| *v).sum::() / values.len() as f32; + let variance = values + .iter() + .map(|(_, v)| (v - mean).powi(2)) + .sum::() + / values.len() as f32; + let dispersion = variance.sqrt(); + + let mut dims = BTreeMap::new(); + for (k, v) in values.iter() { + dims.insert(*k, *v); + } + + let score = Score { + dimensions: dims, + total: mean, + dispersion, + sources: r.sources, + }; + Outputs::new().with_json("score", &score) +} + +fn clamp05(v: f32) -> f32 { + v.clamp(0.0, 5.0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn all_fives_means_five() { + let r = Ratings { + schutz: 5.0, + markt: 5.0, + rechtssicherheit: 5.0, + eu_binnenmarkt: 5.0, + sicherheit: 5.0, + umwelt: 5.0, + einnahmen: 5.0, + sources: Default::default(), + }; + let v: [(&'static str, f32); 7] = [ + ("schutz", clamp05(r.schutz)), + ("markt", clamp05(r.markt)), + ("rechtssicherheit", clamp05(r.rechtssicherheit)), + ("eu_binnenmarkt", clamp05(r.eu_binnenmarkt)), + ("sicherheit", clamp05(r.sicherheit)), + ("umwelt", clamp05(r.umwelt)), + ("einnahmen", clamp05(r.einnahmen)), + ]; + let mean = v.iter().map(|x| x.1).sum::() / 7.0; + assert!((mean - 5.0).abs() < 1e-4); + } + + #[test] + fn out_of_range_clamped() { + assert_eq!(clamp05(-1.0), 0.0); + assert_eq!(clamp05(99.0), 5.0); + } +}