Skip to main content

axon/handlers/
base.rs

1//! AXON Runtime — Handler Base Interface
2//! =======================================
3//! Direct port of `axon/runtime/handlers/base.py`.
4//!
5//! Free-Monad interpreter for the I/O Cognitivo Intention Tree. A Handler
6//! receives pure intentions (`IRManifest`, `IRObserve`) from an
7//! `IRIntentionTree` (Fase 1) and produces concrete outcomes wrapped in
8//! the Lambda Data envelope E = ⟨c, τ, ρ, δ⟩.
9//!
10//! Design anchors (docs/plan_io_cognitivo.md):
11//!   * D1 — Free Monads + Handlers (CPS).
12//!   * D4 — Partition ⇒ CT-3 infrastructure error, NEVER `doubt`.
13//!   * D5 — Curry-Howard λ-L-E; each outcome is a constructive proof witness.
14
15#![allow(dead_code)]
16
17use std::collections::HashMap;
18use std::error::Error;
19use std::fmt;
20
21use chrono::Utc;
22use serde::Serialize;
23use serde_json::Value;
24
25use crate::ir_nodes::{
26    IRFabric, IRIntentionOperation, IRIntentionTree, IRManifest, IRObserve, IRProgram,
27    IRResource,
28};
29
30// ═══════════════════════════════════════════════════════════════════
31//  ΛD ENVELOPE — Lambda Data epistemic vector
32// ═══════════════════════════════════════════════════════════════════
33
34/// Accepted δ (derivation) kinds.
35pub const VALID_DERIVATIONS: &[&str] = &["axiomatic", "observed", "inferred", "mutated"];
36
37/// E = ⟨c, τ, ρ, δ⟩ — epistemic envelope wrapping every handler output.
38#[derive(Debug, Clone, PartialEq, Serialize)]
39pub struct LambdaEnvelope {
40    /// Certainty in [0.0, 1.0]. 1.0 = `know`, 0.0 = `void` (⊥).
41    pub c: f64,
42    /// Temporal frame — ISO-8601 UTC timestamp of the observation.
43    pub tau: String,
44    /// Provenance — handler id + optional cryptographic signature (Fase 6.2).
45    pub rho: String,
46    /// Derivation kind: axiomatic | observed | inferred | mutated.
47    pub delta: String,
48}
49
50impl LambdaEnvelope {
51    /// Construct an envelope, validating c and delta. Panics on violation —
52    /// Python raises ValueError; in Rust these are invariant breaches that
53    /// indicate a handler-layer bug (CT-1), so panicking here is correct.
54    pub fn new(c: f64, tau: String, rho: String, delta: String) -> Self {
55        assert!(
56            (0.0..=1.0).contains(&c),
57            "LambdaEnvelope.c must be in [0.0, 1.0]; got {c}"
58        );
59        assert!(
60            VALID_DERIVATIONS.contains(&delta.as_str()),
61            "LambdaEnvelope.delta must be one of {VALID_DERIVATIONS:?}; got '{delta}'"
62        );
63        LambdaEnvelope { c, tau, rho, delta }
64    }
65
66    /// Return a copy with certainty reduced (used when a lease expires → D2).
67    pub fn decayed(&self, to_certainty: f64) -> Self {
68        LambdaEnvelope::new(to_certainty, self.tau.clone(), self.rho.clone(), self.delta.clone())
69    }
70}
71
72/// Current UTC timestamp as ISO-8601 for ΛD τ frames.
73pub fn now_iso() -> String {
74    Utc::now().to_rfc3339()
75}
76
77/// Construct a ΛD envelope with either the supplied τ or the current time.
78pub fn make_envelope(c: f64, rho: &str, delta: &str, tau: Option<String>) -> LambdaEnvelope {
79    LambdaEnvelope::new(
80        c,
81        tau.unwrap_or_else(now_iso),
82        rho.to_string(),
83        delta.to_string(),
84    )
85}
86
87// ═══════════════════════════════════════════════════════════════════
88//  BLAME CALCULUS — Findler-Felleisen CT-1/CT-2/CT-3 error taxonomy
89// ═══════════════════════════════════════════════════════════════════
90
91/// CT-1: the handler/runtime itself is broken (bug on Axon side).
92pub const BLAME_CALLEE: &str = "CT-1";
93
94/// CT-2: the Axon program made an invalid request (anchor breach, expired
95/// lease, invalid manifest).
96pub const BLAME_CALLER: &str = "CT-2";
97
98/// CT-3: the physical world cannot answer (partition, quota, missing creds).
99pub const BLAME_INFRASTRUCTURE: &str = "CT-3";
100
101/// Every handler-emitted error — always carries a blame tag.
102#[derive(Debug)]
103pub struct HandlerError {
104    pub message: String,
105    pub blame: &'static str,
106    pub kind: HandlerErrorKind,
107    pub cause: Option<Box<dyn Error + Send + Sync + 'static>>,
108}
109
110/// The kind discriminant for a `HandlerError`. Lets callers match on a
111/// specific failure mode without losing the message/cause chain.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum HandlerErrorKind {
114    /// CT-1 — the handler implementation is broken. Always a bug.
115    Callee,
116    /// CT-2 — the Axon program made an invalid request.
117    Caller,
118    /// CT-3 — the physical world cannot answer.
119    Infrastructure,
120    /// Subtype of CT-3 — D4 partition = ⊥ void, NEVER `doubt`.
121    NetworkPartition,
122    /// Subtype of CT-2 — D2 τ expired = Anchor Breach.
123    LeaseExpired,
124    /// Subtype of CT-3 — backing SDK/binary missing.
125    HandlerUnavailable,
126}
127
128impl HandlerError {
129    pub fn callee(msg: impl Into<String>) -> Self {
130        Self { message: msg.into(), blame: BLAME_CALLEE, kind: HandlerErrorKind::Callee, cause: None }
131    }
132
133    pub fn caller(msg: impl Into<String>) -> Self {
134        Self { message: msg.into(), blame: BLAME_CALLER, kind: HandlerErrorKind::Caller, cause: None }
135    }
136
137    pub fn infrastructure(msg: impl Into<String>) -> Self {
138        Self {
139            message: msg.into(),
140            blame: BLAME_INFRASTRUCTURE,
141            kind: HandlerErrorKind::Infrastructure,
142            cause: None,
143        }
144    }
145
146    pub fn network_partition(msg: impl Into<String>) -> Self {
147        Self {
148            message: msg.into(),
149            blame: BLAME_INFRASTRUCTURE,
150            kind: HandlerErrorKind::NetworkPartition,
151            cause: None,
152        }
153    }
154
155    pub fn lease_expired(msg: impl Into<String>) -> Self {
156        Self { message: msg.into(), blame: BLAME_CALLER, kind: HandlerErrorKind::LeaseExpired, cause: None }
157    }
158
159    pub fn handler_unavailable(msg: impl Into<String>) -> Self {
160        Self {
161            message: msg.into(),
162            blame: BLAME_INFRASTRUCTURE,
163            kind: HandlerErrorKind::HandlerUnavailable,
164            cause: None,
165        }
166    }
167
168    pub fn with_cause(mut self, cause: impl Error + Send + Sync + 'static) -> Self {
169        self.cause = Some(Box::new(cause));
170        self
171    }
172}
173
174impl fmt::Display for HandlerError {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        write!(f, "[{}] {}", self.blame, self.message)
177    }
178}
179
180impl Error for HandlerError {
181    fn source(&self) -> Option<&(dyn Error + 'static)> {
182        self.cause.as_deref().map(|b| b as &(dyn Error + 'static))
183    }
184}
185
186// ═══════════════════════════════════════════════════════════════════
187//  HANDLER OUTCOME — the CPS return type
188// ═══════════════════════════════════════════════════════════════════
189
190/// Accepted outcome statuses.
191pub const VALID_OUTCOME_STATUSES: &[&str] = &["ok", "partial", "failed"];
192
193/// The result of β-reducing one Intention Tree node through a Handler.
194///
195/// Immutable so it can be shared across continuations safely.
196#[derive(Debug, Clone, Serialize)]
197pub struct HandlerOutcome {
198    pub operation: String,
199    pub target: String,
200    pub status: String,
201    pub envelope: LambdaEnvelope,
202    pub data: serde_json::Map<String, Value>,
203    pub handler: String,
204}
205
206impl HandlerOutcome {
207    pub fn new(
208        operation: impl Into<String>,
209        target: impl Into<String>,
210        status: impl Into<String>,
211        envelope: LambdaEnvelope,
212        handler: impl Into<String>,
213    ) -> Self {
214        let status = status.into();
215        assert!(
216            VALID_OUTCOME_STATUSES.contains(&status.as_str()),
217            "HandlerOutcome.status must be one of {VALID_OUTCOME_STATUSES:?}; got '{status}'"
218        );
219        HandlerOutcome {
220            operation: operation.into(),
221            target: target.into(),
222            status,
223            envelope,
224            data: serde_json::Map::new(),
225            handler: handler.into(),
226        }
227    }
228
229    pub fn with_data(mut self, data: serde_json::Map<String, Value>) -> Self {
230        self.data = data;
231        self
232    }
233}
234
235// ═══════════════════════════════════════════════════════════════════
236//  HANDLER INTERFACE — the abstract Free-Monad interpreter
237// ═══════════════════════════════════════════════════════════════════
238
239/// A CPS continuation: receives an outcome, returns (possibly transformed) outcome.
240pub type Continuation<'a> = Box<dyn FnMut(HandlerOutcome) -> HandlerOutcome + 'a>;
241
242/// Default continuation that passes outcomes through unchanged.
243pub fn identity_continuation<'a>() -> Continuation<'a> {
244    Box::new(|o| o)
245}
246
247/// Abstract interpreter of the Intention Tree (Free Monad F_Σ(X)).
248///
249/// Concrete implementors provide `provision` + `observe`; the default
250/// `interpret` walks an `IRIntentionTree` and drives CPS evaluation
251/// deterministically in declaration order.
252pub trait Handler {
253    /// Unique handler identifier (used by HandlerRegistry and provenance ρ).
254    fn name(&self) -> &str;
255
256    /// Return `true` iff this handler can interpret the given IR operation.
257    fn supports(&self, op: &IRIntentionOperation) -> bool {
258        matches!(op, IRIntentionOperation::Manifest(_) | IRIntentionOperation::Observe(_))
259    }
260
261    /// Materialize the resources listed in the manifest.
262    fn provision(
263        &mut self,
264        manifest: &IRManifest,
265        resources: &HashMap<String, IRResource>,
266        fabrics: &HashMap<String, IRFabric>,
267        continuation: &mut Continuation<'_>,
268    ) -> Result<HandlerOutcome, HandlerError>;
269
270    /// Take a quorum-gated snapshot of the manifest's real state.
271    fn observe(
272        &mut self,
273        obs: &IRObserve,
274        manifest: &IRManifest,
275        continuation: &mut Continuation<'_>,
276    ) -> Result<HandlerOutcome, HandlerError>;
277
278    /// Release handler-level resources. MUST be idempotent.
279    fn close(&mut self) {}
280
281    /// β-reduce F_Σ(X) → X by walking the tree in declaration order.
282    fn interpret(
283        &mut self,
284        tree: &IRIntentionTree,
285        resources: &HashMap<String, IRResource>,
286        fabrics: &HashMap<String, IRFabric>,
287        manifests: &HashMap<String, IRManifest>,
288    ) -> Result<Vec<HandlerOutcome>, HandlerError> {
289        let mut outcomes: Vec<HandlerOutcome> = Vec::with_capacity(tree.operations.len());
290        let mut pass_through: Continuation<'_> = identity_continuation();
291        for op in &tree.operations {
292            let outcome = match op {
293                IRIntentionOperation::Manifest(m) => {
294                    self.provision(m, resources, fabrics, &mut pass_through)?
295                }
296                IRIntentionOperation::Observe(o) => {
297                    let target = manifests.get(&o.target).ok_or_else(|| {
298                        HandlerError::caller(format!(
299                            "observe '{}' targets unknown manifest '{}' — \
300                             did you forget a declaration?",
301                            o.name, o.target
302                        ))
303                    })?;
304                    self.observe(o, target, &mut pass_through)?
305                }
306            };
307            outcomes.push(outcome);
308        }
309        Ok(outcomes)
310    }
311
312    /// Convenience: extract tree + tables from an `IRProgram` and interpret.
313    fn interpret_program(&mut self, program: &IRProgram) -> Result<Vec<HandlerOutcome>, HandlerError> {
314        let Some(tree) = program.intention_tree.as_ref() else {
315            return Ok(Vec::new());
316        };
317        let resources: HashMap<String, IRResource> = program
318            .resources
319            .iter()
320            .map(|r| (r.name.clone(), r.clone()))
321            .collect();
322        let fabrics: HashMap<String, IRFabric> = program
323            .fabrics
324            .iter()
325            .map(|f| (f.name.clone(), f.clone()))
326            .collect();
327        let manifests: HashMap<String, IRManifest> = program
328            .manifests
329            .iter()
330            .map(|m| (m.name.clone(), m.clone()))
331            .collect();
332        self.interpret(tree, &resources, &fabrics, &manifests)
333    }
334}
335
336// ═══════════════════════════════════════════════════════════════════
337//  HANDLER REGISTRY — plugin registration & dispatch
338// ═══════════════════════════════════════════════════════════════════
339
340/// Keyed registry of available handlers. Used by the CLI/runtime to look
341/// up a handler by name — the single dispatch point so that one `.axon`
342/// program can run under multiple handlers without source changes.
343pub struct HandlerRegistry {
344    handlers: HashMap<String, Box<dyn Handler + Send>>,
345}
346
347impl HandlerRegistry {
348    pub fn new() -> Self {
349        HandlerRegistry { handlers: HashMap::new() }
350    }
351
352    pub fn register(
353        &mut self,
354        handler: Box<dyn Handler + Send>,
355        replace: bool,
356    ) -> Result<(), HandlerError> {
357        let name = handler.name().to_string();
358        if self.handlers.contains_key(&name) && !replace {
359            return Err(HandlerError::callee(format!(
360                "handler '{name}' already registered; pass replace=true to override"
361            )));
362        }
363        self.handlers.insert(name, handler);
364        Ok(())
365    }
366
367    pub fn unregister(&mut self, name: &str) {
368        if let Some(mut handler) = self.handlers.remove(name) {
369            handler.close();
370        }
371    }
372
373    pub fn get(&mut self, name: &str) -> Result<&mut (dyn Handler + Send), HandlerError> {
374        let available = self.names().join(", ");
375        match self.handlers.get_mut(name) {
376            Some(h) => Ok(h.as_mut()),
377            None => Err(HandlerError::caller(format!(
378                "no handler registered with name '{name}'. Available: {}",
379                if available.is_empty() { "(none)" } else { &available }
380            ))),
381        }
382    }
383
384    pub fn names(&self) -> Vec<String> {
385        let mut names: Vec<String> = self.handlers.keys().cloned().collect();
386        names.sort();
387        names
388    }
389
390    pub fn contains(&self, name: &str) -> bool {
391        self.handlers.contains_key(name)
392    }
393
394    pub fn close_all(&mut self) {
395        for (_, mut handler) in self.handlers.drain() {
396            handler.close();
397        }
398    }
399}
400
401impl Default for HandlerRegistry {
402    fn default() -> Self {
403        Self::new()
404    }
405}
406
407impl Drop for HandlerRegistry {
408    fn drop(&mut self) {
409        self.close_all();
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    //! Unit tests mirror `tests/test_handlers_base.py` scope.
416    use super::*;
417
418    #[test]
419    fn envelope_validates_certainty_range() {
420        let e = LambdaEnvelope::new(0.5, "t".into(), "r".into(), "observed".into());
421        assert_eq!(e.c, 0.5);
422    }
423
424    #[test]
425    #[should_panic(expected = "must be in [0.0, 1.0]")]
426    fn envelope_rejects_c_out_of_range() {
427        LambdaEnvelope::new(1.1, "t".into(), "r".into(), "observed".into());
428    }
429
430    #[test]
431    #[should_panic(expected = "delta must be one of")]
432    fn envelope_rejects_invalid_delta() {
433        LambdaEnvelope::new(1.0, "t".into(), "r".into(), "imagined".into());
434    }
435
436    #[test]
437    fn envelope_decayed_preserves_tau_rho_delta() {
438        let e = LambdaEnvelope::new(1.0, "T".into(), "R".into(), "observed".into());
439        let d = e.decayed(0.0);
440        assert_eq!(d.c, 0.0);
441        assert_eq!(d.tau, "T");
442        assert_eq!(d.rho, "R");
443        assert_eq!(d.delta, "observed");
444    }
445
446    #[test]
447    fn make_envelope_uses_supplied_or_current_tau() {
448        let fixed = make_envelope(1.0, "h", "observed", Some("FIXED".into()));
449        assert_eq!(fixed.tau, "FIXED");
450        let fresh = make_envelope(1.0, "h", "observed", None);
451        assert!(!fresh.tau.is_empty());
452    }
453
454    #[test]
455    fn handler_error_display_includes_blame_tag() {
456        let err = HandlerError::caller("oops");
457        assert_eq!(format!("{err}"), "[CT-2] oops");
458    }
459
460    #[test]
461    fn network_partition_is_ct3() {
462        let e = HandlerError::network_partition("partition");
463        assert_eq!(e.blame, BLAME_INFRASTRUCTURE);
464        assert_eq!(e.kind, HandlerErrorKind::NetworkPartition);
465    }
466
467    #[test]
468    fn lease_expired_is_ct2() {
469        let e = HandlerError::lease_expired("expired");
470        assert_eq!(e.blame, BLAME_CALLER);
471        assert_eq!(e.kind, HandlerErrorKind::LeaseExpired);
472    }
473
474    #[test]
475    fn outcome_rejects_invalid_status() {
476        let env = LambdaEnvelope::new(1.0, "t".into(), "h".into(), "observed".into());
477        let result = std::panic::catch_unwind(|| {
478            HandlerOutcome::new("provision", "M", "weird", env, "h")
479        });
480        assert!(result.is_err());
481    }
482
483    struct DummyHandler {
484        name: String,
485        provisions: u32,
486        observes: u32,
487    }
488
489    impl Handler for DummyHandler {
490        fn name(&self) -> &str { &self.name }
491
492        fn provision(
493            &mut self,
494            manifest: &IRManifest,
495            _resources: &HashMap<String, IRResource>,
496            _fabrics: &HashMap<String, IRFabric>,
497            _cont: &mut Continuation<'_>,
498        ) -> Result<HandlerOutcome, HandlerError> {
499            self.provisions += 1;
500            Ok(HandlerOutcome::new(
501                "provision",
502                manifest.name.clone(),
503                "ok",
504                make_envelope(1.0, &self.name, "observed", Some("T".into())),
505                &self.name,
506            ))
507        }
508
509        fn observe(
510            &mut self,
511            obs: &IRObserve,
512            _manifest: &IRManifest,
513            _cont: &mut Continuation<'_>,
514        ) -> Result<HandlerOutcome, HandlerError> {
515            self.observes += 1;
516            Ok(HandlerOutcome::new(
517                "observe",
518                obs.name.clone(),
519                "ok",
520                make_envelope(0.94, &self.name, "observed", Some("T".into())),
521                &self.name,
522            ))
523        }
524    }
525
526    fn fixture_program() -> IRProgram {
527        use crate::ir_generator::IRGenerator;
528        use crate::lexer::Lexer;
529        use crate::parser::Parser;
530
531        let source = r#"
532            resource Db { kind: postgres lifetime: linear }
533            fabric Vpc { provider: aws region: "us-east-1" zones: 1 }
534            manifest Prod { resources: [Db] fabric: Vpc }
535            observe Health from Prod { sources: [prom] quorum: 1 }
536        "#;
537        let tokens = Lexer::new(source, "h").tokenize().expect("lex ok");
538        let program = Parser::new(tokens).parse().expect("parse ok");
539        IRGenerator::new().generate(&program)
540    }
541
542    #[test]
543    fn dummy_handler_interprets_intention_tree_in_order() {
544        let program = fixture_program();
545        assert!(program.intention_tree.is_some());
546        let mut handler = DummyHandler { name: "dummy".into(), provisions: 0, observes: 0 };
547        let outcomes = handler.interpret_program(&program).expect("interpret ok");
548        assert_eq!(outcomes.len(), 2);
549        assert_eq!(outcomes[0].operation, "provision");
550        assert_eq!(outcomes[1].operation, "observe");
551        assert_eq!(handler.provisions, 1);
552        assert_eq!(handler.observes, 1);
553    }
554
555    #[test]
556    fn registry_register_then_get() {
557        let mut reg = HandlerRegistry::new();
558        reg.register(
559            Box::new(DummyHandler { name: "dummy".into(), provisions: 0, observes: 0 }),
560            false,
561        )
562        .expect("register ok");
563        assert!(reg.contains("dummy"));
564        assert_eq!(reg.names(), vec!["dummy".to_string()]);
565        let h = reg.get("dummy").expect("get ok");
566        assert_eq!(h.name(), "dummy");
567    }
568
569    #[test]
570    fn registry_refuses_duplicate_without_replace() {
571        let mut reg = HandlerRegistry::new();
572        reg.register(
573            Box::new(DummyHandler { name: "dup".into(), provisions: 0, observes: 0 }),
574            false,
575        )
576        .unwrap();
577        let err = reg
578            .register(
579                Box::new(DummyHandler { name: "dup".into(), provisions: 0, observes: 0 }),
580                false,
581            )
582            .unwrap_err();
583        assert_eq!(err.kind, HandlerErrorKind::Callee);
584    }
585
586    #[test]
587    fn registry_allows_replace_when_flagged() {
588        let mut reg = HandlerRegistry::new();
589        reg.register(
590            Box::new(DummyHandler { name: "r".into(), provisions: 0, observes: 0 }),
591            false,
592        )
593        .unwrap();
594        reg.register(
595            Box::new(DummyHandler { name: "r".into(), provisions: 0, observes: 0 }),
596            true,
597        )
598        .expect("replace ok");
599    }
600
601    #[test]
602    fn registry_get_unknown_is_caller_blame() {
603        let mut reg = HandlerRegistry::new();
604        match reg.get("ghost") {
605            Err(e) => assert_eq!(e.kind, HandlerErrorKind::Caller),
606            Ok(_) => panic!("registry.get on unknown name must error"),
607        }
608    }
609}