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:
commit
c94b55e539
6 changed files with 648 additions and 0 deletions
118
src/lib.rs
Normal file
118
src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue