use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EffectVector {
#[serde(default)]
pub risk: f64,
#[serde(default)]
pub fairness: f64,
#[serde(default)]
pub privacy: f64,
#[serde(default)]
pub novelty: f64,
#[serde(default)]
pub security: f64,
}
impl EffectVector {
pub fn magnitude(&self) -> f64 {
(self.risk * self.risk
+ self.fairness * self.fairness
+ self.privacy * self.privacy
+ self.novelty * self.novelty
+ self.security * self.security)
.sqrt()
}
pub fn to_kernel_json(&self) -> serde_json::Value {
serde_json::to_value(self).expect("EffectVector serializes to JSON")
}
}
pub fn effect_for_binding_thread(status_ok: bool) -> EffectVector {
if status_ok {
EffectVector::default()
} else {
EffectVector {
security: 1.0,
risk: 0.9,
..Default::default()
}
}
}
pub fn effect_for_tool(name: &str, _args: &serde_json::Value) -> EffectVector {
match name {
"read_file" => EffectVector {
privacy: 0.1,
..Default::default()
},
"list_directory" => EffectVector {
privacy: 0.05,
..Default::default()
},
"write_file" => EffectVector {
security: 0.4,
..Default::default()
},
"exec" => EffectVector {
risk: 0.6,
security: 0.7,
..Default::default()
},
"agent_spawn" => EffectVector {
risk: 0.5,
novelty: 0.6,
security: 0.5,
..Default::default()
},
"agent_message" => EffectVector {
security: 0.3,
..Default::default()
},
"task_status" | "task_result" => EffectVector {
privacy: 0.05,
..Default::default()
},
"obs_recall" => EffectVector {
privacy: 0.05,
..Default::default()
},
_ => EffectVector::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn read_tools_score_privacy() {
let read = effect_for_tool("read_file", &json!({}));
assert!((read.privacy - 0.1).abs() < f64::EPSILON);
assert_eq!(read.security, 0.0);
let list = effect_for_tool("list_directory", &json!({}));
assert!((list.privacy - 0.05).abs() < f64::EPSILON);
}
#[test]
fn write_tool_scores_security() {
let w = effect_for_tool("write_file", &json!({"path": "/tmp/foo"}));
assert!((w.security - 0.4).abs() < f64::EPSILON);
assert_eq!(w.risk, 0.0);
}
#[test]
fn exec_tool_scores_both_risk_and_security() {
let e = effect_for_tool("exec", &json!({"cmd": "ls"}));
assert!((e.risk - 0.6).abs() < f64::EPSILON);
assert!((e.security - 0.7).abs() < f64::EPSILON);
}
#[test]
fn agent_spawn_forces_a_governance_decision() {
let ev = effect_for_tool("agent_spawn", &json!({"goal": "answer 2+2"}));
assert!(
(ev.risk - 0.5).abs() < f64::EPSILON
&& (ev.novelty - 0.6).abs() < f64::EPSILON
&& (ev.security - 0.5).abs() < f64::EPSILON,
"agent_spawn effect vector drifted from the D6 spec"
);
assert!(
ev.magnitude() > 0.8,
"agent_spawn magnitude {} must exceed the 0.8 default threshold",
ev.magnitude()
);
}
#[test]
fn agent_message_is_moderate() {
let ev = effect_for_tool("agent_message", &json!({"task_id": "t", "message": "hi"}));
assert!((ev.security - 0.3).abs() < f64::EPSILON);
assert!(ev.magnitude() < 0.8);
}
#[test]
fn task_introspection_tools_are_near_zero() {
for tool in ["task_status", "task_result", "obs_recall"] {
let ev = effect_for_tool(tool, &json!({"task_id": "t"}));
assert!(
ev.magnitude() < 0.1,
"{tool} is a read and must score near-zero, got {}",
ev.magnitude()
);
}
}
#[test]
fn unknown_tool_is_neutral() {
let u = effect_for_tool("definitely_not_a_real_tool", &json!({}));
assert_eq!(u, EffectVector::default());
assert_eq!(u.magnitude(), 0.0);
}
#[test]
fn binding_thread_effect_scores_high_on_mismatch() {
let ok = effect_for_binding_thread(true);
assert_eq!(ok.magnitude(), 0.0);
let bad = effect_for_binding_thread(false);
assert!((bad.security - 1.0).abs() < f64::EPSILON);
assert!(bad.magnitude() > 0.8);
}
#[test]
fn magnitude_matches_l2_norm() {
let ev = EffectVector {
risk: 0.6,
security: 0.7,
..Default::default()
};
let expected = (0.6f64.powi(2) + 0.7f64.powi(2)).sqrt();
assert!((ev.magnitude() - expected).abs() < 1e-9);
}
#[test]
fn to_kernel_json_has_all_five_dimensions() {
let ev = EffectVector {
risk: 0.6,
fairness: 0.0,
privacy: 0.3,
novelty: 0.0,
security: 0.7,
};
let json = ev.to_kernel_json();
let obj = json.as_object().expect("kernel JSON is an object");
for key in ["risk", "fairness", "privacy", "novelty", "security"] {
assert!(
obj.contains_key(key),
"kernel-side governance::EffectVector requires `{key}` field"
);
}
assert_eq!(obj["risk"].as_f64(), Some(0.6));
assert_eq!(obj["security"].as_f64(), Some(0.7));
assert_eq!(obj["privacy"].as_f64(), Some(0.3));
assert_eq!(obj["fairness"].as_f64(), Some(0.0));
assert_eq!(obj["novelty"].as_f64(), Some(0.0));
}
#[test]
fn to_kernel_json_round_trips() {
let ev = EffectVector {
risk: 0.42,
fairness: 0.11,
privacy: 0.55,
novelty: 0.0,
security: 0.99,
};
let json = ev.to_kernel_json();
let back: EffectVector = serde_json::from_value(json).unwrap();
assert_eq!(back, ev);
}
#[test]
fn args_are_currently_ignored() {
let a = effect_for_tool("write_file", &json!({"path": "/etc/passwd"}));
let b = effect_for_tool("write_file", &json!({"path": "/tmp/safe.log"}));
assert_eq!(a, b);
}
}