coreason-runtime 0.1.0

Kinetic Plane execution engine for the CoReason Tripartite Cybernetic Manifold
Documentation
// Copyright (c) 2026 CoReason, Inc.
// All rights reserved.

//! Local Runtime Simulator — DAG verification and fractal URN stitching.
//!
//! Replaces `coreason_runtime/execution_plane/local_simulator.py`.
//!
//! Provides local dry-run simulation of execution DAGs and fractal URN
//! hierarchical binding for capability composition.
//!
//! Zero Waste: Topological sort and cycle detection delegated to
//! `petgraph::algo::toposort` (already in Cargo.toml). We only write the
//! HashMap → DiGraph conversion glue and domain-specific simulation logic.

use serde::Serialize;
use std::collections::HashMap;

/// A node in the simulated execution DAG.
#[derive(Debug, Clone)]
pub struct SimulationNode {
    pub node_id: String,
    pub node_type: String,
    pub inputs: serde_json::Value,
    pub outputs: serde_json::Value,
    pub dependencies: Vec<String>,
    pub status: NodeStatus,
}

/// Status of a simulation node.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum NodeStatus {
    Pending,
    Running,
    Completed,
    Blocked,
    Failed,
}

impl std::fmt::Display for NodeStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            NodeStatus::Pending => write!(f, "PENDING"),
            NodeStatus::Running => write!(f, "RUNNING"),
            NodeStatus::Completed => write!(f, "COMPLETED"),
            NodeStatus::Blocked => write!(f, "BLOCKED"),
            NodeStatus::Failed => write!(f, "FAILED"),
        }
    }
}

impl SimulationNode {
    pub fn new(node_id: &str, node_type: &str) -> Self {
        Self {
            node_id: node_id.to_string(),
            node_type: node_type.to_string(),
            inputs: serde_json::Value::Object(serde_json::Map::new()),
            outputs: serde_json::Value::Object(serde_json::Map::new()),
            dependencies: Vec::new(),
            status: NodeStatus::Pending,
        }
    }

    pub fn with_dependencies(mut self, deps: Vec<&str>) -> Self {
        self.dependencies = deps.into_iter().map(String::from).collect();
        self
    }
}

/// Result of a DAG simulation.
#[derive(Debug, Clone, Serialize)]
pub struct SimulationResult {
    pub success: bool,
    pub executed_nodes: Vec<String>,
    pub failed_nodes: Vec<String>,
    pub execution_order: Vec<String>,
    pub total_time_ms: f64,
}

/// DAG integrity validation report.
#[derive(Debug, Clone, Serialize)]
pub struct DagValidationReport {
    pub valid: bool,
    pub has_cycle: bool,
    pub dangling_references: Vec<String>,
    pub topological_order: Vec<String>,
    pub node_count: usize,
}

/// Verify a DAG has no cycles or dangling references.
///
/// Zero Waste: Cycle detection and topological sorting delegated to
/// `petgraph::algo::toposort` (already in Cargo.toml). We only write the
/// HashMap → DiGraph conversion glue.
pub fn verify_dag_integrity(dag: &HashMap<String, SimulationNode>) -> DagValidationReport {
    use petgraph::graph::{DiGraph, NodeIndex};

    // Check for dangling references
    let dangling: Vec<String> = dag
        .iter()
        .flat_map(|(node_id, node)| {
            node.dependencies
                .iter()
                .filter(|dep| !dag.contains_key(*dep))
                .map(move |dep| format!("{}{}", node_id, dep))
        })
        .collect();

    // Build petgraph DiGraph from HashMap DAG
    let mut graph = DiGraph::<&str, ()>::new();
    let mut node_map: HashMap<&str, NodeIndex> = HashMap::new();

    for node_id in dag.keys() {
        let idx = graph.add_node(node_id.as_str());
        node_map.insert(node_id.as_str(), idx);
    }

    for (node_id, node) in dag {
        for dep in &node.dependencies {
            if let (Some(&dep_idx), Some(&node_idx)) =
                (node_map.get(dep.as_str()), node_map.get(node_id.as_str()))
            {
                // Edge from dependency → dependent (dep must come before node)
                graph.add_edge(dep_idx, node_idx, ());
            }
        }
    }

    // Zero Waste: petgraph::algo::toposort handles cycle detection
    match petgraph::algo::toposort(&graph, None) {
        Ok(sorted_indices) => {
            let topological_order: Vec<String> = sorted_indices
                .iter()
                .map(|&idx| graph[idx].to_string())
                .collect();
            DagValidationReport {
                valid: dangling.is_empty(),
                has_cycle: false,
                dangling_references: dangling,
                topological_order,
                node_count: dag.len(),
            }
        }
        Err(_cycle) => DagValidationReport {
            valid: false,
            has_cycle: true,
            dangling_references: dangling,
            topological_order: Vec::new(),
            node_count: dag.len(),
        },
    }
}

/// Simulate DAG execution in topological order (dry-run).
pub fn simulate_execution(dag: &mut HashMap<String, SimulationNode>) -> SimulationResult {
    let start = std::time::Instant::now();

    let integrity = verify_dag_integrity(dag);
    if !integrity.valid {
        return SimulationResult {
            success: false,
            executed_nodes: Vec::new(),
            failed_nodes: dag.keys().cloned().collect(),
            execution_order: Vec::new(),
            total_time_ms: 0.0,
        };
    }

    let mut executed: Vec<String> = Vec::new();
    let mut failed: Vec<String> = Vec::new();

    // Execute in topological order
    let order = integrity.topological_order.clone();
    for node_id in &order {
        // Check if all dependencies are completed
        let deps_met = {
            let node = &dag[node_id];
            node.dependencies.iter().all(|dep| {
                dag.get(dep)
                    .map_or(false, |n| n.status == NodeStatus::Completed)
            })
        };

        let node = dag.get_mut(node_id).unwrap();
        node.status = NodeStatus::Running;

        if deps_met {
            node.status = NodeStatus::Completed;
            executed.push(node_id.clone());
        } else {
            node.status = NodeStatus::Blocked;
            failed.push(node_id.clone());
        }
    }

    let elapsed = start.elapsed().as_secs_f64() * 1000.0;

    SimulationResult {
        success: failed.is_empty(),
        executed_nodes: executed,
        failed_nodes: failed,
        execution_order: order,
        total_time_ms: (elapsed * 100.0).round() / 100.0,
    }
}

