1use serde::{Deserialize, Serialize};
22
23pub const VALID_DERIVATIONS: [&str; 5] = [
27 "raw", "derived", "inferred", "aggregated", "transformed",
28];
29
30#[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#[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#[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#[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
99pub 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
147pub 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 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}