Skip to main content

axon/
lambda_runtime.rs

1//! AXON Runtime — Lambda Data (ΛD) Apply (v1.10.0 — Rust mirror).
2//!
3//! Runtime types and helpers for the `lambda apply X to Y` flow-body
4//! statement. Mirror of `axon/runtime/lambda_runtime.py` — same
5//! semantics, same Theorem 5.1 guard, same JSON shape so cross-stack
6//! parity tests can compare byte-for-byte.
7//!
8//! ψ = ⟨T, V, E⟩ where:
9//!   T : String              — Ontology
10//!   V : serde_json::Value   — Bound value (string-typed in Rust runner)
11//!   E : LambdaTensor        — Epistemic tensor ⟨c, τ_start, τ_end, ρ, δ⟩
12//!
13//! Vocabulary follows the ΛD formalism:
14//!   δ ∈ {raw, derived, inferred, aggregated, transformed}
15//!
16//! Theorem 5.1 (Epistemic Degradation) is enforced at runtime by
17//! `enforce_theorem_5_1`, mirroring the compile-time guard in
18//! `axon-frontend::type_checker::check_lambda_data`. Defends against
19//! IR-JSON tampering between compile and execute.
20
21use serde::{Deserialize, Serialize};
22
23/// Mirror of `axon-frontend::type_checker::VALID_DERIVATIONS` and the
24/// Python runtime's `VALID_DERIVATIONS`. Drift is detected by the
25/// cross-stack parity golden in `axon-rs/tests/parity/`.
26pub const VALID_DERIVATIONS: [&str; 5] = [
27    "raw", "derived", "inferred", "aggregated", "transformed",
28];
29
30/// E = ⟨c, τ_start, τ_end, ρ, δ⟩ — the epistemic tensor.
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32pub struct LambdaTensor {
33    pub c: f64,
34    pub tau_start: String,
35    pub tau_end: String,
36    pub rho: String,
37    pub delta: String,
38}
39
40/// ψ = ⟨T, V, E⟩ — the epistemic state vector produced by `lambda apply`.
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct LambdaPsi {
43    #[serde(rename = "T")]
44    pub t: String,
45    #[serde(rename = "V")]
46    pub v: serde_json::Value,
47    #[serde(rename = "E")]
48    pub e: LambdaTensor,
49    pub spec_name: String,
50}
51
52/// Spec snapshot carried through the CompiledStep payload — verbatim
53/// copy of the IR `lambda` declaration so the runtime never needs the
54/// IR. Same shape as the Python `BaseBackend._compile_lambda_apply_step`
55/// metadata (so cross-stack parity is structural not just byte-level).
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct LambdaApplyPayload {
58    pub lambda_data_name: String,
59    pub target: String,
60    pub output_type: String,
61    pub spec_snapshot: SpecSnapshot,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, Default)]
65pub struct SpecSnapshot {
66    #[serde(default)]
67    pub name: String,
68    #[serde(default)]
69    pub ontology: String,
70    #[serde(default)]
71    pub certainty: f64,
72    #[serde(default)]
73    pub temporal_frame_start: String,
74    #[serde(default)]
75    pub temporal_frame_end: String,
76    #[serde(default)]
77    pub provenance: String,
78    #[serde(default)]
79    pub derivation: String,
80}
81
82/// Theorem 5.1 violation — raised by `enforce_theorem_5_1` when a spec
83/// snapshot reaches the dispatcher with c=1.0 + non-raw derivation,
84/// out-of-range certainty, or unknown derivation.
85#[derive(Debug, Clone)]
86pub struct EpistemicDegradationError {
87    pub message: String,
88    pub spec_name: String,
89}
90
91impl std::fmt::Display for EpistemicDegradationError {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(f, "[L3] EpistemicDegradationError: {}", self.message)
94    }
95}
96
97impl std::error::Error for EpistemicDegradationError {}
98
99/// Runtime mirror of the Epistemic Degradation Theorem compile-time
100/// check. Defends against IR-JSON tampering: a tampered IR could carry
101/// a `lambda_data_apply` whose snapshot violates the theorem the
102/// front-end rejected. This guard catches that at apply time, before
103/// the bad envelope propagates downstream.
104pub fn enforce_theorem_5_1(snapshot: &SpecSnapshot) -> Result<(), EpistemicDegradationError> {
105    if !(0.0..=1.0).contains(&snapshot.certainty) {
106        return Err(EpistemicDegradationError {
107            message: format!(
108                "lambda '{}' has out-of-range certainty {} (must be in [0.0, 1.0])",
109                snapshot.name, snapshot.certainty,
110            ),
111            spec_name: snapshot.name.clone(),
112        });
113    }
114
115    if !snapshot.derivation.is_empty()
116        && !VALID_DERIVATIONS.contains(&snapshot.derivation.as_str())
117    {
118        return Err(EpistemicDegradationError {
119            message: format!(
120                "lambda '{}' has unknown derivation '{}' (valid: {})",
121                snapshot.name,
122                snapshot.derivation,
123                VALID_DERIVATIONS.join(", "),
124            ),
125            spec_name: snapshot.name.clone(),
126        });
127    }
128
129    if (snapshot.certainty - 1.0).abs() < f64::EPSILON
130        && !snapshot.derivation.is_empty()
131        && snapshot.derivation != "raw"
132    {
133        return Err(EpistemicDegradationError {
134            message: format!(
135                "Theorem 5.1 violation at apply time: lambda '{}' has \
136                 certainty=1.0 with derivation='{}'. Only 'raw' data may \
137                 carry absolute certainty (c=1.0).",
138                snapshot.name, snapshot.derivation,
139            ),
140            spec_name: snapshot.name.clone(),
141        });
142    }
143
144    Ok(())
145}
146
147/// Construct ψ from a spec snapshot and a resolved target value (here
148/// represented as a JSON value to keep the Rust runner uniform with the
149/// string-typed ExecContext).
150pub fn build_psi(
151    snapshot: &SpecSnapshot,
152    target_value: serde_json::Value,
153) -> Result<LambdaPsi, EpistemicDegradationError> {
154    enforce_theorem_5_1(snapshot)?;
155
156    let delta = if snapshot.derivation.is_empty() {
157        "raw".to_string()
158    } else {
159        snapshot.derivation.clone()
160    };
161
162    Ok(LambdaPsi {
163        t: snapshot.ontology.clone(),
164        v: target_value,
165        e: LambdaTensor {
166            c: snapshot.certainty,
167            tau_start: snapshot.temporal_frame_start.clone(),
168            tau_end: snapshot.temporal_frame_end.clone(),
169            rho: snapshot.provenance.clone(),
170            delta,
171        },
172        spec_name: snapshot.name.clone(),
173    })
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    fn snap(certainty: f64, derivation: &str) -> SpecSnapshot {
181        SpecSnapshot {
182            name: "S".to_string(),
183            ontology: "measurement.temp".to_string(),
184            certainty,
185            temporal_frame_start: "2026-01-01T00:00:00Z".to_string(),
186            temporal_frame_end: "2026-12-31T23:59:59Z".to_string(),
187            provenance: "Sensor-A".to_string(),
188            derivation: derivation.to_string(),
189        }
190    }
191
192    #[test]
193    fn t51_raw_one_legal() {
194        assert!(enforce_theorem_5_1(&snap(1.0, "raw")).is_ok());
195    }
196
197    #[test]
198    fn t51_inferred_below_one_legal() {
199        assert!(enforce_theorem_5_1(&snap(0.7, "inferred")).is_ok());
200    }
201
202    #[test]
203    fn t51_inferred_with_one_rejected() {
204        let err = enforce_theorem_5_1(&snap(1.0, "inferred")).unwrap_err();
205        assert!(err.message.contains("Theorem 5.1"));
206        assert_eq!(err.spec_name, "S");
207    }
208
209    #[test]
210    fn t51_aggregated_with_one_rejected() {
211        assert!(enforce_theorem_5_1(&snap(1.0, "aggregated")).is_err());
212    }
213
214    #[test]
215    fn t51_out_of_range_rejected() {
216        assert!(enforce_theorem_5_1(&snap(1.5, "raw")).is_err());
217        assert!(enforce_theorem_5_1(&snap(-0.1, "raw")).is_err());
218    }
219
220    #[test]
221    fn t51_unknown_derivation_rejected() {
222        let err = enforce_theorem_5_1(&snap(0.5, "unicornified")).unwrap_err();
223        assert!(err.message.contains("unicornified"));
224    }
225
226    #[test]
227    fn t51_empty_derivation_passes() {
228        // Compile-time guard treats empty derivation as legacy/observed
229        // and skips Theorem 5.1; runtime mirrors that.
230        assert!(enforce_theorem_5_1(&snap(1.0, "")).is_ok());
231    }
232
233    #[test]
234    fn build_psi_carries_full_tensor() {
235        let psi = build_psi(&snap(0.9, "raw"), serde_json::json!(23.5)).unwrap();
236        assert_eq!(psi.t, "measurement.temp");
237        assert_eq!(psi.v, serde_json::json!(23.5));
238        assert!((psi.e.c - 0.9).abs() < f64::EPSILON);
239        assert_eq!(psi.e.delta, "raw");
240        assert_eq!(psi.e.rho, "Sensor-A");
241        assert_eq!(psi.spec_name, "S");
242    }
243
244    #[test]
245    fn build_psi_serialises_with_formal_keys() {
246        let psi = build_psi(&snap(1.0, "raw"), serde_json::json!("payload")).unwrap();
247        let json = serde_json::to_value(&psi).unwrap();
248        assert!(json.get("T").is_some());
249        assert!(json.get("V").is_some());
250        assert!(json.get("E").is_some());
251        assert!(json.get("spec_name").is_some());
252    }
253
254    #[test]
255    fn build_psi_json_round_trip() {
256        let psi = build_psi(&snap(0.8, "aggregated"), serde_json::json!(42)).unwrap();
257        let s = serde_json::to_string(&psi).unwrap();
258        let back: LambdaPsi = serde_json::from_str(&s).unwrap();
259        assert_eq!(back, psi);
260    }
261
262    #[test]
263    fn build_psi_t51_violation_propagates() {
264        assert!(build_psi(&snap(1.0, "inferred"), serde_json::json!(1)).is_err());
265    }
266
267    #[test]
268    fn build_psi_empty_derivation_defaults_to_raw() {
269        let psi = build_psi(&snap(1.0, ""), serde_json::json!(1)).unwrap();
270        assert_eq!(psi.e.delta, "raw");
271    }
272}