Skip to main content

axon/
lambda_data.rs

1//! Lambda Data (ΛD) — Epistemic State Vector codec.
2//!
3//! Formal basis (paper_lambda_data.md):
4//!
5//!   ΛD: V → (V × O × C × T)
6//!   ψ = ⟨T, V, E⟩  where  E = ⟨c, τ, ρ, δ⟩
7//!
8//! This module implements the **lossless binary codec** for ΛD state vectors.
9//! Unlike JSON projection (π_JSON(ψ) = V, which discards T and E), the ΛD
10//! binary format preserves the full epistemic tensor across serialization
11//! boundaries.
12//!
13//! Invariants enforced at encode boundary:
14//!   1. Ontological Rigidity:  T ∈ O ∧ T ≠ ⊥
15//!   2. Singular Interpretation: V ∈ dom(T)  (deferred to runtime)
16//!   3. Semantic Conservation: type preservation across transformations
17//!   4. Epistemic Bounding: c ∈ [0,1] ∧ δ ∈ Δ
18//!
19//! Theorem 5.1 (Epistemic Degradation):
20//!   For any composition f operating on ΛD inputs,
21//!     c_out ≤ min(c_in₁, c_in₂, …, c_inₙ)
22//!   Enforced at compose time.
23
24use std::io::{self, Read};
25
26// ── Magic bytes & version ───────────────────────────────────────────────────
27
28/// File signature: "ΛD" in UTF-8 (0xCE 0x9B 0x44) + version byte.
29const MAGIC: [u8; 3] = [0xCE, 0x9B, 0x44]; // "ΛD" as UTF-8
30const FORMAT_VERSION: u8 = 1;
31
32// ── Derivation enum ─────────────────────────────────────────────────────────
33
34/// δ ∈ Δ = {raw, derived, inferred, aggregated, transformed}
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
36pub enum Derivation {
37    Raw = 0,
38    Derived = 1,
39    Inferred = 2,
40    Aggregated = 3,
41    Transformed = 4,
42}
43
44impl Derivation {
45    pub fn from_str(s: &str) -> Option<Self> {
46        match s {
47            "raw" => Some(Derivation::Raw),
48            "derived" => Some(Derivation::Derived),
49            "inferred" => Some(Derivation::Inferred),
50            "aggregated" => Some(Derivation::Aggregated),
51            "transformed" => Some(Derivation::Transformed),
52            _ => None,
53        }
54    }
55
56    pub fn as_str(&self) -> &'static str {
57        match self {
58            Derivation::Raw => "raw",
59            Derivation::Derived => "derived",
60            Derivation::Inferred => "inferred",
61            Derivation::Aggregated => "aggregated",
62            Derivation::Transformed => "transformed",
63        }
64    }
65
66    fn from_byte(b: u8) -> Option<Self> {
67        match b {
68            0 => Some(Derivation::Raw),
69            1 => Some(Derivation::Derived),
70            2 => Some(Derivation::Inferred),
71            3 => Some(Derivation::Aggregated),
72            4 => Some(Derivation::Transformed),
73            _ => None,
74        }
75    }
76}
77
78// ── Epistemic State Vector ──────────────────────────────────────────────────
79
80/// ψ = ⟨T, V, E⟩ where E = ⟨c, τ, ρ, δ⟩
81///
82/// T — Ontological type tag (domain classification)
83/// V — The value payload (opaque bytes, interpretation depends on T)
84/// E — Epistemic tensor:
85///     c — certainty scalar, c ∈ [0, 1]
86///     τ — temporal validity frame [t_start, t_end]
87///     ρ — provenance EntityRef (causal origin)
88///     δ — derivation ∈ Δ
89#[derive(Debug, Clone)]
90pub struct LambdaData {
91    pub name: String,
92    pub ontology: String,             // T
93    pub value: Vec<u8>,               // V (opaque payload)
94    pub certainty: f64,               // c ∈ [0,1]
95    pub temporal_frame_start: String, // τ_start
96    pub temporal_frame_end: String,   // τ_end
97    pub provenance: String,           // ρ
98    pub derivation: Derivation,       // δ
99}
100
101// ── Codec errors ────────────────────────────────────────────────────────────
102
103#[derive(Debug)]
104pub enum LdError {
105    /// Invariant violation at encode boundary.
106    InvariantViolation(String),
107    /// Binary format error during decode.
108    DecodeError(String),
109    /// IO error.
110    Io(io::Error),
111}
112
113impl From<io::Error> for LdError {
114    fn from(e: io::Error) -> Self {
115        LdError::Io(e)
116    }
117}
118
119impl std::fmt::Display for LdError {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            LdError::InvariantViolation(msg) => write!(f, "ΛD invariant violation: {msg}"),
123            LdError::DecodeError(msg) => write!(f, "ΛD decode error: {msg}"),
124            LdError::Io(e) => write!(f, "ΛD I/O error: {e}"),
125        }
126    }
127}
128
129// ── Invariant validation ────────────────────────────────────────────────────
130
131impl LambdaData {
132    /// Validate all encode-boundary invariants.
133    pub fn validate(&self) -> Result<(), LdError> {
134        // Invariant 1 — Ontological Rigidity: T ∈ O ∧ T ≠ ⊥
135        if self.ontology.is_empty() {
136            return Err(LdError::InvariantViolation(format!(
137                "Ontological Rigidity: '{}' has empty ontology (T = ⊥)",
138                self.name
139            )));
140        }
141
142        // Invariant 4 — Epistemic Bounding: c ∈ [0, 1]
143        if self.certainty < 0.0 || self.certainty > 1.0 {
144            return Err(LdError::InvariantViolation(format!(
145                "Epistemic Bounding: certainty={} for '{}' (must be in [0, 1])",
146                self.certainty, self.name
147            )));
148        }
149
150        // Theorem 5.1 — Epistemic Degradation: only raw may carry c = 1.0
151        if self.certainty == 1.0 && self.derivation != Derivation::Raw {
152            return Err(LdError::InvariantViolation(format!(
153                "Epistemic Degradation: '{}' has c=1.0 with δ={}, only raw may carry absolute certainty",
154                self.name, self.derivation.as_str()
155            )));
156        }
157
158        Ok(())
159    }
160}
161
162// ── Binary format ───────────────────────────────────────────────────────────
163//
164// Layout (little-endian):
165//   [3 bytes]  magic: 0xCE 0x9B 0x44 ("ΛD" UTF-8)
166//   [1 byte]   version
167//   [2+N]      name: u16 len + UTF-8 bytes
168//   [2+N]      ontology: u16 len + UTF-8 bytes
169//   [8 bytes]  certainty: f64
170//   [2+N]      temporal_frame_start: u16 len + UTF-8 bytes
171//   [2+N]      temporal_frame_end: u16 len + UTF-8 bytes
172//   [2+N]      provenance: u16 len + UTF-8 bytes
173//   [1 byte]   derivation: u8 enum tag
174//   [4+N]      value: u32 len + raw bytes
175//
176
177/// Encode a ΛD state vector to binary. Validates invariants at boundary.
178pub fn encode(ld: &LambdaData) -> Result<Vec<u8>, LdError> {
179    ld.validate()?;
180
181    let mut buf: Vec<u8> = Vec::new();
182
183    // Header
184    buf.extend_from_slice(&MAGIC);
185    buf.push(FORMAT_VERSION);
186
187    // Strings: name, ontology, temporal frames, provenance
188    write_str(&mut buf, &ld.name)?;
189    write_str(&mut buf, &ld.ontology)?;
190
191    // Certainty (f64 LE)
192    buf.extend_from_slice(&ld.certainty.to_le_bytes());
193
194    // Temporal frame
195    write_str(&mut buf, &ld.temporal_frame_start)?;
196    write_str(&mut buf, &ld.temporal_frame_end)?;
197
198    // Provenance
199    write_str(&mut buf, &ld.provenance)?;
200
201    // Derivation (single byte)
202    buf.push(ld.derivation as u8);
203
204    // Value payload (u32 length prefix)
205    let vlen = ld.value.len() as u32;
206    buf.extend_from_slice(&vlen.to_le_bytes());
207    buf.extend_from_slice(&ld.value);
208
209    Ok(buf)
210}
211
212/// Decode a ΛD state vector from binary. Validates invariants after decode.
213pub fn decode(data: &[u8]) -> Result<LambdaData, LdError> {
214    let mut cursor = io::Cursor::new(data);
215
216    // Magic
217    let mut magic = [0u8; 3];
218    cursor
219        .read_exact(&mut magic)
220        .map_err(|_| LdError::DecodeError("truncated: missing magic bytes".into()))?;
221    if magic != MAGIC {
222        return Err(LdError::DecodeError(format!(
223            "invalid magic: expected [CE 9B 44], got [{:02X} {:02X} {:02X}]",
224            magic[0], magic[1], magic[2]
225        )));
226    }
227
228    // Version
229    let mut ver = [0u8; 1];
230    cursor
231        .read_exact(&mut ver)
232        .map_err(|_| LdError::DecodeError("truncated: missing version byte".into()))?;
233    if ver[0] != FORMAT_VERSION {
234        return Err(LdError::DecodeError(format!(
235            "unsupported version: {} (expected {})",
236            ver[0], FORMAT_VERSION
237        )));
238    }
239
240    // Fields
241    let name = read_str(&mut cursor)?;
242    let ontology = read_str(&mut cursor)?;
243
244    let mut c_bytes = [0u8; 8];
245    cursor
246        .read_exact(&mut c_bytes)
247        .map_err(|_| LdError::DecodeError("truncated: missing certainty".into()))?;
248    let certainty = f64::from_le_bytes(c_bytes);
249
250    let temporal_frame_start = read_str(&mut cursor)?;
251    let temporal_frame_end = read_str(&mut cursor)?;
252    let provenance = read_str(&mut cursor)?;
253
254    let mut d_byte = [0u8; 1];
255    cursor
256        .read_exact(&mut d_byte)
257        .map_err(|_| LdError::DecodeError("truncated: missing derivation".into()))?;
258    let derivation = Derivation::from_byte(d_byte[0])
259        .ok_or_else(|| LdError::DecodeError(format!("invalid derivation tag: {}", d_byte[0])))?;
260
261    let mut vlen_bytes = [0u8; 4];
262    cursor
263        .read_exact(&mut vlen_bytes)
264        .map_err(|_| LdError::DecodeError("truncated: missing value length".into()))?;
265    let vlen = u32::from_le_bytes(vlen_bytes) as usize;
266    let mut value = vec![0u8; vlen];
267    cursor
268        .read_exact(&mut value)
269        .map_err(|_| LdError::DecodeError("truncated: value payload incomplete".into()))?;
270
271    let ld = LambdaData {
272        name,
273        ontology,
274        value,
275        certainty,
276        temporal_frame_start,
277        temporal_frame_end,
278        provenance,
279        derivation,
280    };
281
282    // Validate after decode (invariants must hold on deserialized data)
283    ld.validate()?;
284
285    Ok(ld)
286}
287
288// ── Composition (Theorem 5.1) ───────────────────────────────────────────────
289
290/// Compose two ΛD state vectors under Theorem 5.1 (Epistemic Degradation).
291///
292/// The composed ψ inherits:
293///   c_out = min(c₁, c₂)           — certainty cannot increase
294///   δ_out = max(δ₁, δ₂)           — derivation can only increase (raw < derived < inferred < aggregated < transformed)
295///   τ_out = intersection(τ₁, τ₂)  — temporal frame narrows
296///   ρ_out = "ρ₁ ∘ ρ₂"             — provenance chain concatenation
297pub fn compose(
298    a: &LambdaData,
299    b: &LambdaData,
300    result_name: &str,
301    result_ontology: &str,
302) -> Result<LambdaData, LdError> {
303    // Theorem 5.1: c_out ≤ min(c_in₁, c_in₂)
304    let c_out = a.certainty.min(b.certainty);
305
306    // Derivation: max (most derived wins)
307    let d_out = if (a.derivation as u8) >= (b.derivation as u8) {
308        a.derivation
309    } else {
310        b.derivation
311    };
312
313    // Temporal frame: intersection (most restrictive)
314    let tf_start = if a.temporal_frame_start >= b.temporal_frame_start {
315        &a.temporal_frame_start
316    } else {
317        &b.temporal_frame_start
318    };
319    let tf_end = if a.temporal_frame_end.is_empty() {
320        &b.temporal_frame_end
321    } else if b.temporal_frame_end.is_empty() {
322        &a.temporal_frame_end
323    } else if a.temporal_frame_end <= b.temporal_frame_end {
324        &a.temporal_frame_end
325    } else {
326        &b.temporal_frame_end
327    };
328
329    // Provenance: chain
330    let prov = if a.provenance.is_empty() {
331        b.provenance.clone()
332    } else if b.provenance.is_empty() {
333        a.provenance.clone()
334    } else {
335        format!("{} \u{2218} {}", a.provenance, b.provenance)
336    };
337
338    let composed = LambdaData {
339        name: result_name.to_string(),
340        ontology: result_ontology.to_string(),
341        value: Vec::new(), // composed value is deferred to runtime
342        certainty: c_out,
343        temporal_frame_start: tf_start.clone(),
344        temporal_frame_end: tf_end.clone(),
345        provenance: prov,
346        derivation: d_out,
347    };
348
349    composed.validate()?;
350    Ok(composed)
351}
352
353/// v2.5.0 — tainted-overriding (founder refinement A). A
354/// PROVENANCE member's declared `default_confidence` — an `extension`
355/// member's ceiling (v2.5.0), or the built-in `epistemic:<level>` axis — is
356/// a CEILING on the announced certainty, never a floor. When a value
357/// with certainty `input_c` is annotated with such a member, the
358/// announced certainty degrades to `min(ceiling, input_c)`.
359///
360/// This is the SAME Theorem 5.1 (Epistemic Degradation) rule [`compose`]
361/// applies — `c_out = min(c₁, c₂)` — with the declared ceiling as one
362/// operand. A doubtful input (`input_c < ceiling`) is therefore NEVER
363/// laundered UP to the declared ceiling; certainty cannot increase.
364///
365/// Pure + total. Both operands are clamped to `[0,1]` defensively so a
366/// malformed declared ceiling cannot push the result out of range.
367///
368/// **Scope (honest):** this is the mathematical rule, ready to wire.
369/// Driving it from live step execution needs a path from a step's
370/// effect-row provenance annotation to the runtime ψ-envelope of the
371/// value it produces — today effect rows are static contract metadata,
372/// not runtime certainty carriers, so that plumbing is a separate
373/// feature. The rule here is the contract that plumbing will call.
374pub fn apply_provenance_ceiling(input_c: f64, ceiling: f64) -> f64 {
375    input_c.clamp(0.0, 1.0).min(ceiling.clamp(0.0, 1.0))
376}
377
378// ── JSON projection (lossy) ─────────────────────────────────────────────────
379
380/// π_JSON(ψ) — lossy projection that discards epistemic tensor.
381///
382/// Returns a JSON object with all fields for inspection, but marks
383/// the projection as lossy with ΔH > 0 (information entropy increase).
384pub fn to_json(ld: &LambdaData) -> serde_json::Value {
385    serde_json::json!({
386        "_ld_version": FORMAT_VERSION,
387        "_ld_lossy": true,
388        "name": ld.name,
389        "ontology": ld.ontology,
390        "certainty": ld.certainty,
391        "temporal_frame_start": ld.temporal_frame_start,
392        "temporal_frame_end": ld.temporal_frame_end,
393        "provenance": ld.provenance,
394        "derivation": ld.derivation.as_str(),
395        "value_bytes": ld.value.len(),
396    })
397}
398
399/// Create a LambdaData from IR fields (bridge from compiler to runtime).
400pub fn from_ir(
401    name: &str,
402    ontology: &str,
403    certainty: f64,
404    temporal_frame_start: &str,
405    temporal_frame_end: &str,
406    provenance: &str,
407    derivation: &str,
408) -> Result<LambdaData, LdError> {
409    let d = Derivation::from_str(derivation)
410        .ok_or_else(|| LdError::InvariantViolation(format!("unknown derivation '{derivation}'")))?;
411
412    let ld = LambdaData {
413        name: name.to_string(),
414        ontology: ontology.to_string(),
415        value: Vec::new(),
416        certainty,
417        temporal_frame_start: temporal_frame_start.to_string(),
418        temporal_frame_end: temporal_frame_end.to_string(),
419        provenance: provenance.to_string(),
420        derivation: d,
421    };
422
423    ld.validate()?;
424    Ok(ld)
425}
426
427// ── Wire helpers ────────────────────────────────────────────────────────────
428
429fn write_str(buf: &mut Vec<u8>, s: &str) -> Result<(), LdError> {
430    let bytes = s.as_bytes();
431    if bytes.len() > u16::MAX as usize {
432        return Err(LdError::InvariantViolation(format!(
433            "string too long for ΛD format: {} bytes (max {})",
434            bytes.len(),
435            u16::MAX
436        )));
437    }
438    buf.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
439    buf.extend_from_slice(bytes);
440    Ok(())
441}
442
443fn read_str(cursor: &mut io::Cursor<&[u8]>) -> Result<String, LdError> {
444    let mut len_bytes = [0u8; 2];
445    cursor
446        .read_exact(&mut len_bytes)
447        .map_err(|_| LdError::DecodeError("truncated: missing string length".into()))?;
448    let len = u16::from_le_bytes(len_bytes) as usize;
449    let mut buf = vec![0u8; len];
450    cursor
451        .read_exact(&mut buf)
452        .map_err(|_| LdError::DecodeError("truncated: string payload incomplete".into()))?;
453    String::from_utf8(buf).map_err(|_| LdError::DecodeError("invalid UTF-8 in string field".into()))
454}
455
456// ── CLI entry point ─────────────────────────────────────────────────────────
457
458/// Run `axon ld` subcommand. Returns exit code.
459pub fn run_ld(action: &str, file: &str) -> i32 {
460    match action {
461        "encode" => run_ld_encode(file),
462        "decode" | "inspect" => run_ld_inspect(file),
463        _ => {
464            eprintln!("axon ld: unknown action '{action}'. Use: encode, decode, inspect");
465            2
466        }
467    }
468}
469
470/// Encode an .axon file's ΛD declarations to .ld binary files.
471fn run_ld_encode(file: &str) -> i32 {
472    let source = match std::fs::read_to_string(file) {
473        Ok(s) => s,
474        Err(_) => {
475            eprintln!("X File not found: {file}");
476            return 2;
477        }
478    };
479
480    // Lex → Parse
481    let tokens = match crate::lexer::Lexer::new(&source, file).tokenize() {
482        Ok(t) => t,
483        Err(e) => {
484            eprintln!("X Lexer error: {}", e.message);
485            return 1;
486        }
487    };
488    let mut parser = crate::parser::Parser::new(tokens);
489    let program = match parser.parse() {
490        Ok(p) => p,
491        Err(e) => {
492            eprintln!("X Parse error: {}", e.message);
493            return 1;
494        }
495    };
496
497    // Extract ΛD declarations
498    let mut count = 0;
499    for decl in &program.declarations {
500        if let crate::ast::Declaration::LambdaData(ld_def) = decl {
501            let derivation = if ld_def.derivation.is_empty() {
502                "raw"
503            } else {
504                &ld_def.derivation
505            };
506            let ld = match from_ir(
507                &ld_def.name,
508                &ld_def.ontology,
509                ld_def.certainty,
510                &ld_def.temporal_frame_start,
511                &ld_def.temporal_frame_end,
512                &ld_def.provenance,
513                derivation,
514            ) {
515                Ok(ld) => ld,
516                Err(e) => {
517                    eprintln!("X {e}");
518                    return 1;
519                }
520            };
521
522            let bytes = match encode(&ld) {
523                Ok(b) => b,
524                Err(e) => {
525                    eprintln!("X {e}");
526                    return 1;
527                }
528            };
529
530            let out_path = format!("{}.ld", ld_def.name);
531            if let Err(e) = std::fs::write(&out_path, &bytes) {
532                eprintln!("X Failed to write {out_path}: {e}");
533                return 1;
534            }
535            println!(
536                "  \u{2713} {} \u{2192} {out_path} ({} bytes, c={}, \u{03B4}={})",
537                ld_def.name,
538                bytes.len(),
539                ld.certainty,
540                ld.derivation.as_str()
541            );
542            count += 1;
543        }
544    }
545
546    if count == 0 {
547        eprintln!("X No lambda data declarations found in {file}");
548        return 1;
549    }
550    println!("\n{count} \u{039B}D state vector(s) encoded.");
551    0
552}
553
554/// Decode and inspect an .ld binary file.
555fn run_ld_inspect(file: &str) -> i32 {
556    let data = match std::fs::read(file) {
557        Ok(d) => d,
558        Err(_) => {
559            eprintln!("X File not found: {file}");
560            return 2;
561        }
562    };
563
564    let ld = match decode(&data) {
565        Ok(ld) => ld,
566        Err(e) => {
567            eprintln!("X {e}");
568            return 1;
569        }
570    };
571
572    println!("\u{03C8} = \u{27E8}T, V, E\u{27E9}  where  E = \u{27E8}c, \u{03C4}, \u{03C1}, \u{03B4}\u{27E9}\n");
573    println!("  name:       {}", ld.name);
574    println!("  T (ontology): {}", ld.ontology);
575    println!("  V (payload):  {} bytes", ld.value.len());
576    println!("  c (certainty): {}", ld.certainty);
577    if !ld.temporal_frame_start.is_empty() {
578        let tf = if ld.temporal_frame_end.is_empty() {
579            ld.temporal_frame_start.clone()
580        } else {
581            format!("[{}, {}]", ld.temporal_frame_start, ld.temporal_frame_end)
582        };
583        println!("  \u{03C4} (temporal):  {tf}");
584    }
585    if !ld.provenance.is_empty() {
586        println!("  \u{03C1} (provenance): {}", ld.provenance);
587    }
588    println!("  \u{03B4} (derivation): {}", ld.derivation.as_str());
589    println!(
590        "\n  format: \u{039B}D v{FORMAT_VERSION} ({} bytes)",
591        data.len()
592    );
593    0
594}
595
596#[cfg(test)]
597mod tests {
598    use super::apply_provenance_ceiling;
599
600    /// v2.5.0 — tainted-overriding: the ceiling is a CEILING.
601    #[test]
602    fn provenance_ceiling_is_a_ceiling_not_a_floor() {
603        // input BELOW the ceiling → input wins (no laundering up).
604        assert_eq!(apply_provenance_ceiling(0.40, 0.95), 0.40);
605        // input ABOVE the ceiling → ceiling caps it.
606        assert_eq!(apply_provenance_ceiling(0.99, 0.80), 0.80);
607        // equal → either.
608        assert_eq!(apply_provenance_ceiling(0.80, 0.80), 0.80);
609    }
610
611    /// Theorem 5.1 symmetry: it is `min`, commutative.
612    #[test]
613    fn provenance_ceiling_is_min() {
614        assert_eq!(
615            apply_provenance_ceiling(0.30, 0.70),
616            apply_provenance_ceiling(0.70, 0.30)
617        );
618        assert_eq!(apply_provenance_ceiling(0.30, 0.70), 0.30);
619    }
620
621    /// Defensive clamping: out-of-range operands cannot escape [0,1].
622    #[test]
623    fn provenance_ceiling_clamps_out_of_range() {
624        assert_eq!(apply_provenance_ceiling(1.5, 0.9), 0.9);
625        assert_eq!(apply_provenance_ceiling(0.5, 2.0), 0.5);
626        assert_eq!(apply_provenance_ceiling(-0.2, 0.9), 0.0);
627    }
628}