1use crate::validate::{NonStratifiable, Violation, validate};
14use oxrdf::{Graph, Term};
15use serde::{Deserialize, Serialize};
16use shifty_algebra::Schema;
17use shifty_repair::GraphDelta;
18use std::collections::{HashMap, HashSet};
19
20#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
22pub struct RepairOutcome {
23 pub fixed: Vec<Violation>,
25 pub introduced: Vec<Violation>,
27 pub remaining: Vec<Violation>,
29}
30
31impl RepairOutcome {
32 pub fn is_sound(&self) -> bool {
34 self.introduced.is_empty()
35 }
36
37 pub fn is_progress(&self) -> bool {
39 self.is_sound() && !self.fixed.is_empty()
40 }
41}
42
43pub fn gate(
46 data: &Graph,
47 schema: &Schema,
48 delta: &GraphDelta,
49) -> Result<RepairOutcome, NonStratifiable> {
50 let baseline = validate(data, schema)?.violations;
51 let patched_graph = apply(data, delta);
52 let patched = validate(&patched_graph, schema)?.violations;
53 Ok(diff(baseline, patched))
54}
55
56pub fn apply(data: &Graph, delta: &GraphDelta) -> Graph {
58 let mut g = data.clone();
59 for t in &delta.delete {
60 g.remove(t);
61 }
62 for t in &delta.add {
63 g.insert(t);
64 }
65 g
66}
67
68fn key(v: &Violation) -> (Term, usize) {
71 (v.focus.clone(), v.statement)
72}
73
74fn diff(baseline: Vec<Violation>, patched: Vec<Violation>) -> RepairOutcome {
75 let baseline_keys: HashSet<(Term, usize)> = baseline.iter().map(key).collect();
76 let patched_keys: HashSet<(Term, usize)> = patched.iter().map(key).collect();
77
78 let fixed = baseline
79 .into_iter()
80 .filter(|v| !patched_keys.contains(&key(v)))
81 .collect();
82
83 let mut introduced = Vec::new();
84 let mut remaining = Vec::new();
85 for v in patched {
86 if baseline_keys.contains(&key(&v)) {
87 remaining.push(v);
88 } else {
89 introduced.push(v);
90 }
91 }
92 RepairOutcome {
93 fixed,
94 introduced,
95 remaining,
96 }
97}
98
99pub fn outcome_index(outcome: &RepairOutcome) -> HashMap<(Term, usize), &'static str> {
101 let mut m = HashMap::new();
102 for v in &outcome.fixed {
103 m.insert(key(v), "fixed");
104 }
105 for v in &outcome.remaining {
106 m.insert(key(v), "remaining");
107 }
108 for v in &outcome.introduced {
109 m.insert(key(v), "introduced");
110 }
111 m
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use oxrdf::{NamedNode, Triple};
118 use shifty_parse::{load_turtle, parse_turtle};
119
120 const PREFIXES: &str = r#"
121 @prefix sh: <http://www.w3.org/ns/shacl#> .
122 @prefix ex: <http://ex/> .
123 "#;
124
125 fn schema_and_graph(ttl: &str) -> (Schema, Graph) {
126 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
127 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
128 (parsed.schema, loaded.graph)
129 }
130
131 fn t(s: &str, p: &str, o: &str) -> Triple {
132 Triple::new(
133 NamedNode::new(s).unwrap(),
134 NamedNode::new(p).unwrap(),
135 NamedNode::new(o).unwrap(),
136 )
137 }
138
139 #[test]
140 fn sound_repair_fixes_one_and_introduces_none() {
141 let (schema, graph) = schema_and_graph(&format!(
143 "{PREFIXES}
144 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
145 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
146 "
147 ));
148 let delta = GraphDelta {
149 add: vec![t("http://ex/x", "http://ex/p", "http://ex/y")],
150 delete: vec![],
151 };
152 let outcome = gate(&graph, &schema, &delta).unwrap();
153 assert!(outcome.is_sound());
154 assert!(outcome.is_progress());
155 assert_eq!(outcome.fixed.len(), 1);
156 assert!(outcome.remaining.is_empty());
157 }
158
159 #[test]
160 fn collateral_delete_is_caught_as_introduced() {
161 let (schema, graph) = schema_and_graph(&format!(
163 "{PREFIXES}
164 ex:S a sh:NodeShape ; sh:targetNode ex:x, ex:y ;
165 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
166 ex:x ex:p ex:a .
167 ex:y ex:p ex:b .
168 "
169 ));
170 let delta = GraphDelta {
172 add: vec![],
173 delete: vec![t("http://ex/y", "http://ex/p", "http://ex/b")],
174 };
175 let outcome = gate(&graph, &schema, &delta).unwrap();
176 assert!(!outcome.is_sound(), "introduces a violation at ex:y");
177 assert_eq!(outcome.introduced.len(), 1);
178 assert_eq!(outcome.introduced[0].focus.to_string(), "<http://ex/y>");
179 assert!(outcome.fixed.is_empty());
180 }
181
182 #[test]
183 fn noop_delta_over_conforming_graph_is_empty() {
184 let (schema, graph) = schema_and_graph(&format!(
185 "{PREFIXES}
186 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
187 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
188 ex:x ex:p ex:y .
189 "
190 ));
191 let outcome = gate(&graph, &schema, &GraphDelta::default()).unwrap();
192 assert_eq!(outcome, RepairOutcome::default());
193 }
194
195 #[test]
196 fn end_to_end_synthesized_repair_passes_the_gate() {
197 use crate::synthesize::synthesize;
198 use crate::witness::witness_violations;
199 use shifty_repair::{Plan, instantiate};
200
201 let ttl = format!(
202 "{PREFIXES}
203 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
204 sh:property [ sh:path ex:p ; sh:datatype <http://www.w3.org/2001/XMLSchema#integer> ] .
205 ex:x ex:p \"hello\" .
206 "
207 );
208 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
209 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
210
211 let ws = witness_violations(&loaded.graph, &parsed.schema).unwrap();
212 let tree = synthesize(&parsed.schema.arena, &ws[0]);
213
214 let mut plan = Plan::default();
216 let hole = instantiate(&tree, &plan).open_holes[0].0;
217 plan.binding.insert(
218 hole,
219 oxrdf::Literal::new_typed_literal("7", oxrdf::vocab::xsd::INTEGER).into(),
220 );
221 let delta = instantiate(&tree, &plan).delta;
222
223 let outcome = gate(&loaded.graph, &parsed.schema, &delta).unwrap();
224 assert!(outcome.is_progress(), "{outcome:?}");
225 assert!(outcome.introduced.is_empty());
226 }
227}