ggen_core/
delta.rs

1//! Delta-driven projection for detecting and analyzing RDF graph changes
2//!
3//! This module provides functionality to detect and analyze changes in RDF graphs,
4//! determine which templates are affected by those changes, and support three-way
5//! merging for files that contain both generated and manual content.
6//!
7//! ## Features
8//!
9//! - **Graph Comparison**: Compare two RDF graphs and detect semantic differences
10//! - **Delta Types**: Track additions, deletions, and modifications of triples
11//! - **Impact Analysis**: Determine which templates are affected by graph changes
12//! - **Template Impact**: Calculate impact scores for template regeneration
13//! - **Change Detection**: Efficient change detection using graph hashing
14//!
15//! ## Delta Types
16//!
17//! - **Addition**: New triple added to the graph
18//! - **Deletion**: Triple removed from the graph
19//! - **Modification**: Triple's object value changed
20//!
21//! ## Examples
22//!
23//! ### Detecting Graph Changes
24//!
25//! ```rust,no_run
26//! use ggen_core::delta::{GraphDelta, ImpactAnalyzer};
27//! use ggen_core::graph::Graph;
28//!
29//! # fn main() -> ggen_utils::error::Result<()> {
30//! let old_graph = Graph::new()?;
31//! let new_graph = Graph::new()?;
32//!
33//! let delta = GraphDelta::compute(&old_graph, &new_graph)?;
34//! println!("Detected {} changes", delta.changes.len());
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! ### Analyzing Template Impact
40//!
41//! ```rust,no_run
42//! use ggen_core::delta::ImpactAnalyzer;
43//! use ggen_core::graph::Graph;
44//!
45//! # fn main() -> ggen_utils::error::Result<()> {
46//! let analyzer = ImpactAnalyzer::new();
47//! let old_graph = Graph::new()?;
48//! let new_graph = Graph::new()?;
49//!
50//! let impacts = analyzer.analyze(&old_graph, &new_graph, &["template1.tmpl".into()])?;
51//! for impact in impacts {
52//!     println!("Template {}: impact score {}", impact.template_path.display(), impact.score);
53//! }
54//! # Ok(())
55//! # }
56//! ```
57
58use ahash::AHasher;
59use ggen_utils::error::Result;
60use oxigraph::model::Quad;
61use serde::{Deserialize, Serialize};
62use std::collections::{BTreeMap, BTreeSet};
63use std::fmt;
64use std::hash::{Hash, Hasher};
65
66use crate::graph::Graph;
67
68/// Represents a semantic change in an RDF graph
69///
70/// A `DeltaType` describes a single change between two RDF graphs:
71/// - **Addition**: A new triple was added
72/// - **Deletion**: A triple was removed
73/// - **Modification**: A triple's object value changed
74///
75/// # Examples
76///
77/// ```rust
78/// use ggen_core::delta::DeltaType;
79///
80/// # fn main() {
81/// // Addition example
82/// let addition = DeltaType::Addition {
83///     subject: "http://example.org/subject".to_string(),
84///     predicate: "http://example.org/predicate".to_string(),
85///     object: "http://example.org/object".to_string(),
86/// };
87/// match addition {
88///     DeltaType::Addition { .. } => assert!(true),
89///     _ => panic!("Should be Addition"),
90/// }
91///
92/// // Deletion example
93/// let deletion = DeltaType::Deletion {
94///     subject: "http://example.org/subject".to_string(),
95///     predicate: "http://example.org/predicate".to_string(),
96///     object: "http://example.org/object".to_string(),
97/// };
98/// match deletion {
99///     DeltaType::Deletion { .. } => assert!(true),
100///     _ => panic!("Should be Deletion"),
101/// }
102///
103/// // Modification example
104/// let modification = DeltaType::Modification {
105///     subject: "http://example.org/subject".to_string(),
106///     predicate: "http://example.org/predicate".to_string(),
107///     old_object: "old".to_string(),
108///     new_object: "new".to_string(),
109/// };
110/// match modification {
111///     DeltaType::Modification { .. } => assert!(true),
112///     _ => panic!("Should be Modification"),
113/// }
114/// # }
115/// ```
116///
117/// ```rust,no_run
118/// use ggen_core::delta::DeltaType;
119/// use oxigraph::model::{NamedNode, Literal, Quad};
120///
121/// # fn main() -> ggen_utils::error::Result<()> {
122/// // Create an addition delta
123/// let addition = DeltaType::Addition {
124///     subject: "http://example.org/alice".to_string(),
125///     predicate: "http://example.org/name".to_string(),
126///     object: "Alice".to_string(),
127/// };
128///
129/// // Check which IRIs are affected
130/// let affected = addition.subjects();
131/// assert_eq!(affected, vec!["http://example.org/alice"]);
132/// # Ok(())
133/// # }
134/// ```
135///
136/// ```rust,no_run
137/// use ggen_core::delta::DeltaType;
138///
139/// # fn main() -> ggen_utils::error::Result<()> {
140/// // Create a modification delta
141/// let modification = DeltaType::Modification {
142///     subject: "http://example.org/alice".to_string(),
143///     predicate: "http://example.org/age".to_string(),
144///     old_object: "30".to_string(),
145///     new_object: "31".to_string(),
146/// };
147///
148/// // Check if a specific IRI is affected
149/// assert!(modification.affects_iri("http://example.org/alice"));
150/// # Ok(())
151/// # }
152/// ```
153#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
154pub enum DeltaType {
155    /// A triple was added to the graph
156    Addition {
157        subject: String,
158        predicate: String,
159        object: String,
160    },
161    /// A triple was removed from the graph
162    Deletion {
163        subject: String,
164        predicate: String,
165        object: String,
166    },
167    /// A triple's object changed
168    Modification {
169        subject: String,
170        predicate: String,
171        old_object: String,
172        new_object: String,
173    },
174}
175
176impl DeltaType {
177    /// Create a delta from two quads
178    ///
179    /// Compares an old quad (from baseline graph) and a new quad (from current graph)
180    /// and returns the appropriate delta type, or `None` if there's no change.
181    ///
182    /// # Examples
183    ///
184    /// ```rust,no_run
185    /// use ggen_core::delta::DeltaType;
186    /// use oxigraph::model::{NamedNode, Literal, Quad, Subject, Term};
187    ///
188    /// # fn main() -> ggen_utils::error::Result<()> {
189    /// let old_quad = Quad::new(
190    ///     Subject::NamedNode(NamedNode::new("http://example.org/alice")?),
191    ///     NamedNode::new("http://example.org/name")?,
192    ///     Term::Literal(Literal::new_simple_literal("Alice")),
193    ///     None,
194    /// );
195    ///
196    /// let new_quad = Quad::new(
197    ///     Subject::NamedNode(NamedNode::new("http://example.org/alice")?),
198    ///     NamedNode::new("http://example.org/name")?,
199    ///     Term::Literal(Literal::new_simple_literal("Alice Smith")),
200    ///     None,
201    /// );
202    ///
203    /// // Detect modification
204    /// let delta = DeltaType::from_quads(Some(&old_quad), Some(&new_quad));
205    /// assert!(matches!(delta, Some(DeltaType::Modification { .. })));
206    /// # Ok(())
207    /// # }
208    /// ```
209    pub fn from_quads(old: Option<&Quad>, new: Option<&Quad>) -> Option<Self> {
210        match (old, new) {
211            (None, Some(new_quad)) => Some(DeltaType::Addition {
212                subject: new_quad.subject.to_string(),
213                predicate: new_quad.predicate.to_string(),
214                object: new_quad.object.to_string(),
215            }),
216            (Some(old_quad), None) => Some(DeltaType::Deletion {
217                subject: old_quad.subject.to_string(),
218                predicate: old_quad.predicate.to_string(),
219                object: old_quad.object.to_string(),
220            }),
221            (Some(old_quad), Some(new_quad)) => {
222                if old_quad.subject == new_quad.subject
223                    && old_quad.predicate == new_quad.predicate
224                    && old_quad.object != new_quad.object
225                {
226                    Some(DeltaType::Modification {
227                        subject: old_quad.subject.to_string(),
228                        predicate: old_quad.predicate.to_string(),
229                        old_object: old_quad.object.to_string(),
230                        new_object: new_quad.object.to_string(),
231                    })
232                } else {
233                    None
234                }
235            }
236            (None, None) => None,
237        }
238    }
239
240    /// Get all subjects affected by this delta
241    ///
242    /// Returns a vector of subject IRIs that are affected by this change.
243    ///
244    /// # Examples
245    ///
246    /// ```rust,no_run
247    /// use ggen_core::delta::DeltaType;
248    ///
249    /// # fn main() -> ggen_utils::error::Result<()> {
250    /// let delta = DeltaType::Addition {
251    ///     subject: "http://example.org/alice".to_string(),
252    ///     predicate: "http://example.org/name".to_string(),
253    ///     object: "Alice".to_string(),
254    /// };
255    ///
256    /// let subjects = delta.subjects();
257    /// assert_eq!(subjects, vec!["http://example.org/alice"]);
258    /// # Ok(())
259    /// # }
260    /// ```
261    pub fn subjects(&self) -> Vec<&str> {
262        match self {
263            DeltaType::Addition { subject, .. }
264            | DeltaType::Deletion { subject, .. }
265            | DeltaType::Modification { subject, .. } => vec![subject],
266        }
267    }
268
269    /// Get all predicates affected by this delta
270    pub fn predicates(&self) -> Vec<&str> {
271        match self {
272            DeltaType::Addition { predicate, .. }
273            | DeltaType::Deletion { predicate, .. }
274            | DeltaType::Modification { predicate, .. } => vec![predicate],
275        }
276    }
277
278    /// Check if this delta affects a specific IRI
279    pub fn affects_iri(&self, iri: &str) -> bool {
280        self.subjects().contains(&iri)
281            || self.predicates().contains(&iri)
282            || match self {
283                DeltaType::Addition { object, .. } | DeltaType::Deletion { object, .. } => {
284                    object == iri
285                }
286                DeltaType::Modification {
287                    old_object,
288                    new_object,
289                    ..
290                } => old_object == iri || new_object == iri,
291            }
292    }
293}
294
295impl fmt::Display for DeltaType {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        match self {
298            DeltaType::Addition {
299                subject,
300                predicate,
301                object,
302            } => write!(f, "+ {} {} {}", subject, predicate, object),
303            DeltaType::Deletion {
304                subject,
305                predicate,
306                object,
307            } => write!(f, "- {} {} {}", subject, predicate, object),
308            DeltaType::Modification {
309                subject,
310                predicate,
311                old_object,
312                new_object,
313            } => write!(
314                f,
315                "~ {} {} {} -> {}",
316                subject, predicate, old_object, new_object
317            ),
318        }
319    }
320}
321
322/// Collection of deltas representing the difference between two graphs
323///
324/// A `GraphDelta` contains all changes detected between a baseline graph and
325/// a current graph, along with metadata for tracking and verification.
326///
327/// # Examples
328///
329/// ```rust,no_run
330/// use ggen_core::delta::GraphDelta;
331/// use ggen_core::graph::Graph;
332///
333/// # fn main() -> ggen_utils::error::Result<()> {
334/// let baseline = Graph::new()?;
335/// baseline.insert_turtle(r#"
336///     @prefix ex: <http://example.org/> .
337///     ex:alice a ex:Person .
338/// "#)?;
339///
340/// let current = Graph::new()?;
341/// current.insert_turtle(r#"
342///     @prefix ex: <http://example.org/> .
343///     ex:alice a ex:Person .
344///     ex:bob a ex:Person .
345/// "#)?;
346///
347/// let delta = GraphDelta::new(&baseline, &current)?;
348/// println!("Detected {} changes", delta.deltas.len());
349/// # Ok(())
350/// # }
351/// ```
352#[derive(Debug, Clone, Default, Serialize, Deserialize)]
353pub struct GraphDelta {
354    /// All detected changes
355    pub deltas: Vec<DeltaType>,
356    /// Hash of the baseline graph
357    pub baseline_hash: Option<String>,
358    /// Hash of the current graph
359    pub current_hash: Option<String>,
360    /// Timestamp when delta was computed
361    pub computed_at: chrono::DateTime<chrono::Utc>,
362}
363
364impl GraphDelta {
365    /// Create a new delta by comparing two graphs
366    ///
367    /// Computes all differences between the baseline and current graphs,
368    /// including additions, deletions, and modifications.
369    ///
370    /// # Examples
371    ///
372    /// ```rust,no_run
373    /// use ggen_core::delta::GraphDelta;
374    /// use ggen_core::graph::Graph;
375    ///
376    /// # fn main() -> ggen_utils::error::Result<()> {
377    /// let baseline = Graph::new()?;
378    /// let current = Graph::new()?;
379    /// current.insert_turtle(r#"
380    ///     @prefix ex: <http://example.org/> .
381    ///     ex:alice ex:name "Alice" .
382    /// "#)?;
383    ///
384    /// let delta = GraphDelta::new(&baseline, &current)?;
385    /// assert!(!delta.deltas.is_empty());
386    /// # Ok(())
387    /// # }
388    /// ```
389    pub fn new(baseline: &Graph, current: &Graph) -> Result<Self> {
390        let mut deltas = Vec::new();
391
392        // Get all quads from both graphs
393        let baseline_quads = baseline.get_all_quads()?;
394        let current_quads = current.get_all_quads()?;
395
396        // Create lookup maps for efficient comparison
397        let baseline_map: BTreeMap<(String, String, String), Quad> = baseline_quads
398            .iter()
399            .map(|q| {
400                (
401                    (
402                        q.subject.to_string(),
403                        q.predicate.to_string(),
404                        q.object.to_string(),
405                    ),
406                    q.clone(),
407                )
408            })
409            .collect();
410
411        let current_map: BTreeMap<(String, String, String), Quad> = current_quads
412            .iter()
413            .map(|q| {
414                (
415                    (
416                        q.subject.to_string(),
417                        q.predicate.to_string(),
418                        q.object.to_string(),
419                    ),
420                    q.clone(),
421                )
422            })
423            .collect();
424
425        // Find additions and modifications
426        for ((s, p, o), current_quad) in &current_map {
427            match baseline_map.get(&(s.clone(), p.clone(), o.clone())) {
428                Some(baseline_quad) => {
429                    // Check if objects are different (modification)
430                    if baseline_quad.object != current_quad.object {
431                        deltas.push(DeltaType::Modification {
432                            subject: s.clone(),
433                            predicate: p.clone(),
434                            old_object: baseline_quad.object.to_string(),
435                            new_object: current_quad.object.to_string(),
436                        });
437                    }
438                    // If subjects/predicates match, it's not an addition
439                }
440                None => {
441                    // This is an addition
442                    deltas.push(DeltaType::Addition {
443                        subject: s.clone(),
444                        predicate: p.clone(),
445                        object: o.clone(),
446                    });
447                }
448            }
449        }
450
451        // Find deletions
452        for (s, p, o) in baseline_map.keys() {
453            if !current_map.contains_key(&(s.to_string(), p.clone(), o.clone())) {
454                deltas.push(DeltaType::Deletion {
455                    subject: s.clone(),
456                    predicate: p.clone(),
457                    object: o.clone(),
458                });
459            }
460        }
461
462        Ok(Self {
463            deltas,
464            baseline_hash: baseline.compute_hash().ok(),
465            current_hash: current.compute_hash().ok(),
466            computed_at: chrono::Utc::now(),
467        })
468    }
469
470    /// Get all IRIs affected by this delta
471    ///
472    /// Returns a set of all unique IRIs (subjects, predicates, objects) that
473    /// appear in any of the changes.
474    ///
475    /// # Examples
476    ///
477    /// ```rust,no_run
478    /// use ggen_core::delta::GraphDelta;
479    /// use ggen_core::graph::Graph;
480    ///
481    /// # fn main() -> ggen_utils::error::Result<()> {
482    /// let baseline = Graph::new()?;
483    /// let current = Graph::new()?;
484    /// current.insert_turtle(r#"
485    ///     @prefix ex: <http://example.org/> .
486    ///     ex:alice ex:name "Alice" .
487    /// "#)?;
488    ///
489    /// let delta = GraphDelta::new(&baseline, &current)?;
490    /// let affected = delta.affected_iris();
491    /// assert!(affected.contains("http://example.org/alice"));
492    /// # Ok(())
493    /// # }
494    /// ```
495    pub fn affected_iris(&self) -> BTreeSet<String> {
496        let mut iris = BTreeSet::new();
497        for delta in &self.deltas {
498            iris.extend(delta.subjects().iter().map(|s| s.to_string()));
499            iris.extend(delta.predicates().iter().map(|p| p.to_string()));
500            match delta {
501                DeltaType::Addition { object, .. } | DeltaType::Deletion { object, .. } => {
502                    iris.insert(object.clone());
503                }
504                DeltaType::Modification {
505                    old_object,
506                    new_object,
507                    ..
508                } => {
509                    iris.insert(old_object.clone());
510                    iris.insert(new_object.clone());
511                }
512            }
513        }
514        iris
515    }
516
517    /// Check if this delta affects a specific IRI
518    ///
519    /// Returns `true` if any change in this delta affects the given IRI
520    /// (as subject, predicate, or object).
521    ///
522    /// # Examples
523    ///
524    /// ```rust,no_run
525    /// use ggen_core::delta::GraphDelta;
526    /// use ggen_core::graph::Graph;
527    ///
528    /// # fn main() -> ggen_utils::error::Result<()> {
529    /// let baseline = Graph::new()?;
530    /// let current = Graph::new()?;
531    /// current.insert_turtle(r#"
532    ///     @prefix ex: <http://example.org/> .
533    ///     ex:alice ex:name "Alice" .
534    /// "#)?;
535    ///
536    /// let delta = GraphDelta::new(&baseline, &current)?;
537    /// assert!(delta.affects_iri("http://example.org/alice"));
538    /// # Ok(())
539    /// # }
540    /// ```
541    pub fn affects_iri(&self, iri: &str) -> bool {
542        self.deltas.iter().any(|d| d.affects_iri(iri))
543    }
544
545    /// Check if this delta is empty (no changes)
546    pub fn is_empty(&self) -> bool {
547        self.deltas.is_empty()
548    }
549
550    /// Get the count of each delta type
551    pub fn counts(&self) -> BTreeMap<&str, usize> {
552        let mut counts = BTreeMap::new();
553        for delta in &self.deltas {
554            let key = match delta {
555                DeltaType::Addition { .. } => "additions",
556                DeltaType::Deletion { .. } => "deletions",
557                DeltaType::Modification { .. } => "modifications",
558            };
559            *counts.entry(key).or_insert(0) += 1;
560        }
561        counts
562    }
563
564    /// Filter deltas to only those affecting specific IRIs
565    pub fn filter_by_iris(&self, iris: &[String]) -> Self {
566        let filtered_deltas: Vec<_> = self
567            .deltas
568            .iter()
569            .filter(|d| iris.iter().any(|iri| d.affects_iri(iri)))
570            .cloned()
571            .collect();
572
573        Self {
574            deltas: filtered_deltas,
575            baseline_hash: self.baseline_hash.clone(),
576            current_hash: self.current_hash.clone(),
577            computed_at: self.computed_at,
578        }
579    }
580
581    /// Merge another delta into this one
582    pub fn merge(&mut self, other: GraphDelta) {
583        self.deltas.extend(other.deltas);
584        if self.baseline_hash.is_none() {
585            self.baseline_hash = other.baseline_hash;
586        }
587        if self.current_hash.is_none() {
588            self.current_hash = other.current_hash;
589        }
590    }
591}
592
593impl fmt::Display for GraphDelta {
594    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
595        writeln!(f, "GraphDelta ({} changes):", self.deltas.len())?;
596
597        let counts = self.counts();
598        for (delta_type, count) in counts {
599            writeln!(f, "  {}: {}", delta_type, count)?;
600        }
601
602        if !self.deltas.is_empty() {
603            /// Maximum number of deltas to display before truncating
604            const MAX_DELTAS_DISPLAY: usize = 10;
605
606            writeln!(f)?;
607            for delta in self.deltas.iter().take(MAX_DELTAS_DISPLAY) {
608                writeln!(f, "  {}", delta)?;
609            }
610
611            if self.deltas.len() > MAX_DELTAS_DISPLAY {
612                writeln!(
613                    f,
614                    "  ... and {} more",
615                    self.deltas.len() - MAX_DELTAS_DISPLAY
616                )?;
617            }
618        }
619
620        Ok(())
621    }
622}
623
624/// Template impact analysis for delta-driven regeneration
625#[derive(Debug, Clone, Serialize, Deserialize)]
626pub struct TemplateImpact {
627    /// Path to the template
628    pub template_path: String,
629    /// IRIs from the delta that affect this template
630    pub affected_iris: Vec<String>,
631    /// Confidence score (0.0-1.0) of how likely this template is affected
632    pub confidence: f64,
633    /// Reason for the impact assessment
634    pub reason: String,
635}
636
637impl TemplateImpact {
638    /// Create a new template impact analysis
639    pub fn new(
640        template_path: String, affected_iris: Vec<String>, confidence: f64, reason: String,
641    ) -> Self {
642        Self {
643            template_path,
644            affected_iris,
645            confidence,
646            reason,
647        }
648    }
649
650    /// Check if this impact is above a confidence threshold
651    pub fn is_confident(&self, threshold: f64) -> bool {
652        self.confidence >= threshold
653    }
654}
655
656/// Analyze which templates are affected by a graph delta
657pub struct ImpactAnalyzer {
658    /// Cache of template query patterns for performance
659    #[allow(dead_code)]
660    template_queries: BTreeMap<String, Vec<String>>,
661}
662
663impl Default for ImpactAnalyzer {
664    fn default() -> Self {
665        Self::new()
666    }
667}
668
669impl ImpactAnalyzer {
670    /// Create a new impact analyzer
671    pub fn new() -> Self {
672        Self {
673            template_queries: BTreeMap::new(),
674        }
675    }
676
677    /// Analyze template impacts for a given delta
678    pub fn analyze_impacts(
679        &mut self, delta: &GraphDelta, template_paths: &[String], graph: &Graph,
680    ) -> Result<Vec<TemplateImpact>> {
681        let mut impacts = Vec::new();
682
683        for template_path in template_paths {
684            // Get or cache template queries
685            let queries = self.get_template_queries(template_path, graph)?;
686
687            // Analyze impact based on query patterns
688            let (confidence, reason) = self.assess_impact(delta, &queries);
689
690            if confidence > 0.0 {
691                impacts.push(TemplateImpact::new(
692                    template_path.clone(),
693                    delta.affected_iris().into_iter().collect(),
694                    confidence,
695                    reason,
696                ));
697            }
698        }
699
700        // Sort by confidence (highest first)
701        impacts.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
702
703        Ok(impacts)
704    }
705
706    /// Get SPARQL queries from a template (simplified - in reality would parse template)
707    fn get_template_queries(&mut self, template_path: &str, _graph: &Graph) -> Result<Vec<String>> {
708        // Check if we have cached queries for this template
709        if let Some(queries) = self.template_queries.get(template_path) {
710            return Ok(queries.clone());
711        }
712
713        // This is a simplified implementation
714        // In practice, would need to parse template frontmatter and extract SPARQL queries
715        Ok(vec![
716            "SELECT ?s ?p ?o WHERE { ?s ?p ?o }".to_string(),
717            "SELECT ?class WHERE { ?class a rdfs:Class }".to_string(),
718        ])
719    }
720
721    /// Assess how a delta impacts a set of queries
722    fn assess_impact(&self, delta: &GraphDelta, queries: &[String]) -> (f64, String) {
723        let affected_iris = delta.affected_iris();
724
725        // Simple heuristic: check if any affected IRI appears in any query
726        let mut max_relevance = 0.0;
727        let mut reasons = Vec::new();
728
729        for query in queries {
730            let query_lower = query.to_lowercase();
731
732            for iri in &affected_iris {
733                if query_lower.contains(&iri.to_lowercase()) {
734                    max_relevance = 1.0;
735                    reasons.push(format!("Query directly references IRI: {}", iri));
736                    break;
737                }
738            }
739        }
740
741        // If no direct matches, use pattern-based heuristics
742        if max_relevance == 0.0 {
743            // Check for schema changes (rdfs:Class, rdf:Property, etc.)
744            for iri in &affected_iris {
745                if iri.contains("rdfs:Class") || iri.contains("rdf:Property") {
746                    max_relevance = 0.8;
747                    reasons.push("Schema element changed".to_string());
748                }
749            }
750        }
751
752        let reason = if reasons.is_empty() {
753            "No direct impact detected".to_string()
754        } else {
755            reasons.join("; ")
756        };
757
758        (max_relevance, reason)
759    }
760}
761
762impl Graph {
763    /// Get all quads in the graph for delta computation
764    fn get_all_quads(&self) -> Result<Vec<Quad>> {
765        let pattern = self.quads_for_pattern(None, None, None, None)?;
766        Ok(pattern)
767    }
768
769    /// Compute a deterministic hash of the graph content
770    pub fn compute_hash(&self) -> Result<String> {
771        let quads = self.get_all_quads()?;
772        let mut hasher = AHasher::default();
773
774        // Create a deterministic string representation
775        let mut sorted_quads: Vec<String> = quads
776            .iter()
777            .map(|q| format!("{} {} {}", q.subject, q.predicate, q.object))
778            .collect();
779        sorted_quads.sort();
780
781        for quad_str in sorted_quads {
782            quad_str.hash(&mut hasher);
783        }
784
785        Ok(format!("{:x}", hasher.finish()))
786    }
787}
788
789#[cfg(test)]
790mod tests {
791    use super::*;
792    use crate::graph::Graph;
793
794    fn create_test_graph() -> Result<(Graph, Graph)> {
795        let baseline = Graph::new()?;
796        baseline.insert_turtle(
797            r#"
798            @prefix : <http://example.org/> .
799            @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
800            @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
801            :User a rdfs:Class .
802            :name a rdf:Property ;
803                  rdfs:domain :User .
804        "#,
805        )?;
806
807        let current = Graph::new()?;
808        current.insert_turtle(
809            r#"
810            @prefix : <http://example.org/> .
811            @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
812            @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
813            :User a rdfs:Class .
814            :name a rdf:Property ;
815                  rdfs:domain :User .
816            :email a rdf:Property ;
817                    rdfs:domain :User .
818        "#,
819        )?;
820
821        Ok((baseline, current))
822    }
823
824    #[test]
825    fn test_delta_creation() {
826        let (baseline, current) = create_test_graph().unwrap();
827        let delta = GraphDelta::new(&baseline, &current).unwrap();
828
829        assert!(!delta.is_empty());
830        assert!(delta.affects_iri("<http://example.org/email>"));
831
832        // Should have two additions (the email property has 2 triples: type and domain)
833        let counts = delta.counts();
834        assert_eq!(counts.get("additions"), Some(&2));
835        assert_eq!(counts.get("deletions"), None); // No deletions
836        assert_eq!(counts.get("modifications"), None); // No modifications
837    }
838
839    #[test]
840    fn test_delta_affected_iris() {
841        let (baseline, current) = create_test_graph().unwrap();
842        let delta = GraphDelta::new(&baseline, &current).unwrap();
843
844        let affected = delta.affected_iris();
845        assert!(affected.contains("<http://example.org/email>"));
846        assert!(affected.contains("<http://example.org/User>"));
847        assert!(affected.contains("<http://www.w3.org/2000/01/rdf-schema#domain>"));
848    }
849
850    #[test]
851    fn test_delta_filtering() -> std::result::Result<(), Box<dyn std::error::Error>> {
852        let (baseline, current) = create_test_graph()?;
853        let delta = GraphDelta::new(&baseline, &current)?;
854
855        // Filter to only User-related changes
856        let filtered = delta.filter_by_iris(&["<http://example.org/User>".to_string()]);
857
858        // Should still contain the email addition since it affects User
859        assert!(!filtered.is_empty());
860
861        Ok(())
862    }
863
864    #[test]
865    fn test_impact_analyzer() -> std::result::Result<(), Box<dyn std::error::Error>> {
866        let (baseline, current) = create_test_graph().unwrap();
867        let delta = GraphDelta::new(&baseline, &current).unwrap();
868
869        let mut analyzer = ImpactAnalyzer::new();
870        // Add a mock query that should match the email property
871        analyzer.template_queries.insert(
872            "template1.tmpl".to_string(),
873            vec!["SELECT * WHERE { ?s <http://example.org/email> ?o }".to_string()],
874        );
875
876        let template_paths = vec!["template1.tmpl".to_string()];
877        let impacts = analyzer
878            .analyze_impacts(&delta, &template_paths, &baseline)
879            .unwrap();
880
881        // Should find some impacts since template queries match affected IRIs
882        assert!(!impacts.is_empty());
883
884        Ok(())
885    }
886
887    #[test]
888    fn test_graph_hash() -> std::result::Result<(), Box<dyn std::error::Error>> {
889        let (baseline, current) = create_test_graph().unwrap();
890
891        let hash1 = baseline.compute_hash().unwrap();
892        let hash2 = current.compute_hash().unwrap();
893
894        // Different graphs should have different hashes
895        assert_ne!(hash1, hash2);
896
897        // Same graph should have same hash
898        let hash3 = baseline.compute_hash().unwrap();
899        assert_eq!(hash1, hash3);
900
901        Ok(())
902    }
903
904    #[test]
905    fn test_delta_display() -> std::result::Result<(), Box<dyn std::error::Error>> {
906        let (baseline, current) = create_test_graph().unwrap();
907        let delta = GraphDelta::new(&baseline, &current).unwrap();
908
909        let display = format!("{}", delta);
910        assert!(display.contains("GraphDelta"));
911        assert!(display.contains("additions"));
912        assert!(display.contains("http://example.org/email"));
913
914        Ok(())
915    }
916}