/// Create hierarchical URN bindings for fractal composition.
///
/// Maps child URNs under a parent namespace, creating a fractal
/// capability hierarchy.
pub fn stitch_fractal_urns(parent_urn: &str, child_urns: &[&str]) -> HashMap<String, String> {
    child_urns
        .iter()
        .map(|&child_urn| {
            let leaf = child_urn.rsplit(':').next().unwrap_or(child_urn);
            let fractal_urn = format!("{}:{}", parent_urn, leaf);
            (child_urn.to_string(), fractal_urn)
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_dag() -> HashMap<String, SimulationNode> {
        let mut dag = HashMap::new();
        dag.insert("a".into(), SimulationNode::new("a", "compute"));
        dag.insert(
            "b".into(),
            SimulationNode::new("b", "compute").with_dependencies(vec!["a"]),
        );
        dag.insert(
            "c".into(),
            SimulationNode::new("c", "compute").with_dependencies(vec!["a"]),
        );
        dag.insert(
            "d".into(),
            SimulationNode::new("d", "output").with_dependencies(vec!["b", "c"]),
        );
        dag
    }

    #[test]
    fn test_verify_dag_integrity_valid() {
        let dag = make_dag();
        let report = verify_dag_integrity(&dag);
        assert!(report.valid);
        assert!(!report.has_cycle);
        assert!(report.dangling_references.is_empty());
        assert_eq!(report.node_count, 4);
        // "a" must come before "b" and "c"; "d" must come last
        let order = &report.topological_order;
        let a_pos = order.iter().position(|x| x == "a").unwrap();
        let b_pos = order.iter().position(|x| x == "b").unwrap();
        let c_pos = order.iter().position(|x| x == "c").unwrap();
        let d_pos = order.iter().position(|x| x == "d").unwrap();
        assert!(a_pos < b_pos);
        assert!(a_pos < c_pos);
        assert!(b_pos < d_pos);
        assert!(c_pos < d_pos);
    }

    #[test]
    fn test_verify_dag_detects_cycle() {
        let mut dag = HashMap::new();
        dag.insert(
            "a".into(),
            SimulationNode::new("a", "compute").with_dependencies(vec!["b"]),
        );
        dag.insert(
            "b".into(),
            SimulationNode::new("b", "compute").with_dependencies(vec!["a"]),
        );

        let report = verify_dag_integrity(&dag);
        assert!(!report.valid);
        assert!(report.has_cycle);
        assert!(report.topological_order.is_empty());
    }

    #[test]
    fn test_verify_dag_detects_dangling() {
        let mut dag = HashMap::new();
        dag.insert(
            "a".into(),
            SimulationNode::new("a", "compute").with_dependencies(vec!["missing"]),
        );

        let report = verify_dag_integrity(&dag);
        assert!(!report.valid);
        assert!(!report.dangling_references.is_empty());
    }

    #[test]
    fn test_simulate_execution_valid() {
        let mut dag = make_dag();
        let result = simulate_execution(&mut dag);
        assert!(result.success);
        assert_eq!(result.executed_nodes.len(), 4);
        assert!(result.failed_nodes.is_empty());
    }

    #[test]
    fn test_simulate_execution_invalid_dag() {
        let mut dag = HashMap::new();
        dag.insert(
            "a".into(),
            SimulationNode::new("a", "compute").with_dependencies(vec!["b"]),
        );
        dag.insert(
            "b".into(),
            SimulationNode::new("b", "compute").with_dependencies(vec!["a"]),
        );

        let result = simulate_execution(&mut dag);
        assert!(!result.success);
        assert_eq!(result.failed_nodes.len(), 2);
    }

    #[test]
    fn test_stitch_fractal_urns() {
        let bindings = stitch_fractal_urns(
            "urn:coreason:agent:orchestrator",
            &["urn:coreason:tool:calculator", "urn:coreason:tool:search"],
        );

        assert_eq!(
            bindings["urn:coreason:tool:calculator"],
            "urn:coreason:agent:orchestrator:calculator"
        );
        assert_eq!(
            bindings["urn:coreason:tool:search"],
            "urn:coreason:agent:orchestrator:search"
        );
    }

    #[test]
    fn test_stitch_fractal_urns_no_colon() {
        let bindings = stitch_fractal_urns("parent", &["child"]);
        assert_eq!(bindings["child"], "parent:child");
    }

    #[test]
    fn test_empty_dag() {
        let dag: HashMap<String, SimulationNode> = HashMap::new();
        let report = verify_dag_integrity(&dag);
        assert!(report.valid);
        assert_eq!(report.node_count, 0);
    }

    #[test]
    fn test_single_node_dag() {
        let mut dag = HashMap::new();
        dag.insert("solo".into(), SimulationNode::new("solo", "compute"));

        let mut dag_mut = dag.clone();
        let result = simulate_execution(&mut dag_mut);
        assert!(result.success);
        assert_eq!(result.executed_nodes.len(), 1);
    }
}