1use crate::validate::{NonStratifiable, Violation, validate_with_context};
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(
53 data: &Graph,
54 context: &Graph,
55 schema: &Schema,
56 delta: &GraphDelta,
57) -> Result<RepairOutcome, NonStratifiable> {
58 let baseline = validate_with_context(data, context, schema)?.violations;
59 let patched_data = apply(data, delta);
60 let patched_context = apply(context, delta);
61 let patched = validate_with_context(&patched_data, &patched_context, schema)?.violations;
62 Ok(diff(baseline, patched))
63}
64
65pub fn apply(data: &Graph, delta: &GraphDelta) -> Graph {
67 let mut g = data.clone();
68 for t in &delta.delete {
69 g.remove(t);
70 }
71 for t in &delta.add {
72 g.insert(t);
73 }
74 g
75}
76
77fn key(v: &Violation) -> (Term, usize) {
80 (v.focus.clone(), v.statement)
81}
82
83fn diff(baseline: Vec<Violation>, patched: Vec<Violation>) -> RepairOutcome {
84 let baseline_keys: HashSet<(Term, usize)> = baseline.iter().map(key).collect();
85 let patched_keys: HashSet<(Term, usize)> = patched.iter().map(key).collect();
86
87 let fixed = baseline
88 .into_iter()
89 .filter(|v| !patched_keys.contains(&key(v)))
90 .collect();
91
92 let mut introduced = Vec::new();
93 let mut remaining = Vec::new();
94 for v in patched {
95 if baseline_keys.contains(&key(&v)) {
96 remaining.push(v);
97 } else {
98 introduced.push(v);
99 }
100 }
101 RepairOutcome {
102 fixed,
103 introduced,
104 remaining,
105 }
106}
107
108pub fn outcome_index(outcome: &RepairOutcome) -> HashMap<(Term, usize), &'static str> {
110 let mut m = HashMap::new();
111 for v in &outcome.fixed {
112 m.insert(key(v), "fixed");
113 }
114 for v in &outcome.remaining {
115 m.insert(key(v), "remaining");
116 }
117 for v in &outcome.introduced {
118 m.insert(key(v), "introduced");
119 }
120 m
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use oxrdf::{NamedNode, Triple};
127 use shifty_parse::{load_turtle, parse_turtle};
128
129 const PREFIXES: &str = r#"
130 @prefix sh: <http://www.w3.org/ns/shacl#> .
131 @prefix ex: <http://ex/> .
132 "#;
133
134 fn schema_and_graph(ttl: &str) -> (Schema, Graph) {
135 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
136 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
137 (parsed.schema, loaded.graph)
138 }
139
140 fn t(s: &str, p: &str, o: &str) -> Triple {
141 Triple::new(
142 NamedNode::new(s).unwrap(),
143 NamedNode::new(p).unwrap(),
144 NamedNode::new(o).unwrap(),
145 )
146 }
147
148 #[test]
149 fn sound_repair_fixes_one_and_introduces_none() {
150 let (schema, graph) = schema_and_graph(&format!(
152 "{PREFIXES}
153 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
154 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
155 "
156 ));
157 let delta = GraphDelta {
158 add: vec![t("http://ex/x", "http://ex/p", "http://ex/y")],
159 delete: vec![],
160 };
161 let outcome = gate(&graph, &graph, &schema, &delta).unwrap();
162 assert!(outcome.is_sound());
163 assert!(outcome.is_progress());
164 assert_eq!(outcome.fixed.len(), 1);
165 assert!(outcome.remaining.is_empty());
166 }
167
168 #[test]
169 fn collateral_delete_is_caught_as_introduced() {
170 let (schema, graph) = schema_and_graph(&format!(
172 "{PREFIXES}
173 ex:S a sh:NodeShape ; sh:targetNode ex:x, ex:y ;
174 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
175 ex:x ex:p ex:a .
176 ex:y ex:p ex:b .
177 "
178 ));
179 let delta = GraphDelta {
181 add: vec![],
182 delete: vec![t("http://ex/y", "http://ex/p", "http://ex/b")],
183 };
184 let outcome = gate(&graph, &graph, &schema, &delta).unwrap();
185 assert!(!outcome.is_sound(), "introduces a violation at ex:y");
186 assert_eq!(outcome.introduced.len(), 1);
187 assert_eq!(outcome.introduced[0].focus.to_string(), "<http://ex/y>");
188 assert!(outcome.fixed.is_empty());
189 }
190
191 #[test]
192 fn noop_delta_over_conforming_graph_is_empty() {
193 let (schema, graph) = schema_and_graph(&format!(
194 "{PREFIXES}
195 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
196 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
197 ex:x ex:p ex:y .
198 "
199 ));
200 let outcome = gate(&graph, &graph, &schema, &GraphDelta::default()).unwrap();
201 assert_eq!(outcome, RepairOutcome::default());
202 }
203
204 #[test]
205 fn end_to_end_synthesized_repair_passes_the_gate() {
206 use crate::synthesize::synthesize;
207 use crate::witness::witness_violations;
208 use shifty_repair::{Plan, instantiate};
209
210 let ttl = format!(
211 "{PREFIXES}
212 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
213 sh:property [ sh:path ex:p ; sh:datatype <http://www.w3.org/2001/XMLSchema#integer> ] .
214 ex:x ex:p \"hello\" .
215 "
216 );
217 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
218 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
219
220 let ws = witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).unwrap();
221 let tree = synthesize(&parsed.schema.arena, &ws[0]);
222
223 let mut plan = Plan::default();
225 let hole = instantiate(&tree, &plan).open_holes[0].0;
226 plan.binding.insert(
227 hole,
228 oxrdf::Literal::new_typed_literal("7", oxrdf::vocab::xsd::INTEGER).into(),
229 );
230 let delta = instantiate(&tree, &plan).delta;
231
232 let outcome = gate(&loaded.graph, &loaded.graph, &parsed.schema, &delta).unwrap();
233 assert!(outcome.is_progress(), "{outcome:?}");
234 assert!(outcome.introduced.is_empty());
235 }
236
237 #[test]
238 fn context_gate_discharges_subclass_from_shapes_hierarchy() {
239 use crate::validate::graph_union;
240
241 let shapes_ttl = r#"
244 @prefix sh: <http://www.w3.org/ns/shacl#> .
245 @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
246 @prefix ex: <http://ex/> .
247 ex:Sub rdfs:subClassOf ex:Super .
248 ex:S a sh:NodeShape ; sh:targetClass ex:Vav ;
249 sh:property [ sh:path ex:hasPoint ; sh:minCount 1 ; sh:class ex:Super ] .
250 "#;
251 let data_ttl = "@prefix ex: <http://ex/> . ex:vav1 a ex:Vav .";
252
253 let schema = parse_turtle(shapes_ttl.as_bytes(), None).unwrap().schema;
254 let shapes = load_turtle(shapes_ttl.as_bytes(), None).unwrap().graph;
255 let data = load_turtle(data_ttl.as_bytes(), None).unwrap().graph;
256 let context = graph_union(&data, &shapes);
257
258 let rdf_type = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
260 let delta = GraphDelta {
261 add: vec![
262 t("http://ex/n1", rdf_type, "http://ex/Sub"),
263 t("http://ex/vav1", "http://ex/hasPoint", "http://ex/n1"),
264 ],
265 delete: vec![],
266 };
267
268 let data_only = gate(&data, &data, &schema, &delta).unwrap();
271 assert!(!data_only.is_progress(), "{data_only:?}");
272
273 let with_ctx = gate(&data, &context, &schema, &delta).unwrap();
276 assert!(with_ctx.is_progress(), "{with_ctx:?}");
277 assert!(with_ctx.introduced.is_empty(), "{with_ctx:?}");
278 }
279}