1use crate::witness::{
15 BlockReason as WBlock, EvaluationStatus, EvidenceKind, EvidenceNodeRef, FocusWitness,
16 PathSupport, RelKind, SatTrace, Witness,
17};
18use oxrdf::{NamedOrBlankNode, Term, Triple};
19use serde::{Deserialize, Serialize};
20use shifty_algebra::{Path, Shape, ShapeArena, ShapeId};
21use shifty_repair::{
22 BlockReason, Edit, Hole, HoleConstraint, NodeId, RepairTree, Slot, TriplePattern,
23};
24use std::collections::{BTreeMap, BTreeSet};
25
26type Materialized = (Vec<Edit>, Vec<(Hole, HoleConstraint)>);
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct EvidenceOrigin {
33 pub statement: Option<usize>,
36 pub path: Vec<usize>,
37 pub constraint_id: ShapeId,
38 pub node: Option<Term>,
39 pub status: EvaluationStatus,
40 pub kind: EvidenceKind,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct SynthesizedRepair {
51 pub tree: RepairTree,
52 pub origins: BTreeMap<NodeId, Vec<EvidenceOrigin>>,
53}
54
55impl SynthesizedRepair {
56 pub fn origins_for(&self, node: NodeId) -> &[EvidenceOrigin] {
57 self.origins.get(&node).map(Vec::as_slice).unwrap_or(&[])
58 }
59
60 pub fn into_tree(self) -> RepairTree {
61 self.tree
62 }
63}
64
65pub fn synthesize(arena: &ShapeArena, w: &FocusWitness) -> RepairTree {
67 let mut s = Synth::new(arena, false);
68 s.repair_witness(w)
69}
70
71pub fn synthesize_with_origins(arena: &ShapeArena, w: &FocusWitness) -> SynthesizedRepair {
75 let mut s = Synth::new(arena, true);
76 let tree = s.repair_witness(w);
77 s.finish(tree)
78}
79
80pub fn synthesize_focus(arena: &ShapeArena, ws: &[FocusWitness]) -> RepairTree {
83 let mut s = Synth::new(arena, false);
84 let children: Vec<RepairTree> = ws.iter().map(|w| s.repair_witness(w)).collect();
85 s.all(children)
86}
87
88pub fn synthesize_focus_with_origins(arena: &ShapeArena, ws: &[FocusWitness]) -> SynthesizedRepair {
91 let mut s = Synth::new(arena, true);
92 let children: Vec<RepairTree> = ws.iter().map(|w| s.repair_witness(w)).collect();
93 let roots = ws
94 .iter()
95 .map(|w| {
96 evidence_origin(
97 (w.statement != usize::MAX).then_some(w.statement),
98 &[],
99 EvidenceNodeRef::Failure(&w.failure),
100 )
101 })
102 .collect();
103 let tree = s.with_origins(roots, |s| s.all(children));
104 s.finish(tree)
105}
106
107struct Synth<'a> {
108 arena: &'a ShapeArena,
109 next_node: u32,
110 next_hole: u32,
111 statement: Option<Option<usize>>,
114 evidence_path: Vec<usize>,
115 current_origins: Vec<EvidenceOrigin>,
116 origins: BTreeMap<NodeId, Vec<EvidenceOrigin>>,
117 track_origins: bool,
118}
119
120impl<'a> Synth<'a> {
121 fn new(arena: &'a ShapeArena, track_origins: bool) -> Self {
122 Self {
123 arena,
124 next_node: 0,
125 next_hole: 0,
126 statement: None,
127 evidence_path: Vec::new(),
128 current_origins: Vec::new(),
129 origins: BTreeMap::new(),
130 track_origins,
131 }
132 }
133
134 fn node(&mut self) -> NodeId {
135 let n = NodeId(self.next_node);
136 self.next_node += 1;
137 if !self.current_origins.is_empty() {
138 self.origins.insert(n, self.current_origins.clone());
139 }
140 n
141 }
142
143 fn finish(mut self, tree: RepairTree) -> SynthesizedRepair {
144 let mut retained = BTreeSet::new();
145 collect_tree_nodes(&tree, &mut retained);
146 self.origins.retain(|node, _| retained.contains(node));
147 SynthesizedRepair {
148 tree,
149 origins: self.origins,
150 }
151 }
152
153 fn repair_witness(&mut self, witness: &FocusWitness) -> RepairTree {
154 let statement = (witness.statement != usize::MAX).then_some(witness.statement);
155 let previous_statement = self.statement.replace(statement);
156 let previous_path = std::mem::take(&mut self.evidence_path);
157 let tree = self.repair(&witness.failure);
158 self.evidence_path = previous_path;
159 self.statement = previous_statement;
160 tree
161 }
162
163 fn with_origins<T>(
164 &mut self,
165 origins: Vec<EvidenceOrigin>,
166 f: impl FnOnce(&mut Self) -> T,
167 ) -> T {
168 let previous = std::mem::replace(&mut self.current_origins, origins);
169 let value = f(self);
170 self.current_origins = previous;
171 value
172 }
173
174 fn with_evidence<T>(
175 &mut self,
176 evidence: EvidenceNodeRef<'_>,
177 f: impl FnOnce(&mut Self) -> T,
178 ) -> T {
179 if !self.track_origins {
180 return f(self);
181 }
182 let statement = self
183 .statement
184 .expect("evidence synthesis must be rooted in a FocusWitness");
185 let origin = evidence_origin(statement, &self.evidence_path, evidence);
186 self.with_origins(vec![origin], f)
187 }
188
189 fn with_child<T>(&mut self, index: usize, f: impl FnOnce(&mut Self) -> T) -> T {
190 if !self.track_origins {
191 return f(self);
192 }
193 self.evidence_path.push(index);
194 let value = f(self);
195 self.evidence_path.pop();
196 value
197 }
198
199 fn repair_child(&mut self, index: usize, value: &Witness) -> RepairTree {
200 self.with_child(index, |s| s.repair(value))
201 }
202
203 fn break_child(&mut self, index: usize, value: &SatTrace) -> RepairTree {
204 self.with_child(index, |s| s.break_(value))
205 }
206
207 fn hole(&mut self) -> Hole {
208 let h = Hole(self.next_hole);
209 self.next_hole += 1;
210 h
211 }
212
213 fn all(&mut self, children: Vec<RepairTree>) -> RepairTree {
217 let mut kept = Vec::with_capacity(children.len());
218 for c in children {
219 match c {
220 RepairTree::Blocked(_, r) => return RepairTree::Blocked(self.node(), r),
221 RepairTree::Noop(_) => {}
222 other => kept.push(other),
223 }
224 }
225 match kept.len() {
226 0 => RepairTree::Noop(self.node()),
227 1 => kept.pop().unwrap(),
228 _ => RepairTree::All {
229 id: self.node(),
230 children: kept,
231 },
232 }
233 }
234
235 fn any(&mut self, children: Vec<RepairTree>) -> RepairTree {
237 let had = !children.is_empty();
238 let mut reason = None;
239 let mut kept = Vec::with_capacity(children.len());
240 for c in children {
241 match c {
242 RepairTree::Blocked(_, r) => reason = Some(r),
243 other => kept.push(other),
244 }
245 }
246 match kept.len() {
247 0 if had => RepairTree::Blocked(
248 self.node(),
249 reason.unwrap_or(BlockReason::Unsupported("no repair branch".into())),
250 ),
251 0 => RepairTree::Noop(self.node()),
252 1 => kept.pop().unwrap(),
253 _ => RepairTree::Any {
254 id: self.node(),
255 children: kept,
256 },
257 }
258 }
259
260 fn blocked(&mut self, reason: BlockReason) -> RepairTree {
261 RepairTree::Blocked(self.node(), reason)
262 }
263
264 fn edits(&mut self, edits: Vec<Edit>, holes: Vec<(Hole, HoleConstraint)>) -> RepairTree {
265 RepairTree::Edits {
266 id: self.node(),
267 edits,
268 holes,
269 }
270 }
271
272 fn repair(&mut self, w: &Witness) -> RepairTree {
275 self.with_evidence(EvidenceNodeRef::Failure(w), |s| s.repair_inner(w))
276 }
277
278 fn repair_inner(&mut self, w: &Witness) -> RepairTree {
279 match w {
280 Witness::Atom {
281 shape, produced_by, ..
282 } => self.repair_atom(*shape, produced_by.as_ref()),
283 Witness::Relational {
284 kind,
285 lhs,
286 rhs,
287 offending,
288 ..
289 } => self.repair_relational(*kind, lhs, rhs, offending),
290 Witness::Closed {
291 node, offenders, ..
292 } => {
293 let edits = offenders
294 .iter()
295 .map(|(p, o)| {
296 Edit::delete(TriplePattern::new(
297 Slot::Bound(node.clone()),
298 Slot::Bound(Term::NamedNode(p.clone())),
299 Slot::Bound(o.clone()),
300 ))
301 })
302 .collect();
303 self.edits(edits, vec![])
304 }
305 Witness::Not { inner, .. } => self.break_child(0, inner),
306 Witness::All { failed, .. } => {
307 let cs = failed
308 .iter()
309 .enumerate()
310 .map(|(index, child)| self.repair_child(index, child))
311 .collect();
312 self.all(cs)
313 }
314 Witness::Any { branches, .. } => {
315 let cs = branches
316 .iter()
317 .enumerate()
318 .map(|(index, child)| self.repair_child(index, child))
319 .collect();
320 self.any(cs)
321 }
322 Witness::CountLow {
323 node,
324 path,
325 qualifier,
326 have,
327 min,
328 sibling_qualifiers,
329 ..
330 } => {
331 let effective = self.effective_qualifiers(*qualifier, sibling_qualifiers);
332 let body = self.build_value(node, path, &effective);
333 if body.is_blocked() {
334 return body;
335 }
336 RepairTree::Repeat {
337 id: self.node(),
338 body: Box::new(body),
339 min: min - have,
340 max: None,
341 }
342 }
343 Witness::CountHigh {
344 node,
345 path,
346 matched,
347 max,
348 per_value,
349 ..
350 } => self.repair_count_high(node, path, matched, *max, per_value),
351 Witness::Opaque { .. } => self.blocked(BlockReason::OpaqueSparql),
352 }
353 }
354
355 fn repair_atom(&mut self, shape: ShapeId, produced_by: Option<&PathSupport>) -> RepairTree {
356 let Some(ps) = produced_by else {
358 return self.blocked(BlockReason::CannotMutateIdentity);
360 };
361 let Some(edge) = cut_edge(ps) else {
362 return self.blocked(BlockReason::CannotMutateIdentity);
363 };
364 let del = Edit::delete(delete_pattern(&edge));
365 let hole = self.hole();
366 let add = Edit::add(TriplePattern::new(
367 Slot::Bound(subj_term(&edge)),
368 Slot::Bound(Term::NamedNode(edge.predicate.clone())),
369 Slot::Open(hole),
370 ));
371 let constraint = self.leaf_constraint(shape);
372 self.edits(vec![del, add], vec![(hole, constraint)])
373 }
374
375 fn repair_relational(
376 &mut self,
377 kind: RelKind,
378 lhs: &[(Term, PathSupport)],
379 rhs: &[(Term, PathSupport)],
380 offending: &[(Term, Term)],
381 ) -> RepairTree {
382 match kind {
383 RelKind::Eq => {
384 self.blocked(BlockReason::Unsupported("sh:equals reconciliation".into()))
385 }
386 RelKind::Disj => {
387 let parts = offending
389 .iter()
390 .map(|(v, _)| {
391 let mut opts = Vec::new();
392 opts.extend(self.delete_value(v, lhs));
393 opts.extend(self.delete_value(v, rhs));
394 self.any(opts)
395 })
396 .collect();
397 self.all(parts)
398 }
399 RelKind::UniqueLang => {
400 let parts = offending
402 .iter()
403 .map(|(a, b)| {
404 let mut opts = Vec::new();
405 opts.extend(self.delete_value(a, lhs));
406 opts.extend(self.delete_value(b, lhs));
407 self.any(opts)
408 })
409 .collect();
410 self.all(parts)
411 }
412 RelKind::Lt | RelKind::Le => {
413 let parts = offending
415 .iter()
416 .map(|(a, _)| {
417 let opts = self.delete_value(a, lhs);
418 self.any(opts)
419 })
420 .collect();
421 self.all(parts)
422 }
423 }
424 }
425
426 fn delete_value(&mut self, v: &Term, set: &[(Term, PathSupport)]) -> Vec<RepairTree> {
428 set.iter()
429 .filter(|(val, _)| val == v)
430 .filter_map(|(_, ps)| cut_edge(ps))
431 .map(|edge| self.edits(vec![Edit::delete(delete_pattern(&edge))], vec![]))
432 .collect()
433 }
434
435 fn repair_count_high(
436 &mut self,
437 node: &Term,
438 path: &Path,
439 matched: &[(Term, PathSupport)],
440 max: u64,
441 per_value: &[(Term, Witness)],
442 ) -> RepairTree {
443 if !per_value.is_empty() {
444 let cs = per_value
446 .iter()
447 .enumerate()
448 .map(|(index, (_, witness))| self.repair_child(index, witness))
449 .collect();
450 return self.all(cs);
451 }
452 let d = (matched.len() as u64).saturating_sub(max);
454 if d == 0 {
455 return self.blocked(BlockReason::Unsupported(
456 "maxCount: nothing to delete".into(),
457 ));
458 }
459 let Path::Pred(p) = path else {
460 return self.blocked(BlockReason::Unsupported(
461 "maxCount over a compound path".into(),
462 ));
463 };
464 let hole = self.hole();
465 let values: Vec<Term> = matched.iter().map(|(v, _)| v.clone()).collect();
466 let body = self.edits(
467 vec![Edit::delete(TriplePattern::new(
468 Slot::Bound(node.clone()),
469 Slot::Bound(Term::NamedNode(p.clone())),
470 Slot::Open(hole),
471 ))],
472 vec![(hole, HoleConstraint::OneOf(values))],
473 );
474 RepairTree::Repeat {
475 id: self.node(),
476 body: Box::new(body),
477 min: d,
478 max: Some(d),
479 }
480 }
481
482 fn build_value(&mut self, subject: &Term, path: &Path, qualifiers: &[ShapeId]) -> RepairTree {
488 let vh = self.hole();
489 let Some((edits, mut holes)) = self.materialize_path(subject.clone(), path, vh) else {
490 return self.blocked(BlockReason::Unsupported("path materialization".into()));
491 };
492 holes.push((vh, self.value_constraint(qualifiers)));
493 self.edits(edits, holes)
494 }
495
496 fn materialize_path(&mut self, subject: Term, path: &Path, vh: Hole) -> Option<Materialized> {
502 match path {
503 Path::Pred(p) => Some((
504 vec![Edit::add(TriplePattern::new(
505 Slot::Bound(subject),
506 Slot::Bound(Term::NamedNode(p.clone())),
507 Slot::Open(vh),
508 ))],
509 vec![],
510 )),
511 Path::Inverse(inner) => {
512 if let Path::Pred(p) = inner.as_ref() {
513 Some((
514 vec![Edit::add(TriplePattern::new(
515 Slot::Open(vh),
516 Slot::Bound(Term::NamedNode(p.clone())),
517 Slot::Bound(subject),
518 ))],
519 vec![],
520 ))
521 } else {
522 None
523 }
524 }
525 Path::Star(inner) => self.materialize_path(subject, inner, vh),
528 Path::Seq(steps) => self.materialize_seq(subject, steps, vh),
529 _ => None,
530 }
531 }
532
533 fn materialize_seq(&mut self, subject: Term, steps: &[Path], vh: Hole) -> Option<Materialized> {
538 let mut end = steps.len();
539 while end > 0 && matches!(steps[end - 1], Path::Star(_)) {
540 end -= 1;
541 }
542 let steps = &steps[..end];
543 if steps.is_empty() {
544 return None; }
546 let mut edits = Vec::new();
547 let mut holes = Vec::new();
548 let mut cursor = Slot::Bound(subject);
549 for (i, step) in steps.iter().enumerate() {
550 let target = if i == steps.len() - 1 {
551 Slot::Open(vh)
552 } else {
553 let m = self.hole();
554 holes.push((m, HoleConstraint::Fresh));
555 Slot::Open(m)
556 };
557 match step {
558 Path::Pred(p) => edits.push(Edit::add(TriplePattern::new(
559 cursor.clone(),
560 Slot::Bound(Term::NamedNode(p.clone())),
561 target.clone(),
562 ))),
563 Path::Inverse(inner) => {
564 let Path::Pred(p) = inner.as_ref() else {
565 return None;
566 };
567 edits.push(Edit::add(TriplePattern::new(
568 target.clone(),
569 Slot::Bound(Term::NamedNode(p.clone())),
570 cursor.clone(),
571 )));
572 }
573 _ => return None, }
575 cursor = target;
576 }
577 Some((edits, holes))
578 }
579
580 fn effective_qualifiers(&self, qualifier: ShapeId, siblings: &[ShapeId]) -> Vec<ShapeId> {
588 let mut out: Vec<ShapeId> = Vec::with_capacity(1 + siblings.len());
589 for id in std::iter::once(qualifier).chain(siblings.iter().copied()) {
590 if matches!(self.arena.get(id), Shape::Top | Shape::Pending) {
591 continue;
592 }
593 if !out.contains(&id) {
594 out.push(id);
595 }
596 }
597 out
598 }
599
600 fn value_constraint(&self, qualifiers: &[ShapeId]) -> HoleConstraint {
606 match qualifiers {
607 [] => HoleConstraint::AnyNode,
608 [single] => self.single_value_constraint(*single),
609 many => HoleConstraint::ConformsToAll(many.to_vec()),
610 }
611 }
612
613 fn single_value_constraint(&self, qualifier: ShapeId) -> HoleConstraint {
614 match self.arena.get(qualifier) {
615 Shape::Top | Shape::Pending => HoleConstraint::AnyNode,
616 Shape::TestConst(c) => HoleConstraint::Const(c.clone()),
617 Shape::TestType(t) => HoleConstraint::Typed(t.clone()),
618 Shape::TestKind(k) => HoleConstraint::Kind(*k),
619 _ => HoleConstraint::ConformsTo(qualifier),
620 }
621 }
622
623 fn leaf_constraint(&self, shape: ShapeId) -> HoleConstraint {
624 match self.arena.get(shape) {
625 Shape::TestConst(c) => HoleConstraint::Const(c.clone()),
626 Shape::TestType(t) => HoleConstraint::Typed(t.clone()),
627 Shape::TestKind(k) => HoleConstraint::Kind(*k),
628 _ => HoleConstraint::ConformsTo(shape),
629 }
630 }
631
632 fn break_(&mut self, s: &SatTrace) -> RepairTree {
635 self.with_evidence(EvidenceNodeRef::Satisfaction(s), |this| this.break_inner(s))
636 }
637
638 fn break_inner(&mut self, s: &SatTrace) -> RepairTree {
639 match s {
640 SatTrace::Atom { produced_by, .. } => match cut_edge(produced_by) {
641 Some(edge) => self.edits(vec![Edit::delete(delete_pattern(&edge))], vec![]),
642 None => self.blocked(BlockReason::CannotMutateIdentity),
643 },
644 SatTrace::AllHeld { children, .. } => {
645 let cs = children
647 .iter()
648 .enumerate()
649 .map(|(index, child)| self.break_child(index, child))
650 .collect();
651 self.any(cs)
652 }
653 SatTrace::AnyHeld { satisfied, .. } => {
654 let cs = satisfied
656 .iter()
657 .enumerate()
658 .map(|(index, child)| self.break_child(index, child))
659 .collect();
660 self.all(cs)
661 }
662 SatTrace::CountHeld { matches, min, .. } => {
663 if matches!(min, Some(m) if *m > 0) {
667 let cs = matches
668 .iter()
669 .enumerate()
670 .map(|(index, (_, _, trace))| self.break_child(index, trace))
671 .collect();
672 self.all(cs)
673 } else {
674 self.blocked(BlockReason::Unsupported(
675 "falsify maxCount needs adds".into(),
676 ))
677 }
678 }
679 SatTrace::ForAllHeld { values, .. } => {
680 let choices = values
681 .iter()
682 .enumerate()
683 .map(|(index, (_, _, trace))| self.break_child(index, trace))
684 .collect();
685 self.any(choices)
686 }
687 SatTrace::NotHeld { inner_fails, .. } => self.repair_child(0, inner_fails),
688 SatTrace::Irrefutable { .. } => {
689 self.blocked(BlockReason::Unsupported("tautology".into()))
690 }
691 SatTrace::Blocked { reason, .. } => {
692 let r = match reason {
693 WBlock::OpaqueSparql => BlockReason::OpaqueSparql,
694 WBlock::ClosedNeedsAdd => {
695 BlockReason::Unsupported("closed needs an addition".into())
696 }
697 WBlock::Unsupported => {
698 BlockReason::Unsupported("relational falsification".into())
699 }
700 };
701 self.blocked(r)
702 }
703 SatTrace::Coinductive { .. } => self.blocked(BlockReason::Coinductive),
704 }
705 }
706}
707
708fn evidence_origin(
709 statement: Option<usize>,
710 path: &[usize],
711 evidence: EvidenceNodeRef<'_>,
712) -> EvidenceOrigin {
713 EvidenceOrigin {
714 statement,
715 path: path.to_vec(),
716 constraint_id: evidence.constraint_id(),
717 node: evidence.node().cloned(),
718 status: evidence.status(),
719 kind: evidence.evidence_kind(),
720 }
721}
722
723fn collect_tree_nodes(tree: &RepairTree, out: &mut BTreeSet<NodeId>) {
724 out.insert(tree.id());
725 match tree {
726 RepairTree::All { children, .. } | RepairTree::Any { children, .. } => {
727 children
728 .iter()
729 .for_each(|child| collect_tree_nodes(child, out));
730 }
731 RepairTree::Repeat { body, .. } => collect_tree_nodes(body, out),
732 RepairTree::Noop(_) | RepairTree::Blocked(..) | RepairTree::Edits { .. } => {}
733 }
734}
735
736fn cut_edge(ps: &PathSupport) -> Option<Triple> {
741 match ps {
742 PathSupport::Empty => None,
743 PathSupport::Edge(t) => Some(t.clone()),
744 PathSupport::Chain(v) => v.last().and_then(cut_edge),
745 PathSupport::Alt(v) => v.first().and_then(cut_edge),
746 }
747}
748
749fn delete_pattern(t: &Triple) -> TriplePattern {
750 TriplePattern::new(
751 Slot::Bound(subj_term(t)),
752 Slot::Bound(Term::NamedNode(t.predicate.clone())),
753 Slot::Bound(t.object.clone()),
754 )
755}
756
757fn subj_term(t: &Triple) -> Term {
758 match &t.subject {
759 NamedOrBlankNode::NamedNode(n) => Term::NamedNode(n.clone()),
760 NamedOrBlankNode::BlankNode(b) => Term::BlankNode(b.clone()),
761 }
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767 use crate::witness::{Evidence, witness_node, witness_violations};
768 use shifty_parse::{load_turtle, parse_turtle};
769 use shifty_repair::{Plan, instantiate};
770
771 const PREFIXES: &str = r#"
772 @prefix sh: <http://www.w3.org/ns/shacl#> .
773 @prefix ex: <http://ex/> .
774 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
775 "#;
776
777 fn synth_one(ttl: &str) -> (RepairTree, shifty_algebra::Schema) {
778 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
779 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
780 let ws =
781 witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).expect("stratifiable");
782 assert_eq!(ws.len(), 1, "expected exactly one focus witness");
783 (synthesize(&parsed.schema.arena, &ws[0]), parsed.schema)
784 }
785
786 #[test]
787 fn every_retained_repair_node_links_to_its_exact_evidence_occurrence() {
788 let ttl = format!(
789 "{PREFIXES}
790 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
791 sh:not [ sh:class ex:C ] .
792 ex:x a ex:C .
793 "
794 );
795 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
796 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
797 let witnesses =
798 witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).expect("stratifiable");
799 assert_eq!(witnesses.len(), 1);
800
801 let synthesized = synthesize_with_origins(&parsed.schema.arena, &witnesses[0]);
802 assert_eq!(
803 synthesize(&parsed.schema.arena, &witnesses[0]),
804 synthesized.tree,
805 "origin capture must not change the compatibility repair tree",
806 );
807 let mut nodes = BTreeSet::new();
808 collect_tree_nodes(&synthesized.tree, &mut nodes);
809 assert_eq!(
810 synthesized.origins.keys().copied().collect::<BTreeSet<_>>(),
811 nodes,
812 "every retained repair node has an origin and discarded smart-constructor nodes do not",
813 );
814
815 let evidence = Evidence::Failure(witnesses[0].failure.clone());
816 for origins in synthesized.origins.values() {
817 assert_eq!(origins.len(), 1);
818 let origin = &origins[0];
819 let mut cursor = evidence.walk()[0];
820 for &index in &origin.path {
821 cursor = cursor.children()[index];
822 }
823 assert_eq!(origin.statement, Some(witnesses[0].statement));
824 assert_eq!(origin.constraint_id, cursor.constraint_id());
825 assert_eq!(origin.node.as_ref(), cursor.node());
826 assert_eq!(origin.status, cursor.status());
827 assert_eq!(origin.kind, cursor.evidence_kind());
828 }
829
830 assert!(
831 synthesized
832 .origins
833 .values()
834 .flatten()
835 .any(|origin| origin.status == EvaluationStatus::Pass),
836 "repairing a failed Not must link its deletive operators to the inner satisfaction",
837 );
838 }
839
840 #[test]
841 fn direct_node_repair_has_no_authored_statement_origin() {
842 let ttl = format!(
843 "{PREFIXES}
844 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
845 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
846 "
847 );
848 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
849 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
850 let statement = &parsed.schema.statements[0];
851 let node = Term::NamedNode(oxrdf::NamedNode::new("http://ex/direct").unwrap());
852 let witness = witness_node(&loaded.graph, &parsed.schema, &node, statement.shape)
853 .expect("stratifiable")
854 .expect("direct node fails");
855
856 let synthesized = synthesize_with_origins(&parsed.schema.arena, &witness);
857 assert!(
858 synthesized
859 .origins
860 .values()
861 .flatten()
862 .all(|origin| origin.statement.is_none())
863 );
864 }
865
866 #[test]
867 fn joint_repair_root_links_every_failure_it_groups() {
868 let ttl = format!(
869 "{PREFIXES}
870 ex:S1 a sh:NodeShape ; sh:targetNode ex:x ;
871 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
872 ex:S2 a sh:NodeShape ; sh:targetNode ex:x ;
873 sh:property [ sh:path ex:q ; sh:minCount 1 ] .
874 "
875 );
876 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
877 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
878 let witnesses =
879 witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).expect("stratifiable");
880 assert_eq!(witnesses.len(), 2);
881
882 let synthesized = synthesize_focus_with_origins(&parsed.schema.arena, &witnesses);
883 let RepairTree::All { id, .. } = &synthesized.tree else {
884 panic!("two independent obligations must retain a joint All root")
885 };
886 let origins = synthesized.origins_for(*id);
887 assert_eq!(origins.len(), 2);
888 assert_eq!(
889 origins
890 .iter()
891 .filter_map(|origin| origin.statement)
892 .collect::<BTreeSet<_>>(),
893 witnesses.iter().map(|witness| witness.statement).collect(),
894 );
895 assert!(origins.iter().all(|origin| origin.path.is_empty()));
896 }
897
898 #[test]
899 fn min_count_yields_a_repeat_adding_one_value() {
900 let ttl = format!(
901 "{PREFIXES}
902 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
903 sh:property [ sh:path ex:p ; sh:minCount 2 ] .
904 ex:x ex:p ex:y .
905 "
906 );
907 let (tree, _) = synth_one(&ttl);
908 let id = match &tree {
910 RepairTree::Repeat { id, min: 1, .. } => *id,
911 other => panic!("expected Repeat{{min:1}}, got {other:?}"),
912 };
913 let plan = Plan {
915 count: std::collections::HashMap::from([(id, 1)]),
916 ..Default::default()
917 };
918 let disc = instantiate(&tree, &plan);
919 assert_eq!(disc.open_holes.len(), 1);
920 let (hole, _) = disc.open_holes[0].clone();
921 let mut bound = plan;
922 bound.binding.insert(
923 hole,
924 Term::NamedNode(oxrdf::NamedNode::new("http://ex/z").unwrap()),
925 );
926 let out = instantiate(&tree, &bound);
927 assert_eq!(out.delta.add.len(), 1);
928 assert!(out.open_holes.is_empty());
929 }
930
931 #[test]
932 fn datatype_value_replace_instantiates_to_delete_plus_add() {
933 let ttl = format!(
934 "{PREFIXES}
935 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
936 sh:property [ sh:path ex:p ; sh:datatype xsd:integer ] .
937 ex:x ex:p \"hello\" .
938 "
939 );
940 let (tree, _) = synth_one(&ttl);
941 let mut plan = Plan::default();
943 let disc = instantiate(&tree, &plan);
945 assert_eq!(disc.open_holes.len(), 1, "one replacement hole");
946 let (hole, constraint) = disc.open_holes[0].clone();
947 assert!(matches!(constraint, HoleConstraint::Typed(_)));
948 plan.binding.insert(
949 hole,
950 Term::from(oxrdf::Literal::new_typed_literal(
951 "7",
952 oxrdf::vocab::xsd::INTEGER,
953 )),
954 );
955 let out = instantiate(&tree, &plan);
956 assert_eq!(out.delta.delete.len(), 1, "deletes the bad value");
957 assert_eq!(out.delta.add.len(), 1, "adds a good one");
958 }
959
960 #[test]
961 fn focus_nodekind_is_blocked() {
962 let ttl = format!(
963 "{PREFIXES}
964 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
965 sh:nodeKind sh:Literal .
966 ex:x ex:p ex:y .
967 "
968 );
969 let (tree, _) = synth_one(&ttl);
970 assert!(matches!(
971 tree,
972 RepairTree::Blocked(_, BlockReason::CannotMutateIdentity)
973 ));
974 }
975
976 #[test]
977 fn class_qualified_min_count_builds_a_type_assertion() {
978 let ttl = format!(
982 "{PREFIXES}
983 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
984 sh:property [ sh:path ex:part ;
985 sh:qualifiedValueShape ex:PartShape ; sh:qualifiedMinCount 1 ] .
986 ex:PartShape a sh:NodeShape ; sh:class ex:Widget .
987 ex:x a ex:Thing .
988 "
989 );
990 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
991 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
992 let ws = witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).unwrap();
993 let parent = synthesize(&parsed.schema.arena, &ws[0]);
994 let mut plan = Plan {
996 count: std::collections::HashMap::new(),
997 ..Default::default()
998 };
999 let parent_hole = {
1001 let disc = instantiate(
1002 &parent,
1003 &Plan {
1004 count: std::collections::HashMap::from([(parent.id(), 1)]),
1005 ..Default::default()
1006 },
1007 );
1008 disc.open_holes[0].clone()
1009 };
1010 let HoleConstraint::ConformsTo(part_shape) = parent_hole.1 else {
1012 panic!("expected ConformsTo, got {:?}", parent_hole.1);
1013 };
1014 let fresh = Term::NamedNode(oxrdf::NamedNode::new("http://ex/part1").unwrap());
1015 let sub_fw =
1016 crate::witness::witness_node(&loaded.graph, &parsed.schema, &fresh, part_shape)
1017 .unwrap()
1018 .expect("fresh node fails PartShape");
1019 let sub = synthesize(&parsed.schema.arena, &sub_fw);
1020 assert!(
1021 !sub.is_blocked(),
1022 "class build must not be blocked: {sub:?}"
1023 );
1024 let disc = instantiate(
1026 &sub,
1027 &Plan {
1028 count: std::collections::HashMap::from([(sub.id(), 1)]),
1029 ..Default::default()
1030 },
1031 );
1032 let (h, c) = disc.open_holes[0].clone();
1033 assert!(
1034 matches!(c, HoleConstraint::Const(_)),
1035 "type hole is Const(C): {c:?}"
1036 );
1037 plan.count.insert(sub.id(), 1);
1038 plan.binding.insert(
1039 h,
1040 Term::NamedNode(oxrdf::NamedNode::new("http://ex/Widget").unwrap()),
1041 );
1042 let out = instantiate(&sub, &plan);
1043 assert_eq!(out.delta.add.len(), 1);
1044 assert_eq!(
1045 out.delta.add[0].predicate.as_str(),
1046 "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
1047 );
1048 }
1049
1050 #[test]
1051 fn closed_violation_deletes_the_extra_triple() {
1052 let ttl = format!(
1053 "{PREFIXES}
1054 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1055 sh:closed true ;
1056 sh:property [ sh:path ex:p ; sh:minCount 0 ] .
1057 ex:x ex:p ex:y ; ex:extra ex:z .
1058 "
1059 );
1060 let (tree, _) = synth_one(&ttl);
1061 let out = instantiate(&tree, &Plan::default());
1062 assert!(out.delta.add.is_empty());
1063 assert_eq!(out.delta.delete.len(), 1);
1064 assert_eq!(
1065 out.delta.delete[0].predicate,
1066 oxrdf::NamedNode::new("http://ex/extra").unwrap()
1067 );
1068 }
1069
1070 fn sole_hole(tree: &RepairTree) -> HoleConstraint {
1072 let disc = instantiate(
1073 tree,
1074 &Plan {
1075 count: std::collections::HashMap::from([(tree.id(), 1)]),
1076 ..Default::default()
1077 },
1078 );
1079 assert_eq!(disc.open_holes.len(), 1, "expected exactly one open hole");
1080 disc.open_holes[0].1.clone()
1081 }
1082
1083 fn class_named(name: &str) -> Term {
1084 Term::NamedNode(oxrdf::NamedNode::new(name).unwrap())
1085 }
1086
1087 #[test]
1088 fn min_count_with_class_value_constraint_carries_the_class() {
1089 let ttl = format!(
1094 "{PREFIXES}
1095 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1096 sh:property [ sh:path ex:p ; sh:minCount 1 ; sh:class ex:C ] .
1097 ex:x a ex:Thing .
1098 "
1099 );
1100 let (tree, schema) = synth_one(&ttl);
1101 let HoleConstraint::ConformsTo(s) = sole_hole(&tree) else {
1102 panic!("expected a single ConformsTo hole");
1103 };
1104 assert_eq!(
1105 shifty_algebra::render::class_target_shape(s, &schema.arena),
1106 Some(class_named("http://ex/C")),
1107 );
1108 }
1109
1110 #[test]
1111 fn qualified_min_count_carries_qualifier_and_sibling_universal() {
1112 let ttl = format!(
1117 "{PREFIXES}
1118 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1119 sh:property [ sh:path ex:p ;
1120 sh:qualifiedValueShape ex:Q ; sh:qualifiedMinCount 1 ;
1121 sh:class ex:C ] .
1122 ex:Q a sh:NodeShape ; sh:nodeKind sh:IRI .
1123 ex:x a ex:Thing .
1124 "
1125 );
1126 let (tree, schema) = synth_one(&ttl);
1127 let HoleConstraint::ConformsToAll(ss) = sole_hole(&tree) else {
1128 panic!("expected a ConformsToAll hole carrying every qualifier");
1129 };
1130 assert_eq!(ss.len(), 2, "qualifier + sibling universal");
1131 let classes: Vec<Term> = ss
1133 .iter()
1134 .filter_map(|s| shifty_algebra::render::class_target_shape(*s, &schema.arena))
1135 .collect();
1136 assert_eq!(classes, vec![class_named("http://ex/C")]);
1137 assert!(
1139 ss.iter().any(|s| matches!(
1140 schema.arena.get(*s),
1141 Shape::Annotated { .. } | Shape::TestKind(_)
1142 )),
1143 "the qualifiedValueShape must still be present: {ss:?}",
1144 );
1145 }
1146}