kotoba_core/
topology.rs

1//! Topology validation and graph processing module
2//!
3//! This module provides functionality for validating and processing
4//! process network topologies defined in dag.jsonnet files.
5
6use std::collections::HashMap;
7use serde::{Deserialize, Serialize};
8use kotoba_errors::KotobaError;
9
10/// Represents a node in the topology graph
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct Node {
13    /// Node name/identifier
14    pub name: String,
15    /// File system path for the node
16    pub path: String,
17    /// Type of the node (e.g., "crate", "service", "workflow")
18    pub node_type: String,
19    /// Human-readable description
20    pub description: String,
21    /// List of dependencies (nodes this node depends on)
22    pub dependencies: Vec<String>,
23    /// List of capabilities this node provides
24    pub provides: Vec<String>,
25    /// Current status (e.g., "active", "inactive", "deprecated")
26    pub status: String,
27    /// Build order for topological sorting
28    pub build_order: u32,
29}
30
31/// Represents an edge in the topology graph
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub struct Edge {
34    /// Source node name
35    pub from: String,
36    /// Target node name
37    pub to: String,
38}
39
40/// Complete topology graph representation
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct TopologyGraph {
43    /// All nodes in the topology
44    pub nodes: HashMap<String, Node>,
45    /// All edges in the topology
46    pub edges: Vec<Edge>,
47    /// Topologically sorted order of nodes
48    pub topological_order: Vec<String>,
49    /// Reverse topological order (dependencies first)
50    pub reverse_topological_order: Vec<String>,
51}
52
53impl TopologyGraph {
54    /// Create a new empty topology graph
55    pub fn new() -> Self {
56        Self {
57            nodes: HashMap::new(),
58            edges: Vec::new(),
59            topological_order: Vec::new(),
60            reverse_topological_order: Vec::new(),
61        }
62    }
63
64    /// Add a node to the topology
65    pub fn add_node(&mut self, node: Node) {
66        self.nodes.insert(node.name.clone(), node);
67    }
68
69    /// Add an edge to the topology
70    pub fn add_edge(&mut self, edge: Edge) {
71        self.edges.push(edge);
72    }
73
74    /// Get the number of nodes
75    pub fn node_count(&self) -> usize {
76        self.nodes.len()
77    }
78
79    /// Get the number of edges
80    pub fn edge_count(&self) -> usize {
81        self.edges.len()
82    }
83}
84
85/// Individual validation check result
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ValidationCheck {
88    /// Name of the check
89    pub name: String,
90    /// Whether the check passed
91    pub is_valid: bool,
92    /// List of errors (empty if valid)
93    pub errors: Vec<String>,
94    /// List of warnings
95    pub warnings: Vec<String>,
96}
97
98/// Complete validation result
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ValidationResult {
101    /// Overall validation status
102    pub is_valid: bool,
103    /// Individual check results
104    pub checks: Vec<ValidationCheck>,
105    /// Summary statistics
106    pub statistics: ValidationStatistics,
107}
108
109impl ValidationResult {
110    /// Create a new validation result
111    pub fn new() -> Self {
112        Self {
113            is_valid: true,
114            checks: Vec::new(),
115            statistics: ValidationStatistics::default(),
116        }
117    }
118
119    /// Add a check result
120    pub fn add_check(&mut self, check: ValidationCheck) {
121        if !check.is_valid {
122            self.is_valid = false;
123        }
124        self.checks.push(check);
125    }
126
127    /// Format the result as a human-readable string
128    pub fn format(&self) -> String {
129        let mut output = format!("Topology Validation Result: {}\n", if self.is_valid { "PASS" } else { "FAIL" });
130        output.push_str("================================\n\n");
131
132        for check in &self.checks {
133            let status = if check.is_valid { "✓" } else { "✗" };
134            output.push_str(&format!("{} {}\n", status, check.name));
135
136            for error in &check.errors {
137                output.push_str(&format!("  Error: {}\n", error));
138            }
139
140            for warning in &check.warnings {
141                output.push_str(&format!("  Warning: {}\n", warning));
142            }
143
144            output.push_str("\n");
145        }
146
147        output.push_str(&format!("Statistics:\n"));
148        output.push_str(&format!("  Total checks: {}\n", self.checks.len()));
149        output.push_str(&format!("  Passed: {}\n", self.checks.iter().filter(|c| c.is_valid).count()));
150        output.push_str(&format!("  Failed: {}\n", self.checks.iter().filter(|c| !c.is_valid).count()));
151
152        output
153    }
154}
155
156/// Validation statistics
157#[derive(Debug, Clone, Serialize, Deserialize, Default)]
158pub struct ValidationStatistics {
159    /// Number of nodes
160    pub node_count: usize,
161    /// Number of edges
162    pub edge_count: usize,
163    /// Number of cycles detected
164    pub cycle_count: usize,
165    /// Number of orphaned nodes
166    pub orphaned_nodes: usize,
167}
168
169/// Topology validator for checking graph properties
170pub struct TopologyValidator {
171    graph: TopologyGraph,
172}
173
174impl TopologyValidator {
175    /// Create a new validator for the given topology graph
176    pub fn new(graph: TopologyGraph) -> Self {
177        Self { graph }
178    }
179
180    /// Run all validation checks
181    pub fn validate_all(&self) -> Result<ValidationResult> {
182        let mut result = ValidationResult::new();
183
184        // Update statistics
185        result.statistics.node_count = self.graph.node_count();
186        result.statistics.edge_count = self.graph.edge_count();
187
188        // Check topological ordering
189        result.add_check(self.validate_topological_order()?);
190
191        // Check for cycles
192        result.add_check(self.validate_no_cycles()?);
193
194        // Check node consistency
195        result.add_check(self.validate_node_consistency()?);
196
197        // Check edge consistency
198        result.add_check(self.validate_edge_consistency()?);
199
200        // Check build order
201        result.add_check(self.validate_build_order()?);
202
203        Ok(result)
204    }
205
206    /// Validate topological ordering
207    fn validate_topological_order(&self) -> Result<ValidationCheck> {
208        let mut check = ValidationCheck {
209            name: "Topological Order".to_string(),
210            is_valid: true,
211            errors: Vec::new(),
212            warnings: Vec::new(),
213        };
214
215        let topo_order = &self.graph.topological_order;
216        let rev_topo_order = &self.graph.reverse_topological_order;
217
218        // Check lengths
219        if topo_order.len() != self.graph.nodes.len() {
220            check.is_valid = false;
221            check.errors.push(format!(
222                "Topological order length mismatch: expected {}, got {}",
223                self.graph.nodes.len(),
224                topo_order.len()
225            ));
226        }
227
228        if rev_topo_order.len() != self.graph.nodes.len() {
229            check.is_valid = false;
230            check.errors.push(format!(
231                "Reverse topological order length mismatch: expected {}, got {}",
232                self.graph.nodes.len(),
233                rev_topo_order.len()
234            ));
235        }
236
237        // Check that all nodes are included
238        for node_name in self.graph.nodes.keys() {
239            if !topo_order.contains(node_name) {
240                check.is_valid = false;
241                check.errors.push(format!("Node '{}' missing from topological order", node_name));
242            }
243            if !rev_topo_order.contains(node_name) {
244                check.is_valid = false;
245                check.errors.push(format!("Node '{}' missing from reverse topological order", node_name));
246            }
247        }
248
249        Ok(check)
250    }
251
252    /// Validate that the graph has no cycles
253    fn validate_no_cycles(&self) -> Result<ValidationCheck> {
254        let mut check = ValidationCheck {
255            name: "No Cycles".to_string(),
256            is_valid: true,
257            errors: Vec::new(),
258            warnings: Vec::new(),
259        };
260
261        // Simple cycle detection using DFS
262        let mut visited = std::collections::HashSet::new();
263        let mut rec_stack = std::collections::HashSet::new();
264
265        for node_name in self.graph.nodes.keys() {
266            if !visited.contains(node_name) {
267                if self.has_cycle(node_name, &mut visited, &mut rec_stack) {
268                    check.is_valid = false;
269                    check.errors.push("Cycle detected in topology graph".to_string());
270                    break;
271                }
272            }
273        }
274
275        Ok(check)
276    }
277
278    /// DFS-based cycle detection helper
279    fn has_cycle(&self, node: &str, visited: &mut std::collections::HashSet<String>,
280                 rec_stack: &mut std::collections::HashSet<String>) -> bool {
281        visited.insert(node.to_string());
282        rec_stack.insert(node.to_string());
283
284        // Check all neighbors
285        for edge in &self.graph.edges {
286            if edge.from == node {
287                let neighbor = &edge.to;
288                if !visited.contains(neighbor) {
289                    if self.has_cycle(neighbor, visited, rec_stack) {
290                        return true;
291                    }
292                } else if rec_stack.contains(neighbor) {
293                    return true;
294                }
295            }
296        }
297
298        rec_stack.remove(node);
299        false
300    }
301
302    /// Validate node consistency
303    fn validate_node_consistency(&self) -> Result<ValidationCheck> {
304        let mut check = ValidationCheck {
305            name: "Node Consistency".to_string(),
306            is_valid: true,
307            errors: Vec::new(),
308            warnings: Vec::new(),
309        };
310
311        for (node_name, node) in &self.graph.nodes {
312            // Check that dependencies exist
313            for dep in &node.dependencies {
314                if !self.graph.nodes.contains_key(dep) {
315                    check.is_valid = false;
316                    check.errors.push(format!(
317                        "Node '{}' has dependency '{}' which does not exist",
318                        node_name, dep
319                    ));
320                }
321            }
322
323            // Check build order is reasonable
324            if node.build_order == 0 {
325                check.warnings.push(format!("Node '{}' has build order 0", node_name));
326            }
327        }
328
329        Ok(check)
330    }
331
332    /// Validate edge consistency
333    fn validate_edge_consistency(&self) -> Result<ValidationCheck> {
334        let mut check = ValidationCheck {
335            name: "Edge Consistency".to_string(),
336            is_valid: true,
337            errors: Vec::new(),
338            warnings: Vec::new(),
339        };
340
341        for edge in &self.graph.edges {
342            // Check that both nodes exist
343            if !self.graph.nodes.contains_key(&edge.from) {
344                check.is_valid = false;
345                check.errors.push(format!("Edge references non-existent source node '{}'", edge.from));
346            }
347            if !self.graph.nodes.contains_key(&edge.to) {
348                check.is_valid = false;
349                check.errors.push(format!("Edge references non-existent target node '{}'", edge.to));
350            }
351
352            // Check for self-loops
353            if edge.from == edge.to {
354                check.warnings.push(format!("Self-loop detected on node '{}'", edge.from));
355            }
356        }
357
358        Ok(check)
359    }
360
361    /// Validate build order
362    fn validate_build_order(&self) -> Result<ValidationCheck> {
363        let mut check = ValidationCheck {
364            name: "Build Order".to_string(),
365            is_valid: true,
366            errors: Vec::new(),
367            warnings: Vec::new(),
368        };
369
370        let mut build_orders: Vec<u32> = self.graph.nodes.values()
371            .map(|n| n.build_order)
372            .collect();
373        build_orders.sort();
374        build_orders.dedup();
375
376        // Check for gaps in build order
377        if !build_orders.is_empty() {
378            let min_order = build_orders[0];
379            let _max_order = *build_orders.last().unwrap();
380
381            if min_order != 1 {
382                check.warnings.push(format!("Build order starts at {} instead of 1", min_order));
383            }
384
385            // Check for gaps
386            for i in 1..build_orders.len() {
387                let expected = build_orders[i-1] + 1;
388                if build_orders[i] != expected {
389                    check.warnings.push(format!(
390                        "Gap in build order: missing order {}",
391                        expected
392                    ));
393                }
394            }
395        }
396
397        Ok(check)
398    }
399}
400
401/// Result type alias for topology operations
402pub type Result<T> = std::result::Result<T, KotobaError>;
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn test_empty_topology() {
410        let graph = TopologyGraph::new();
411        assert_eq!(graph.node_count(), 0);
412        assert_eq!(graph.edge_count(), 0);
413    }
414
415    #[test]
416    fn test_add_node() {
417        let mut graph = TopologyGraph::new();
418        let node = Node {
419            name: "test".to_string(),
420            path: "/test".to_string(),
421            node_type: "crate".to_string(),
422            description: "Test node".to_string(),
423            dependencies: Vec::new(),
424            provides: Vec::new(),
425            status: "active".to_string(),
426            build_order: 1,
427        };
428
429        graph.add_node(node);
430        assert_eq!(graph.node_count(), 1);
431        assert!(graph.nodes.contains_key("test"));
432    }
433
434    #[test]
435    fn test_add_edge() {
436        let mut graph = TopologyGraph::new();
437        let edge = Edge {
438            from: "a".to_string(),
439            to: "b".to_string(),
440        };
441
442        graph.add_edge(edge);
443        assert_eq!(graph.edge_count(), 1);
444    }
445
446    #[test]
447    fn test_validation_result_formatting() {
448        let mut result = ValidationResult::new();
449        result.add_check(ValidationCheck {
450            name: "Test Check".to_string(),
451            is_valid: false,
452            errors: vec!["Test error".to_string()],
453            warnings: vec!["Test warning".to_string()],
454        });
455
456        let formatted = result.format();
457        assert!(formatted.contains("FAIL"));
458        assert!(formatted.contains("Test error"));
459        assert!(formatted.contains("Test warning"));
460    }
461}