ggen_cli_lib/cmds/graph/
diff.rs

1use clap::Args;
2use ggen_core::delta::{GraphDelta, ImpactAnalyzer};
3use ggen_core::graph::Graph;
4use ggen_utils::error::Result;
5use std::path::PathBuf;
6
7#[derive(Args, Debug)]
8pub struct DiffArgs {
9    /// Path to baseline RDF file
10    #[arg(short, long)]
11    pub baseline: PathBuf,
12
13    /// Path to current RDF file
14    #[arg(short, long)]
15    pub current: PathBuf,
16
17    /// Output format (human, json, hash)
18    #[arg(short = 'o', long, default_value = "human")]
19    pub format: String,
20
21    /// Filter results to specific IRIs
22    #[arg(short = 'f', long)]
23    pub filter: Vec<String>,
24
25    /// Show affected templates
26    #[arg(short = 't', long)]
27    pub show_templates: bool,
28
29    /// Template paths to analyze for impacts
30    #[arg(short = 'T', long)]
31    pub template_paths: Vec<PathBuf>,
32}
33
34pub async fn run(args: &DiffArgs) -> Result<()> {
35    // Load baseline graph
36    let baseline_graph = Graph::new()?;
37    baseline_graph.load_path(&args.baseline)?;
38
39    // Load current graph
40    let current_graph = Graph::new()?;
41    current_graph.load_path(&args.current)?;
42
43    // Compute delta
44    let delta = GraphDelta::new(&baseline_graph, &current_graph)?;
45
46    // Apply filters if specified
47    let delta = if !args.filter.is_empty() {
48        delta.filter_by_iris(&args.filter)
49    } else {
50        delta
51    };
52
53    // Output based on format
54    match args.format.as_str() {
55        "human" => print_human(&delta)?,
56        "json" => println!("{}", serde_json::to_string_pretty(&delta)?),
57        "hash" => println!("{}", delta.baseline_hash.as_ref().unwrap_or(&String::new())),
58        _ => {
59            return Err(ggen_utils::error::Error::new(&format!(
60                "Unknown format: {}",
61                args.format
62            )))
63        }
64    }
65
66    // Show template impacts if requested
67    if args.show_templates && !args.template_paths.is_empty() {
68        let mut analyzer = ImpactAnalyzer::new();
69        let impacts = analyzer.analyze_impacts(
70            &delta,
71            &args
72                .template_paths
73                .iter()
74                .map(|p| p.to_string_lossy().to_string())
75                .collect::<Vec<_>>(),
76            &baseline_graph,
77        )?;
78
79        println!("\nTemplate Impacts:");
80        for impact in impacts {
81            if impact.is_confident(0.1) {
82                println!(
83                    "  {}: {:.2} - {}",
84                    impact.template_path, impact.confidence, impact.reason
85                );
86            }
87        }
88    }
89
90    Ok(())
91}
92
93fn print_human(delta: &GraphDelta) -> Result<()> {
94    if delta.is_empty() {
95        println!("No changes detected between graphs.");
96        return Ok(());
97    }
98
99    println!("Graph Delta Summary:");
100    println!(
101        "  Baseline hash: {}",
102        delta.baseline_hash.as_deref().unwrap_or("unknown")
103    );
104    println!(
105        "  Current hash:  {}",
106        delta.current_hash.as_deref().unwrap_or("unknown")
107    );
108    println!(
109        "  Computed at:   {}",
110        delta.computed_at.format("%Y-%m-%d %H:%M:%S UTC")
111    );
112
113    let counts = delta.counts();
114    println!("  Changes:");
115    println!(
116        "    Additions:     {}",
117        counts.get("additions").unwrap_or(&0)
118    );
119    println!(
120        "    Deletions:     {}",
121        counts.get("deletions").unwrap_or(&0)
122    );
123    println!(
124        "    Modifications: {}",
125        counts.get("modifications").unwrap_or(&0)
126    );
127
128    if !delta.deltas.is_empty() {
129        println!("\nDetailed Changes:");
130        for (i, delta_change) in delta.deltas.iter().enumerate() {
131            println!("  {}. {}", i + 1, delta_change);
132        }
133    }
134
135    Ok(())
136}