Download src/validators/governance-validator.rs from Snapkitty/icp-dag-crystallizer: direct link, hf CLI and curl.
- Browser
- Download file 25.2 kB
-
https://huggingface.co/Snapkitty/icp-dag-crystallizer/resolve/main/src/validators/governance-validator.rs
- Command line
-
hf download hf://Snapkitty/icp-dag-crystallizer/src/validators/governance-validator.rs
-
curl -L -o governance-validator.rs https://huggingface.co/Snapkitty/icp-dag-crystallizer/resolve/main/src/validators/governance-validator.rs
25.2 kB
| // SPDX-License-Identifier: LicenseRef-Sovereign-Leviathan-SL-AGPL3-001 | |
| // Licensing: see LICENSE in the repository root. | |
| /// ICP-DAG Governance Protocol Validator | |
| /// Validates agent communication against Integrity Constraint Protocol rules | |
| /// with Directed Acyclic Graph (DAG) consistency checking | |
| /// | |
| /// Rules: | |
| /// I2: DAG acyclicity - no cycles in agent dependency graph | |
| /// I3: Unknown claims cannot authorize - unknown state prevents decision | |
| /// I4: Contradicted claims cannot authorize - conflicting proofs invalidate | |
| /// I5: Execution needs authorized decision - exec→decision→proof→claim chain | |
| /// I9: Claim state consistency - no simultaneous unknown+verified | |
| /// I10: Reachability acyclicity - transitive dependencies must be acyclic | |
| use std::collections::{HashMap, HashSet, VecDeque}; | |
| use std::fmt; | |
| use serde::{Deserialize, Serialize}; | |
| pub enum ClaimState { | |
| Unknown, | |
| Verified, | |
| Contradicted, | |
| Pending, | |
| } | |
| pub struct Claim { | |
| pub id: String, | |
| pub agent: String, | |
| pub content: String, | |
| pub state: ClaimState, | |
| pub timestamp: u64, | |
| } | |
| pub struct Proof { | |
| pub claim_id: String, | |
| pub proof_chain: Vec<String>, // chain of claims this proof depends on | |
| pub verifier_agent: String, | |
| pub is_valid: bool, | |
| } | |
| pub struct Decision { | |
| pub decision_id: String, | |
| pub agent: String, | |
| pub supporting_proofs: Vec<String>, | |
| pub conflicting_proofs: Vec<String>, | |
| pub authorized: bool, | |
| pub timestamp: u64, | |
| } | |
| pub struct Execution { | |
| pub exec_id: String, | |
| pub agent: String, | |
| pub decision_id: String, | |
| pub executed: bool, | |
| pub timestamp: u64, | |
| } | |
| pub struct AgentMessage { | |
| pub sender: String, | |
| pub receiver: String, | |
| pub claim: Claim, | |
| pub message_id: String, | |
| pub depends_on: Vec<String>, // message IDs this depends on | |
| } | |
| pub struct ProtocolViolation { | |
| pub violation_type: String, | |
| pub description: String, | |
| pub affected_entity: String, | |
| pub rule_id: String, | |
| } | |
| pub struct ValidationReport { | |
| pub violations: Vec<ProtocolViolation>, | |
| pub is_valid: bool, | |
| pub dag_structure: DAGStructure, | |
| pub claim_analysis: ClaimAnalysis, | |
| pub execution_chains: Vec<ExecutionChain>, | |
| } | |
| pub struct DAGStructure { | |
| pub nodes: Vec<String>, | |
| pub edges: Vec<(String, String)>, | |
| pub is_acyclic: bool, | |
| pub cycles: Vec<Vec<String>>, | |
| pub topological_sort: Vec<String>, | |
| } | |
| pub struct ClaimAnalysis { | |
| pub total_claims: usize, | |
| pub unknown_claims: usize, | |
| pub verified_claims: usize, | |
| pub contradicted_claims: usize, | |
| pub state_inconsistencies: Vec<String>, | |
| } | |
| pub struct ExecutionChain { | |
| pub chain_id: String, | |
| pub execution_id: String, | |
| pub decision_id: String, | |
| pub proofs: Vec<String>, | |
| pub claims: Vec<String>, | |
| pub is_valid_chain: bool, | |
| } | |
| pub struct ICPDAGValidator { | |
| claims: HashMap<String, Claim>, | |
| proofs: HashMap<String, Proof>, | |
| decisions: HashMap<String, Decision>, | |
| executions: HashMap<String, Execution>, | |
| messages: Vec<AgentMessage>, | |
| violations: Vec<ProtocolViolation>, | |
| } | |
| impl ICPDAGValidator { | |
| pub fn new() -> Self { | |
| ICPDAGValidator { | |
| claims: HashMap::new(), | |
| proofs: HashMap::new(), | |
| decisions: HashMap::new(), | |
| executions: HashMap::new(), | |
| messages: Vec::new(), | |
| violations: Vec::new(), | |
| } | |
| } | |
| /// Register a claim in the validation system | |
| pub fn register_claim(&mut self, claim: Claim) { | |
| self.claims.insert(claim.id.clone(), claim); | |
| } | |
| /// Register a proof linking claims | |
| pub fn register_proof(&mut self, proof: Proof) { | |
| self.proofs.insert(proof.claim_id.clone(), proof); | |
| } | |
| /// Register a decision with supporting/conflicting proofs | |
| pub fn register_decision(&mut self, decision: Decision) { | |
| self.decisions.insert(decision.decision_id.clone(), decision); | |
| } | |
| /// Register an execution of a decision | |
| pub fn register_execution(&mut self, execution: Execution) { | |
| self.executions.insert(execution.exec_id.clone(), execution); | |
| } | |
| /// Register agent-to-agent communication messages | |
| pub fn register_message(&mut self, message: AgentMessage) { | |
| self.messages.push(message); | |
| } | |
| /// Rule I3: Unknown claims cannot authorize | |
| fn validate_i3_unknown_claims(&mut self) { | |
| for (decision_id, decision) in &self.decisions { | |
| for proof_id in &decision.supporting_proofs { | |
| if let Some(proof) = self.proofs.get(proof_id) { | |
| for claim_id in &proof.proof_chain { | |
| if let Some(claim) = self.claims.get(claim_id) { | |
| if claim.state == ClaimState::Unknown { | |
| self.violations.push(ProtocolViolation { | |
| violation_type: "I3_VIOLATION".to_string(), | |
| description: format!( | |
| "Unknown claim {} used in proof chain for decision {}", | |
| claim_id, decision_id | |
| ), | |
| affected_entity: claim_id.clone(), | |
| rule_id: "I3".to_string(), | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /// Rule I4: Contradicted claims cannot authorize | |
| fn validate_i4_contradicted_claims(&mut self) { | |
| for (decision_id, decision) in &self.decisions { | |
| // Check if supporting proofs depend on contradicted claims | |
| for proof_id in &decision.supporting_proofs { | |
| if let Some(proof) = self.proofs.get(proof_id) { | |
| for claim_id in &proof.proof_chain { | |
| if let Some(claim) = self.claims.get(claim_id) { | |
| if claim.state == ClaimState::Contradicted { | |
| self.violations.push(ProtocolViolation { | |
| violation_type: "I4_VIOLATION".to_string(), | |
| description: format!( | |
| "Contradicted claim {} used in proof for decision {}", | |
| claim_id, decision_id | |
| ), | |
| affected_entity: claim_id.clone(), | |
| rule_id: "I4".to_string(), | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Check for conflicting proofs | |
| if !decision.conflicting_proofs.is_empty() && !decision.supporting_proofs.is_empty() { | |
| self.violations.push(ProtocolViolation { | |
| violation_type: "I4_VIOLATION".to_string(), | |
| description: format!( | |
| "Decision {} has both supporting and conflicting proofs", | |
| decision_id | |
| ), | |
| affected_entity: decision_id.clone(), | |
| rule_id: "I4".to_string(), | |
| }); | |
| } | |
| } | |
| } | |
| /// Rule I5: Execution needs authorized decision | |
| fn validate_i5_execution_chain(&mut self) { | |
| for (exec_id, execution) in &self.executions { | |
| if !execution.executed { | |
| return; // Skip unexecuted | |
| } | |
| // Trace: execution → decision → proof → claim | |
| if let Some(decision) = self.decisions.get(&execution.decision_id) { | |
| if !decision.authorized { | |
| self.violations.push(ProtocolViolation { | |
| violation_type: "I5_VIOLATION".to_string(), | |
| description: format!( | |
| "Execution {} uses unauthorized decision {}", | |
| exec_id, &execution.decision_id | |
| ), | |
| affected_entity: exec_id.clone(), | |
| rule_id: "I5".to_string(), | |
| }); | |
| continue; | |
| } | |
| // Verify proofs exist and are valid | |
| for proof_id in &decision.supporting_proofs { | |
| if let Some(proof) = self.proofs.get(proof_id) { | |
| if !proof.is_valid { | |
| self.violations.push(ProtocolViolation { | |
| violation_type: "I5_VIOLATION".to_string(), | |
| description: format!( | |
| "Execution {} depends on invalid proof {}", | |
| exec_id, proof_id | |
| ), | |
| affected_entity: exec_id.clone(), | |
| rule_id: "I5".to_string(), | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /// Rule I9: Claim state consistency | |
| fn validate_i9_claim_consistency(&mut self) -> ClaimAnalysis { | |
| let mut analysis = ClaimAnalysis { | |
| total_claims: self.claims.len(), | |
| unknown_claims: 0, | |
| verified_claims: 0, | |
| contradicted_claims: 0, | |
| state_inconsistencies: Vec::new(), | |
| }; | |
| let mut claim_state_map: HashMap<String, HashSet<ClaimState>> = HashMap::new(); | |
| for (claim_id, claim) in &self.claims { | |
| match claim.state { | |
| ClaimState::Unknown => analysis.unknown_claims += 1, | |
| ClaimState::Verified => analysis.verified_claims += 1, | |
| ClaimState::Contradicted => analysis.contradicted_claims += 1, | |
| ClaimState::Pending => {} | |
| } | |
| claim_state_map | |
| .entry(claim_id.clone()) | |
| .or_insert_with(HashSet::new) | |
| .insert(claim.state.clone()); | |
| } | |
| // Check for simultaneous unknown+verified | |
| for (claim_id, states) in &claim_state_map { | |
| if states.contains(&ClaimState::Unknown) && states.contains(&ClaimState::Verified) { | |
| analysis.state_inconsistencies.push(claim_id.clone()); | |
| self.violations.push(ProtocolViolation { | |
| violation_type: "I9_VIOLATION".to_string(), | |
| description: format!( | |
| "Claim {} has simultaneous unknown and verified states", | |
| claim_id | |
| ), | |
| affected_entity: claim_id.clone(), | |
| rule_id: "I9".to_string(), | |
| }); | |
| } | |
| // Cannot be both verified and contradicted | |
| if states.contains(&ClaimState::Verified) && states.contains(&ClaimState::Contradicted) | |
| { | |
| analysis.state_inconsistencies.push(claim_id.clone()); | |
| self.violations.push(ProtocolViolation { | |
| violation_type: "I9_VIOLATION".to_string(), | |
| description: format!( | |
| "Claim {} has simultaneous verified and contradicted states", | |
| claim_id | |
| ), | |
| affected_entity: claim_id.clone(), | |
| rule_id: "I9".to_string(), | |
| }); | |
| } | |
| } | |
| analysis | |
| } | |
| /// Rules I2 & I10: Acyclicity detection using DFS | |
| fn validate_acyclicity(&self) -> DAGStructure { | |
| let mut dag_structure = DAGStructure { | |
| nodes: Vec::new(), | |
| edges: Vec::new(), | |
| is_acyclic: true, | |
| cycles: Vec::new(), | |
| topological_sort: Vec::new(), | |
| }; | |
| // Build dependency graph from messages | |
| let mut graph: HashMap<String, Vec<String>> = HashMap::new(); | |
| let mut all_agents: HashSet<String> = HashSet::new(); | |
| for message in &self.messages { | |
| all_agents.insert(message.sender.clone()); | |
| all_agents.insert(message.receiver.clone()); | |
| graph | |
| .entry(message.sender.clone()) | |
| .or_insert_with(Vec::new) | |
| .push(message.receiver.clone()); | |
| // Add dependencies | |
| for dep_id in &message.depends_on { | |
| if let Some(dep_msg) = self.messages.iter().find(|m| &m.message_id == dep_id) { | |
| graph | |
| .entry(dep_msg.sender.clone()) | |
| .or_insert_with(Vec::new) | |
| .push(message.sender.clone()); | |
| } | |
| } | |
| } | |
| dag_structure.nodes = all_agents.iter().cloned().collect(); | |
| for (from, tos) in &graph { | |
| for to in tos { | |
| dag_structure.edges.push((from.clone(), to.clone())); | |
| } | |
| } | |
| // Detect cycles using DFS | |
| let cycles = self.detect_cycles(&graph); | |
| dag_structure.is_acyclic = cycles.is_empty(); | |
| dag_structure.cycles = cycles; | |
| // Topological sort if acyclic | |
| if dag_structure.is_acyclic { | |
| dag_structure.topological_sort = self.topological_sort(&graph); | |
| } | |
| dag_structure | |
| } | |
| /// Detect cycles using depth-first search | |
| fn detect_cycles(&self, graph: &HashMap<String, Vec<String>>) -> Vec<Vec<String>> { | |
| let mut visited: HashMap<String, i32> = HashMap::new(); | |
| let mut cycles: Vec<Vec<String>> = Vec::new(); | |
| let mut path: Vec<String> = Vec::new(); | |
| for node in graph.keys() { | |
| if !visited.contains_key(node) { | |
| self.dfs_cycle(node, graph, &mut visited, &mut path, &mut cycles); | |
| } | |
| } | |
| cycles | |
| } | |
| /// DFS helper for cycle detection | |
| fn dfs_cycle( | |
| &self, | |
| node: &str, | |
| graph: &HashMap<String, Vec<String>>, | |
| visited: &mut HashMap<String, i32>, | |
| path: &mut Vec<String>, | |
| cycles: &mut Vec<Vec<String>>, | |
| ) { | |
| visited.insert(node.to_string(), 0); // 0 = visiting | |
| path.push(node.to_string()); | |
| if let Some(neighbors) = graph.get(node) { | |
| for neighbor in neighbors { | |
| match visited.get(neighbor).copied() { | |
| None => { | |
| self.dfs_cycle(neighbor, graph, visited, path, cycles); | |
| } | |
| Some(0) => { | |
| // Back edge found - cycle detected | |
| if let Some(cycle_start) = path.iter().position(|n| n == neighbor) { | |
| let cycle = path[cycle_start..].to_vec(); | |
| cycles.push(cycle); | |
| } | |
| } | |
| _ => {} // Already visited | |
| } | |
| } | |
| } | |
| path.pop(); | |
| visited.insert(node.to_string(), 1); // 1 = done | |
| } | |
| /// Topological sort using Kahn's algorithm | |
| fn topological_sort(&self, graph: &HashMap<String, Vec<String>>) -> Vec<String> { | |
| let mut in_degree: HashMap<String, usize> = HashMap::new(); | |
| let mut all_nodes: HashSet<String> = HashSet::new(); | |
| // Initialize in-degrees | |
| for (from, tos) in graph { | |
| all_nodes.insert(from.clone()); | |
| in_degree.entry(from.clone()).or_insert(0); | |
| for to in tos { | |
| all_nodes.insert(to.clone()); | |
| *in_degree.entry(to.clone()).or_insert(0) += 1; | |
| } | |
| } | |
| let mut queue: VecDeque<String> = VecDeque::new(); | |
| for node in &all_nodes { | |
| if in_degree.get(node).copied().unwrap_or(0) == 0 { | |
| queue.push_back(node.clone()); | |
| } | |
| } | |
| let mut result: Vec<String> = Vec::new(); | |
| while let Some(node) = queue.pop_front() { | |
| result.push(node.clone()); | |
| if let Some(neighbors) = graph.get(&node) { | |
| for neighbor in neighbors { | |
| let degree = in_degree.entry(neighbor.clone()).or_insert(0); | |
| *degree -= 1; | |
| if *degree == 0 { | |
| queue.push_back(neighbor.clone()); | |
| } | |
| } | |
| } | |
| } | |
| result | |
| } | |
| /// Build execution chains: execution → decision → proofs → claims | |
| fn build_execution_chains(&self) -> Vec<ExecutionChain> { | |
| let mut chains: Vec<ExecutionChain> = Vec::new(); | |
| for (exec_id, execution) in &self.executions { | |
| let mut chain = ExecutionChain { | |
| chain_id: format!("chain_{}", exec_id), | |
| execution_id: exec_id.clone(), | |
| decision_id: execution.decision_id.clone(), | |
| proofs: Vec::new(), | |
| claims: Vec::new(), | |
| is_valid_chain: true, | |
| }; | |
| if let Some(decision) = self.decisions.get(&execution.decision_id) { | |
| chain.proofs = decision.supporting_proofs.clone(); | |
| for proof_id in &chain.proofs { | |
| if let Some(proof) = self.proofs.get(proof_id) { | |
| for claim_id in &proof.proof_chain { | |
| chain.claims.push(claim_id.clone()); | |
| } | |
| } | |
| } | |
| // Validate chain | |
| chain.is_valid_chain = decision.authorized | |
| && chain.proofs.iter().all(|p| { | |
| self.proofs | |
| .get(p) | |
| .map(|proof| proof.is_valid) | |
| .unwrap_or(false) | |
| }) | |
| && chain.claims.iter().all(|c| { | |
| self.claims | |
| .get(c) | |
| .map(|claim| claim.state == ClaimState::Verified) | |
| .unwrap_or(false) | |
| }); | |
| } | |
| chains.push(chain); | |
| } | |
| chains | |
| } | |
| /// Perform complete validation | |
| pub fn validate(&mut self) -> ValidationReport { | |
| self.violations.clear(); | |
| // Rule validations | |
| self.validate_i3_unknown_claims(); | |
| self.validate_i4_contradicted_claims(); | |
| self.validate_i5_execution_chain(); | |
| let claim_analysis = self.validate_i9_claim_consistency(); | |
| let dag_structure = self.validate_acyclicity(); | |
| // Check acyclicity rules | |
| if !dag_structure.is_acyclic { | |
| for cycle in &dag_structure.cycles { | |
| self.violations.push(ProtocolViolation { | |
| violation_type: "I2_I10_VIOLATION".to_string(), | |
| description: format!("Cycle detected in DAG: {:?}", cycle), | |
| affected_entity: cycle.join(" -> "), | |
| rule_id: "I2_I10".to_string(), | |
| }); | |
| } | |
| } | |
| let execution_chains = self.build_execution_chains(); | |
| let is_valid = self.violations.is_empty(); | |
| ValidationReport { | |
| violations: self.violations.clone(), | |
| is_valid, | |
| dag_structure, | |
| claim_analysis, | |
| execution_chains, | |
| } | |
| } | |
| } | |
| impl fmt::Display for ValidationReport { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| writeln!(f, "=== ICP-DAG Governance Protocol Validation Report ===")?; | |
| writeln!(f)?; | |
| writeln!(f, "Overall Status: {}", if self.is_valid { "VALID" } else { "INVALID" })?; | |
| writeln!(f, "Total Violations: {}", self.violations.len())?; | |
| writeln!(f)?; | |
| if !self.violations.is_empty() { | |
| writeln!(f, "--- Protocol Violations ---")?; | |
| for (idx, violation) in self.violations.iter().enumerate() { | |
| writeln!(f, "{}. [{}] {}", idx + 1, violation.rule_id, violation.violation_type)?; | |
| writeln!(f, " Entity: {}", violation.affected_entity)?; | |
| writeln!(f, " Desc: {}", violation.description)?; | |
| } | |
| writeln!(f)?; | |
| } | |
| writeln!(f, "--- Claim Analysis ---")?; | |
| writeln!(f, "Total Claims: {}", self.claim_analysis.total_claims)?; | |
| writeln!(f, "Verified: {}", self.claim_analysis.verified_claims)?; | |
| writeln!(f, "Unknown: {}", self.claim_analysis.unknown_claims)?; | |
| writeln!(f, "Contradicted: {}", self.claim_analysis.contradicted_claims)?; | |
| writeln!(f, "State Inconsistencies: {}", self.claim_analysis.state_inconsistencies.len())?; | |
| writeln!(f)?; | |
| writeln!(f, "--- DAG Structure ---")?; | |
| writeln!(f, "Nodes: {}", self.dag_structure.nodes.len())?; | |
| writeln!(f, "Edges: {}", self.dag_structure.edges.len())?; | |
| writeln!(f, "Acyclic: {}", self.dag_structure.is_acyclic)?; | |
| if !self.dag_structure.cycles.is_empty() { | |
| writeln!(f, "Cycles Found: {}", self.dag_structure.cycles.len())?; | |
| } | |
| writeln!(f)?; | |
| writeln!(f, "--- Execution Chains ---")?; | |
| writeln!(f, "Total Chains: {}", self.execution_chains.len())?; | |
| for chain in &self.execution_chains { | |
| writeln!(f, " {}: {} proofs, {} claims, Valid: {}", | |
| chain.chain_id, chain.proofs.len(), chain.claims.len(), chain.is_valid_chain)?; | |
| } | |
| Ok(()) | |
| } | |
| } | |
| mod tests { | |
| use super::*; | |
| fn test_i3_unknown_claims() { | |
| let mut validator = ICPDAGValidator::new(); | |
| let unknown_claim = Claim { | |
| id: "claim_1".to_string(), | |
| agent: "agent_a".to_string(), | |
| content: "test".to_string(), | |
| state: ClaimState::Unknown, | |
| timestamp: 0, | |
| }; | |
| validator.register_claim(unknown_claim); | |
| let proof = Proof { | |
| claim_id: "proof_1".to_string(), | |
| proof_chain: vec!["claim_1".to_string()], | |
| verifier_agent: "agent_b".to_string(), | |
| is_valid: true, | |
| }; | |
| validator.register_proof(proof); | |
| let decision = Decision { | |
| decision_id: "dec_1".to_string(), | |
| agent: "agent_c".to_string(), | |
| supporting_proofs: vec!["proof_1".to_string()], | |
| conflicting_proofs: vec![], | |
| authorized: true, | |
| timestamp: 0, | |
| }; | |
| validator.register_decision(decision); | |
| let report = validator.validate(); | |
| assert!(!report.is_valid); | |
| assert!(report.violations.iter().any(|v| v.rule_id == "I3")); | |
| } | |
| fn test_i9_state_consistency() { | |
| let mut validator = ICPDAGValidator::new(); | |
| let claim = Claim { | |
| id: "claim_1".to_string(), | |
| agent: "agent_a".to_string(), | |
| content: "test".to_string(), | |
| state: ClaimState::Verified, | |
| timestamp: 0, | |
| }; | |
| validator.register_claim(claim); | |
| let report = validator.validate(); | |
| assert!(report.is_valid); | |
| assert_eq!(report.claim_analysis.verified_claims, 1); | |
| } | |
| } | |
| fn main() { | |
| let mut validator = ICPDAGValidator::new(); | |
| // Example: Register verified claim | |
| let verified_claim = Claim { | |
| id: "claim_verified_1".to_string(), | |
| agent: "agent_governance".to_string(), | |
| content: "Governance protocol v1.0 active".to_string(), | |
| state: ClaimState::Verified, | |
| timestamp: 1000, | |
| }; | |
| validator.register_claim(verified_claim); | |
| // Example: Register proof | |
| let proof = Proof { | |
| claim_id: "proof_1".to_string(), | |
| proof_chain: vec!["claim_verified_1".to_string()], | |
| verifier_agent: "agent_validator".to_string(), | |
| is_valid: true, | |
| }; | |
| validator.register_proof(proof); | |
| // Example: Register authorized decision | |
| let decision = Decision { | |
| decision_id: "decision_1".to_string(), | |
| agent: "agent_consensus".to_string(), | |
| supporting_proofs: vec!["proof_1".to_string()], | |
| conflicting_proofs: vec![], | |
| authorized: true, | |
| timestamp: 1001, | |
| }; | |
| validator.register_decision(decision); | |
| // Example: Register execution | |
| let execution = Execution { | |
| exec_id: "exec_1".to_string(), | |
| agent: "agent_executor".to_string(), | |
| decision_id: "decision_1".to_string(), | |
| executed: true, | |
| timestamp: 1002, | |
| }; | |
| validator.register_execution(execution); | |
| // Validate | |
| let report = validator.validate(); | |
| println!("{}", report); | |
| println!("\nDAG as JSON:"); | |
| println!("{}", serde_json::to_string_pretty(&report.dag_structure).unwrap()); | |
| } | |