feat: initial law-benefit-score v0.1.0 (law.benefit_score@0.1.0)

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 <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-18 11:25:47 +02:00
commit c94b55e539
6 changed files with 648 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
target/
**/*.rs.bk
.DS_Store

443
Cargo.lock generated Normal file
View file

@ -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"

36
Cargo.toml Normal file
View file

@ -0,0 +1,36 @@
[package]
name = "law_benefit_score"
version = "0.1.0"
edition = "2024"
authors = ["Dr. Stefan Flemming <chain@flemming.ai>"]
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]

44
module.yaml Normal file
View file

@ -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: []

4
rust-toolchain.toml Normal file
View file

@ -0,0 +1,4 @@
[toolchain]
channel = "1.86"
targets = ["wasm32-wasip2"]
components = ["rustfmt", "clippy"]

118
src/lib.rs Normal file
View file

@ -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<String, String>,
}
#[derive(Debug, Serialize)]
struct Score {
dimensions: BTreeMap<&'static str, f32>,
total: f32,
dispersion: f32,
sources: BTreeMap<String, String>,
}
#[fai_module]
pub fn invoke(_ctx: Context, inputs: Inputs) -> Result<Outputs, ModuleError> {
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::<f32>() / values.len() as f32;
let variance = values
.iter()
.map(|(_, v)| (v - mean).powi(2))
.sum::<f32>()
/ 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::<f32>() / 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);
}
}