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