bijux-dag-core 0.4.1

Deterministic DAG kernel for graph parsing, validation, canonicalization, planning, and identity.
Documentation
use crate::Graph;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet, VecDeque};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ConditionalExecution {
    pub condition_id: String,
    pub expression: String,
    pub deterministic: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BranchDecisionNode {
    pub node_id: String,
    pub output_contract: String,
    pub true_target_group: String,
    pub false_target_group: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct JoinSemantics {
    pub join_node_id: String,
    pub reconciliation_mode: String,
    pub deterministic_ordering: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PartitionSemantics {
    pub partition_key: String,
    pub partition_source: String,
    pub stable_partition_ids: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MapFanOutSemantics {
    pub map_node_id: String,
    pub partition: PartitionSemantics,
    pub expansion_rule_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ReduceFanInSemantics {
    pub reduce_node_id: String,
    pub aggregation_order: String,
    pub deterministic: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WindowingSemantics {
    pub window_unit: String,
    pub boundary_start_unix_ms: u128,
    pub boundary_end_unix_ms: u128,
    pub inclusive_end: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GraphTemplate {
    pub template_id: String,
    pub frozen_expansion_contract: String,
    pub parameters: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SubgraphEmbedding {
    pub parent_graph_id: String,
    pub embedded_graph_id: String,
    pub entry_nodes: Vec<String>,
    pub exit_nodes: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GraphCompositionContract {
    pub base_graph_id: String,
    pub inherited_graph_ids: Vec<String>,
    pub immutable_after_compile: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ParameterBindingSemantics {
    pub graph_bindings: BTreeMap<String, String>,
    pub node_bindings: BTreeMap<String, BTreeMap<String, String>>,
    pub runtime_bindings: BTreeMap<String, String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LateBindingRule {
    pub binding_name: String,
    pub allowed_pre_compile: bool,
    pub allowed_post_compile: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DynamicEdgeExpansionRule {
    pub rule_id: String,
    pub source_node_id: String,
    pub deterministic: bool,
    pub snapshot_captured: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SemanticNodeExistenceExplanation {
    pub node_id: String,
    pub reason: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SemanticEdgeExistenceExplanation {
    pub from_node_id: String,
    pub to_node_id: String,
    pub reason: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SemanticOrderingExplanation {
    pub node_id: String,
    pub ordered_after: Vec<String>,
    pub reason: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GraphExplainabilityModel {
    pub node_explanations: Vec<SemanticNodeExistenceExplanation>,
    pub edge_explanations: Vec<SemanticEdgeExistenceExplanation>,
    pub ordering_explanations: Vec<SemanticOrderingExplanation>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct StaticAnalysisReport {
    pub unreachable_nodes: Vec<String>,
    pub dead_branch_nodes: Vec<String>,
    pub noop_join_nodes: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SemanticDiffClass {
    Topology,
    Policy,
    MetadataOnly,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum CompatibilityClassification {
    Safe,
    ReplaySafe,
    CacheBreaking,
    ScheduleBreaking,
    PolicyBreaking,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SemanticDiffReport {
    pub class: SemanticDiffClass,
    pub compatibility: CompatibilityClassification,
    pub changed_nodes: Vec<String>,
    pub changed_edges: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NormalizedSemanticGraph {
    pub graph: Graph,
    pub partition_expansion_contracts: Vec<String>,
    pub dynamic_edge_rules: Vec<DynamicEdgeExpansionRule>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GraphMigrationPatch {
    pub from_spec: String,
    pub to_spec: String,
    pub steps: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GraphComplexityScore {
    pub node_count: usize,
    pub edge_count: usize,
    pub branch_factor: f64,
    pub depth_estimate: usize,
    pub score: f64,
}

pub fn enforce_late_binding_immutability(rules: &[LateBindingRule]) -> Result<(), String> {
    for rule in rules {
        if rule.allowed_post_compile {
            return Err(format!(
                "late binding '{}' violates snapshot immutability",
                rule.binding_name
            ));
        }
    }
    Ok(())
}

pub fn normalize_semantic_graph(
    graph: &Graph,
    partition_contracts: Vec<String>,
    dynamic_edge_rules: Vec<DynamicEdgeExpansionRule>,
) -> Result<NormalizedSemanticGraph, String> {
    for rule in &dynamic_edge_rules {
        if !rule.deterministic || !rule.snapshot_captured {
            return Err(format!(
                "dynamic edge rule '{}' must be deterministic and snapshot-captured",
                rule.rule_id
            ));
        }
    }
    Ok(NormalizedSemanticGraph {
        graph: graph.clone().canonicalize(),
        partition_expansion_contracts: partition_contracts,
        dynamic_edge_rules,
    })
}

pub fn semantic_diff(before: &Graph, after: &Graph) -> SemanticDiffReport {
    let before_nodes: BTreeSet<String> = before.nodes.iter().map(|n| n.id.clone()).collect();
    let after_nodes: BTreeSet<String> = after.nodes.iter().map(|n| n.id.clone()).collect();
    let changed_nodes =
        before_nodes.symmetric_difference(&after_nodes).cloned().collect::<Vec<_>>();
    let before_edges: BTreeSet<String> =
        before.edges.iter().map(|e| format!("{}->{}", e.from.node_id, e.to.node_id)).collect();
    let after_edges: BTreeSet<String> =
        after.edges.iter().map(|e| format!("{}->{}", e.from.node_id, e.to.node_id)).collect();
    let changed_edges =
        before_edges.symmetric_difference(&after_edges).cloned().collect::<Vec<_>>();
    if !changed_nodes.is_empty() || !changed_edges.is_empty() {
        return SemanticDiffReport {
            class: SemanticDiffClass::Topology,
            compatibility: CompatibilityClassification::CacheBreaking,
            changed_nodes,
            changed_edges,
        };
    }
    if before.nondeterminism_allowed != after.nondeterminism_allowed {
        return SemanticDiffReport {
            class: SemanticDiffClass::Policy,
            compatibility: CompatibilityClassification::PolicyBreaking,
            changed_nodes,
            changed_edges,
        };
    }
    SemanticDiffReport {
        class: SemanticDiffClass::MetadataOnly,
        compatibility: CompatibilityClassification::ReplaySafe,
        changed_nodes,
        changed_edges,
    }
}

pub fn classify_compatibility(diff: &SemanticDiffReport) -> CompatibilityClassification {
    diff.compatibility.clone()
}

pub fn migration_patch(from_spec: &str, to_spec: &str) -> GraphMigrationPatch {
    GraphMigrationPatch {
        from_spec: from_spec.to_string(),
        to_spec: to_spec.to_string(),
        steps: vec![
            "parse previous graph snapshot".to_string(),
            "apply schema translation rules".to_string(),
            "recompute canonical graph fingerprint".to_string(),
            "emit migration diagnostics".to_string(),
        ],
    }
}

pub fn explain_graph(graph: &Graph) -> GraphExplainabilityModel {
    let node_explanations = graph
        .nodes
        .iter()
        .map(|n| SemanticNodeExistenceExplanation {
            node_id: n.id.clone(),
            reason: "declared in graph snapshot".to_string(),
        })
        .collect();
    let edge_explanations = graph
        .edges
        .iter()
        .map(|e| SemanticEdgeExistenceExplanation {
            from_node_id: e.from.node_id.clone(),
            to_node_id: e.to.node_id.clone(),
            reason: "declared dependency edge".to_string(),
        })
        .collect();
    let ordering_explanations = graph
        .nodes
        .iter()
        .map(|n| {
            let predecessors = graph
                .edges
                .iter()
                .filter(|e| e.to.node_id == n.id)
                .map(|e| e.from.node_id.clone())
                .collect::<Vec<_>>();
            SemanticOrderingExplanation {
                node_id: n.id.clone(),
                ordered_after: predecessors,
                reason: "topological precedence".to_string(),
            }
        })
        .collect();
    GraphExplainabilityModel { node_explanations, edge_explanations, ordering_explanations }
}

pub fn static_analysis(graph: &Graph) -> StaticAnalysisReport {
    let mut indegree: BTreeMap<String, usize> =
        graph.nodes.iter().map(|n| (n.id.clone(), 0)).collect();
    let mut adj: BTreeMap<String, Vec<String>> =
        graph.nodes.iter().map(|n| (n.id.clone(), vec![])).collect();
    for edge in &graph.edges {
        *indegree.entry(edge.to.node_id.clone()).or_insert(0) += 1;
        adj.entry(edge.from.node_id.clone()).or_default().push(edge.to.node_id.clone());
    }
    let mut queue: VecDeque<String> = indegree
        .iter()
        .filter_map(|(id, &d)| if d == 0 { Some(id.clone()) } else { None })
        .collect();
    let mut reachable = BTreeSet::new();
    while let Some(node) = queue.pop_front() {
        if !reachable.insert(node.clone()) {
            continue;
        }
        for next in adj.get(&node).cloned().unwrap_or_default() {
            let deg = indegree.entry(next.clone()).or_insert(0);
            *deg = deg.saturating_sub(1);
            if *deg == 0 {
                queue.push_back(next);
            }
        }
    }
    let unreachable_nodes = graph
        .nodes
        .iter()
        .filter_map(|n| (!reachable.contains(&n.id)).then_some(n.id.clone()))
        .collect::<Vec<_>>();
    let dead_branch_nodes = graph
        .nodes
        .iter()
        .filter(|n| n.group.as_deref() == Some("dead-branch"))
        .map(|n| n.id.clone())
        .collect::<Vec<_>>();
    let noop_join_nodes = graph
        .nodes
        .iter()
        .filter(|n| n.tags.iter().any(|t| t == "noop-join"))
        .map(|n| n.id.clone())
        .collect::<Vec<_>>();
    StaticAnalysisReport { unreachable_nodes, dead_branch_nodes, noop_join_nodes }
}

pub fn complexity_score(graph: &Graph) -> GraphComplexityScore {
    let node_count = graph.nodes.len();
    let edge_count = graph.edges.len();
    let mut outgoing = BTreeMap::<String, usize>::new();
    for edge in &graph.edges {
        *outgoing.entry(edge.from.node_id.clone()).or_insert(0) += 1;
    }
    let branch_factor = if node_count == 0 {
        0.0
    } else {
        outgoing.values().copied().sum::<usize>() as f64 / node_count as f64
    };
    let depth_estimate = graph.topo_order().map(|o| o.len()).unwrap_or(node_count);
    let score = node_count as f64 + edge_count as f64 * 1.5 + branch_factor * 2.0;
    GraphComplexityScore { node_count, edge_count, branch_factor, depth_estimate, score }
}