1use crate::frozen::FrozenIndexedDataset;
28use crate::path::{PathBackend, node_of, pred, succ};
29use crate::sparql::{SparqlDiagnostic, SparqlExecutor};
30use crate::validate::{NonStratifiable, ShapeEvaluator, focus_nodes_with, uses_shapes_graph};
31use oxrdf::{Graph, NamedNode, Term, Triple};
32use serde::{Deserialize, Serialize};
33use shifty_algebra::{ConstraintKind, Path, Schema, Selector, Shape, ShapeArena, ShapeId};
34use shifty_opt::analyze;
35use std::collections::{BTreeSet, HashSet, VecDeque};
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
40pub struct FocusWitness {
41 pub focus: Term,
42 pub statement: usize,
44 pub failure: Failure,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
49pub enum RelKind {
50 Eq,
51 Disj,
52 Lt,
53 Le,
54 UniqueLang,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
63#[serde(tag = "type", content = "details", rename_all = "snake_case")]
64pub enum PathSupport {
65 Empty,
67 Edge(Triple),
69 Chain(Vec<PathSupport>),
71 Alt(Vec<PathSupport>),
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
79pub struct QualifiedMatch {
80 pub value: Term,
81 pub path_support: PathSupport,
82 pub satisfaction: Box<Satisfaction>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
87pub struct RejectedCandidate {
88 pub value: Term,
89 pub path_support: PathSupport,
90 pub failure: Box<Failure>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
95#[serde(tag = "type", content = "details", rename_all = "snake_case")]
96pub enum Failure {
97 Atom {
101 shape: ShapeId,
102 node: Term,
103 observed: Vec<Term>,
104 expected: Box<Shape>,
105 reached_by: Path,
106 produced_by: Option<PathSupport>,
107 },
108 Relational {
112 shape: ShapeId,
113 node: Term,
114 kind: RelKind,
115 lhs: Vec<(Term, PathSupport)>,
116 rhs: Vec<(Term, PathSupport)>,
117 offending: Vec<(Term, Term)>,
118 },
119 Closed {
121 shape: ShapeId,
122 node: Term,
123 offenders: Vec<(NamedNode, Term)>,
124 },
125 Not {
127 shape: ShapeId,
128 node: Term,
129 inner: Box<Satisfaction>,
130 },
131 All {
133 shape: ShapeId,
134 node: Term,
135 failed: Vec<Failure>,
136 },
137 Any {
139 shape: ShapeId,
140 node: Term,
141 branches: Vec<Failure>,
142 },
143 CountLow {
152 shape: ShapeId,
153 node: Term,
154 path: Path,
155 qualifier: ShapeId,
156 have: u64,
157 min: u64,
158 qualifying_matches: Vec<QualifiedMatch>,
159 rejected_candidates: Vec<RejectedCandidate>,
160 sibling_qualifiers: Vec<ShapeId>,
161 },
162 CountHigh {
166 shape: ShapeId,
167 node: Term,
168 path: Path,
169 qualifier: ShapeId,
170 matched: Vec<(Term, PathSupport)>,
171 max: u64,
172 excess_values: Vec<(Term, PathSupport)>,
173 per_value: Vec<(Term, Failure)>,
174 },
175 Opaque {
177 shape: ShapeId,
178 node: Term,
179 messages: Vec<Term>,
180 diagnostic: Option<SparqlDiagnostic>,
181 },
182}
183
184pub type Witness = Failure;
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
191#[serde(tag = "type", content = "details", rename_all = "snake_case")]
192pub enum Satisfaction {
193 Irrefutable { shape: ShapeId },
195 Atom {
197 shape: ShapeId,
198 node: Term,
199 observed: Vec<Term>,
200 expected: Box<Shape>,
201 reached_by: Path,
202 produced_by: PathSupport,
203 },
204 AllHeld {
206 shape: ShapeId,
207 node: Term,
208 children: Vec<Satisfaction>,
209 },
210 AnyHeld {
212 shape: ShapeId,
213 node: Term,
214 satisfied: Vec<Satisfaction>,
215 },
216 CountHeld {
219 shape: ShapeId,
220 node: Term,
221 path: Path,
222 qualifier: ShapeId,
223 matches: Vec<(Term, PathSupport, Satisfaction)>,
224 observed_count: u64,
225 min: Option<u64>,
226 max: Option<u64>,
227 },
228 ForAllHeld {
232 shape: ShapeId,
233 node: Term,
234 path: Path,
235 qualifier: ShapeId,
236 values: Vec<(Term, PathSupport, Satisfaction)>,
237 },
238 NotHeld {
240 shape: ShapeId,
241 node: Term,
242 inner_fails: Box<Failure>,
243 },
244 Blocked {
247 shape: ShapeId,
248 node: Term,
249 reason: BlockReason,
250 },
251 Coinductive { shape: ShapeId, node: Term },
254}
255
256pub type SatTrace = Satisfaction;
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
262#[serde(tag = "status", content = "evidence", rename_all = "snake_case")]
263pub enum Evidence {
264 #[serde(rename = "pass")]
265 Satisfaction(Satisfaction),
266 #[serde(rename = "fail")]
267 Failure(Failure),
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
271#[serde(rename_all = "snake_case")]
272pub enum EvaluationStatus {
273 Pass,
274 Fail,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
283#[serde(rename_all = "snake_case")]
284pub enum EvidenceKind {
285 Irrefutable,
286 AtomHeld,
287 AllHeld,
288 AnyHeld,
289 CountHeld,
290 AllValuesHeld,
291 NotHeld,
292 Blocked,
293 Coinductive,
294 AtomFailed,
295 RelationalFailed,
296 ClosedFailed,
297 NotFailed,
298 AllFailed,
299 AnyFailed,
300 CountLow,
301 CountHigh,
302 Opaque,
303}
304
305impl EvidenceKind {
306 pub fn status(self) -> EvaluationStatus {
307 match self {
308 Self::Irrefutable
309 | Self::AtomHeld
310 | Self::AllHeld
311 | Self::AnyHeld
312 | Self::CountHeld
313 | Self::AllValuesHeld
314 | Self::NotHeld
315 | Self::Blocked
316 | Self::Coinductive => EvaluationStatus::Pass,
317 Self::AtomFailed
318 | Self::RelationalFailed
319 | Self::ClosedFailed
320 | Self::NotFailed
321 | Self::AllFailed
322 | Self::AnyFailed
323 | Self::CountLow
324 | Self::CountHigh
325 | Self::Opaque => EvaluationStatus::Fail,
326 }
327 }
328
329 pub fn as_str(self) -> &'static str {
330 match self {
331 Self::Irrefutable => "irrefutable",
332 Self::AtomHeld => "atom_held",
333 Self::AllHeld => "all_held",
334 Self::AnyHeld => "any_held",
335 Self::CountHeld => "count_held",
336 Self::AllValuesHeld => "all_values_held",
337 Self::NotHeld => "not_held",
338 Self::Blocked => "blocked",
339 Self::Coinductive => "coinductive",
340 Self::AtomFailed => "atom_failed",
341 Self::RelationalFailed => "relational_failed",
342 Self::ClosedFailed => "closed_failed",
343 Self::NotFailed => "not_failed",
344 Self::AllFailed => "all_failed",
345 Self::AnyFailed => "any_failed",
346 Self::CountLow => "count_low",
347 Self::CountHigh => "count_high",
348 Self::Opaque => "opaque",
349 }
350 }
351}
352
353impl Evidence {
354 pub fn status(&self) -> EvaluationStatus {
355 match self {
356 Self::Satisfaction(_) => EvaluationStatus::Pass,
357 Self::Failure(_) => EvaluationStatus::Fail,
358 }
359 }
360
361 pub fn walk(&self) -> Vec<EvidenceNodeRef<'_>> {
364 let mut out = Vec::new();
365 walk_evidence(self, &mut out);
366 out
367 }
368
369 pub fn supporting_triples(&self) -> Vec<Triple> {
370 let mut triples = Vec::new();
371 collect_support(self, &mut triples);
372 dedup_stable(triples)
373 }
374
375 pub fn path_supports(&self) -> Vec<PathSupport> {
378 let mut supports = Vec::new();
379 collect_path_supports(self, &mut supports);
380 supports.into_iter().fold(Vec::new(), |mut out, value| {
381 if !out.contains(&value) {
382 out.push(value);
383 }
384 out
385 })
386 }
387
388 pub fn matched_values(&self) -> Vec<Term> {
389 let mut values = Vec::new();
390 collect_matched(self, &mut values);
391 dedup_stable(values)
392 }
393
394 pub fn matched_values_by_path(&self) -> Vec<(Path, Vec<Term>)> {
403 let mut pairs = Vec::new();
404 collect_matched_by_path(self, &mut pairs);
405 let mut out: Vec<(Path, Vec<Term>)> = Vec::new();
406 for (path, value) in pairs {
407 match out.iter_mut().find(|(seen, _)| seen == path) {
408 Some((_, values)) => {
409 if !values.contains(value) {
410 values.push(value.clone());
411 }
412 }
413 None => out.push((path.clone(), vec![value.clone()])),
414 }
415 }
416 out
417 }
418
419 pub fn values_for_path(&self, path: &Path) -> Vec<Term> {
424 let mut pairs = Vec::new();
425 collect_matched_by_path(self, &mut pairs);
426 dedup_stable(
427 pairs
428 .into_iter()
429 .filter(|(seen, _)| *seen == path)
430 .map(|(_, value)| value.clone())
431 .collect(),
432 )
433 }
434
435 pub fn missing_obligations(&self) -> Vec<MissingObligation> {
436 let mut out = Vec::new();
437 collect_missing(self, &mut out);
438 out
439 }
440
441 pub fn offending_values(&self) -> Vec<Term> {
442 let mut values = Vec::new();
443 collect_offending(self, &mut values);
444 dedup_stable(values)
445 }
446
447 pub fn source_constraints(&self) -> Vec<ShapeId> {
448 dedup_stable(
449 self.walk()
450 .into_iter()
451 .map(EvidenceNodeRef::constraint_id)
452 .collect(),
453 )
454 }
455
456 pub fn explain(&self) -> String {
457 self.walk()
458 .into_iter()
459 .enumerate()
460 .map(|(index, node)| format!("{index}: {} @{}", node.kind(), node.constraint_id().0))
461 .collect::<Vec<_>>()
462 .join("\n")
463 }
464
465 pub fn to_json(&self) -> serde_json::Result<String> {
466 serde_json::to_string(self)
467 }
468}
469
470#[derive(Debug, Clone, Copy)]
471pub enum EvidenceNodeRef<'a> {
472 Satisfaction(&'a SatTrace),
473 Failure(&'a Witness),
474}
475
476impl<'a> EvidenceNodeRef<'a> {
477 pub fn constraint_id(self) -> ShapeId {
478 match self {
479 Self::Satisfaction(value) => satisfaction_constraint_id(value),
480 Self::Failure(value) => failure_constraint_id(value),
481 }
482 }
483
484 pub fn node(self) -> Option<&'a Term> {
490 match self {
491 Self::Satisfaction(value) => satisfaction_node(value),
492 Self::Failure(value) => Some(failure_node(value)),
493 }
494 }
495
496 pub fn status(self) -> EvaluationStatus {
497 self.evidence_kind().status()
498 }
499
500 pub fn evidence_kind(self) -> EvidenceKind {
501 match self {
502 Self::Satisfaction(value) => match value {
503 SatTrace::Irrefutable { .. } => EvidenceKind::Irrefutable,
504 SatTrace::Atom { .. } => EvidenceKind::AtomHeld,
505 SatTrace::AllHeld { .. } => EvidenceKind::AllHeld,
506 SatTrace::AnyHeld { .. } => EvidenceKind::AnyHeld,
507 SatTrace::CountHeld { .. } => EvidenceKind::CountHeld,
508 SatTrace::ForAllHeld { .. } => EvidenceKind::AllValuesHeld,
509 SatTrace::NotHeld { .. } => EvidenceKind::NotHeld,
510 SatTrace::Blocked { .. } => EvidenceKind::Blocked,
511 SatTrace::Coinductive { .. } => EvidenceKind::Coinductive,
512 },
513 Self::Failure(value) => match value {
514 Witness::Atom { .. } => EvidenceKind::AtomFailed,
515 Witness::Relational { .. } => EvidenceKind::RelationalFailed,
516 Witness::Closed { .. } => EvidenceKind::ClosedFailed,
517 Witness::Not { .. } => EvidenceKind::NotFailed,
518 Witness::All { .. } => EvidenceKind::AllFailed,
519 Witness::Any { .. } => EvidenceKind::AnyFailed,
520 Witness::CountLow { .. } => EvidenceKind::CountLow,
521 Witness::CountHigh { .. } => EvidenceKind::CountHigh,
522 Witness::Opaque { .. } => EvidenceKind::Opaque,
523 },
524 }
525 }
526
527 pub fn kind(self) -> &'static str {
528 self.evidence_kind().as_str()
529 }
530
531 pub fn children(self) -> Vec<Self> {
539 let mut out = Vec::new();
540 self.for_each_child(|child| out.push(child));
541 out
542 }
543
544 fn for_each_child(self, mut visit: impl FnMut(Self)) {
545 match self {
546 Self::Failure(value) => match value {
547 Witness::Not { inner, .. } => visit(Self::Satisfaction(inner)),
548 Witness::All { failed, .. } => {
549 failed.iter().for_each(|child| visit(Self::Failure(child)))
550 }
551 Witness::Any { branches, .. } => branches
552 .iter()
553 .for_each(|child| visit(Self::Failure(child))),
554 Witness::CountLow {
555 qualifying_matches,
556 rejected_candidates,
557 ..
558 } => {
559 qualifying_matches.iter().for_each(|item| {
560 visit(Self::Satisfaction(&item.satisfaction));
561 });
562 rejected_candidates
563 .iter()
564 .for_each(|item| visit(Self::Failure(&item.failure)));
565 }
566 Witness::CountHigh { per_value, .. } => per_value
567 .iter()
568 .for_each(|(_, child)| visit(Self::Failure(child))),
569 Witness::Atom { .. }
570 | Witness::Relational { .. }
571 | Witness::Closed { .. }
572 | Witness::Opaque { .. } => {}
573 },
574 Self::Satisfaction(value) => match value {
575 SatTrace::AllHeld { children, .. } => children
576 .iter()
577 .for_each(|child| visit(Self::Satisfaction(child))),
578 SatTrace::AnyHeld { satisfied, .. } => satisfied
579 .iter()
580 .for_each(|child| visit(Self::Satisfaction(child))),
581 SatTrace::CountHeld { matches, .. } => matches
582 .iter()
583 .for_each(|(_, _, child)| visit(Self::Satisfaction(child))),
584 SatTrace::ForAllHeld { values, .. } => values
585 .iter()
586 .for_each(|(_, _, child)| visit(Self::Satisfaction(child))),
587 SatTrace::NotHeld { inner_fails, .. } => visit(Self::Failure(inner_fails)),
588 SatTrace::Irrefutable { .. }
589 | SatTrace::Atom { .. }
590 | SatTrace::Blocked { .. }
591 | SatTrace::Coinductive { .. } => {}
592 },
593 }
594 }
595}
596
597#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
605pub struct MissingObligation {
606 pub constraint_id: ShapeId,
607 pub node: Term,
609 pub path: Path,
611 pub qualifier: ShapeId,
614 pub observed_count: u64,
615 pub required_count: u64,
616 pub missing: u64,
617}
618
619fn dedup_stable<T: Clone + Eq + std::hash::Hash>(values: Vec<T>) -> Vec<T> {
620 let mut seen = HashSet::new();
621 values
622 .into_iter()
623 .filter(|value| seen.insert(value.clone()))
624 .collect()
625}
626
627fn failure_constraint_id(value: &Witness) -> ShapeId {
628 match value {
629 Witness::Atom { shape, .. }
630 | Witness::Relational { shape, .. }
631 | Witness::Closed { shape, .. }
632 | Witness::Not { shape, .. }
633 | Witness::All { shape, .. }
634 | Witness::Any { shape, .. }
635 | Witness::CountLow { shape, .. }
636 | Witness::CountHigh { shape, .. }
637 | Witness::Opaque { shape, .. } => *shape,
638 }
639}
640
641fn satisfaction_constraint_id(value: &SatTrace) -> ShapeId {
642 match value {
643 SatTrace::Irrefutable { shape }
644 | SatTrace::Atom { shape, .. }
645 | SatTrace::AllHeld { shape, .. }
646 | SatTrace::AnyHeld { shape, .. }
647 | SatTrace::CountHeld { shape, .. }
648 | SatTrace::ForAllHeld { shape, .. }
649 | SatTrace::NotHeld { shape, .. }
650 | SatTrace::Blocked { shape, .. }
651 | SatTrace::Coinductive { shape, .. } => *shape,
652 }
653}
654
655fn failure_node(value: &Witness) -> &Term {
656 match value {
657 Witness::Atom { node, .. }
658 | Witness::Relational { node, .. }
659 | Witness::Closed { node, .. }
660 | Witness::Not { node, .. }
661 | Witness::All { node, .. }
662 | Witness::Any { node, .. }
663 | Witness::CountLow { node, .. }
664 | Witness::CountHigh { node, .. }
665 | Witness::Opaque { node, .. } => node,
666 }
667}
668
669fn satisfaction_node(value: &SatTrace) -> Option<&Term> {
670 match value {
671 SatTrace::Irrefutable { .. } => None,
672 SatTrace::Atom { node, .. }
673 | SatTrace::AllHeld { node, .. }
674 | SatTrace::AnyHeld { node, .. }
675 | SatTrace::CountHeld { node, .. }
676 | SatTrace::ForAllHeld { node, .. }
677 | SatTrace::NotHeld { node, .. }
678 | SatTrace::Blocked { node, .. }
679 | SatTrace::Coinductive { node, .. } => Some(node),
680 }
681}
682
683fn walk_evidence<'a>(value: &'a Evidence, out: &mut Vec<EvidenceNodeRef<'a>>) {
684 let root = match value {
685 Evidence::Satisfaction(value) => EvidenceNodeRef::Satisfaction(value),
686 Evidence::Failure(value) => EvidenceNodeRef::Failure(value),
687 };
688 walk_node(root, out);
689}
690
691fn walk_node<'a>(value: EvidenceNodeRef<'a>, out: &mut Vec<EvidenceNodeRef<'a>>) {
692 out.push(value);
693 value.for_each_child(|child| walk_node(child, out));
694}
695
696fn support_triples(value: &PathSupport, out: &mut Vec<Triple>) {
697 match value {
698 PathSupport::Empty => {}
699 PathSupport::Edge(triple) => out.push(triple.clone()),
700 PathSupport::Chain(children) | PathSupport::Alt(children) => {
701 children
702 .iter()
703 .for_each(|child| support_triples(child, out));
704 }
705 }
706}
707
708fn collect_support(value: &Evidence, out: &mut Vec<Triple>) {
709 for node in value.walk() {
710 match node {
711 EvidenceNodeRef::Satisfaction(SatTrace::Atom { produced_by, .. }) => {
712 support_triples(produced_by, out);
713 }
714 EvidenceNodeRef::Satisfaction(SatTrace::CountHeld { matches, .. }) => matches
715 .iter()
716 .for_each(|(_, support, _)| support_triples(support, out)),
717 EvidenceNodeRef::Satisfaction(SatTrace::ForAllHeld { values, .. }) => values
718 .iter()
719 .for_each(|(_, support, _)| support_triples(support, out)),
720 EvidenceNodeRef::Failure(Witness::Atom {
721 produced_by: Some(support),
722 ..
723 }) => support_triples(support, out),
724 EvidenceNodeRef::Failure(Witness::Relational { lhs, rhs, .. }) => lhs
725 .iter()
726 .chain(rhs)
727 .for_each(|(_, support)| support_triples(support, out)),
728 EvidenceNodeRef::Failure(Witness::CountLow {
729 qualifying_matches,
730 rejected_candidates,
731 ..
732 }) => {
733 qualifying_matches
734 .iter()
735 .for_each(|item| support_triples(&item.path_support, out));
736 rejected_candidates
737 .iter()
738 .for_each(|item| support_triples(&item.path_support, out));
739 }
740 EvidenceNodeRef::Failure(Witness::CountHigh { matched, .. }) => matched
741 .iter()
742 .for_each(|(_, support)| support_triples(support, out)),
743 _ => {}
744 }
745 }
746}
747
748fn collect_path_supports(value: &Evidence, out: &mut Vec<PathSupport>) {
749 for node in value.walk() {
750 match node {
751 EvidenceNodeRef::Satisfaction(SatTrace::Atom { produced_by, .. }) => {
752 out.push(produced_by.clone());
753 }
754 EvidenceNodeRef::Satisfaction(SatTrace::CountHeld { matches, .. }) => {
755 out.extend(matches.iter().map(|(_, support, _)| support.clone()));
756 }
757 EvidenceNodeRef::Satisfaction(SatTrace::ForAllHeld { values, .. }) => {
758 out.extend(values.iter().map(|(_, support, _)| support.clone()));
759 }
760 EvidenceNodeRef::Failure(Witness::Atom {
761 produced_by: Some(support),
762 ..
763 }) => out.push(support.clone()),
764 EvidenceNodeRef::Failure(Witness::Relational { lhs, rhs, .. }) => {
765 out.extend(lhs.iter().chain(rhs).map(|(_, support)| support.clone()))
766 }
767 EvidenceNodeRef::Failure(Witness::CountLow {
768 qualifying_matches,
769 rejected_candidates,
770 ..
771 }) => {
772 out.extend(
773 qualifying_matches
774 .iter()
775 .map(|item| item.path_support.clone()),
776 );
777 out.extend(
778 rejected_candidates
779 .iter()
780 .map(|item| item.path_support.clone()),
781 );
782 }
783 EvidenceNodeRef::Failure(Witness::CountHigh { matched, .. }) => {
784 out.extend(matched.iter().map(|(_, support)| support.clone()));
785 }
786 _ => {}
787 }
788 }
789}
790
791fn collect_matched(value: &Evidence, out: &mut Vec<Term>) {
792 for node in value.walk() {
793 match node {
794 EvidenceNodeRef::Satisfaction(SatTrace::CountHeld { matches, .. }) => {
795 out.extend(matches.iter().map(|(value, _, _)| value.clone()));
796 }
797 EvidenceNodeRef::Satisfaction(SatTrace::ForAllHeld { values, .. }) => {
798 out.extend(values.iter().map(|(value, _, _)| value.clone()));
799 }
800 EvidenceNodeRef::Failure(Witness::CountLow {
801 qualifying_matches, ..
802 }) => out.extend(qualifying_matches.iter().map(|item| item.value.clone())),
803 EvidenceNodeRef::Failure(Witness::CountHigh { matched, .. }) => {
804 out.extend(matched.iter().map(|(value, _)| value.clone()));
805 }
806 _ => {}
807 }
808 }
809}
810
811fn collect_matched_by_path<'a>(value: &'a Evidence, out: &mut Vec<(&'a Path, &'a Term)>) {
814 for node in value.walk() {
815 match node {
816 EvidenceNodeRef::Satisfaction(SatTrace::CountHeld { path, matches, .. }) => {
817 out.extend(matches.iter().map(|(value, _, _)| (path, value)));
818 }
819 EvidenceNodeRef::Satisfaction(SatTrace::ForAllHeld { path, values, .. }) => {
820 out.extend(values.iter().map(|(value, _, _)| (path, value)));
821 }
822 EvidenceNodeRef::Failure(Witness::CountLow {
823 path,
824 qualifying_matches,
825 ..
826 }) => out.extend(qualifying_matches.iter().map(|item| (path, &item.value))),
827 EvidenceNodeRef::Failure(Witness::CountHigh { path, matched, .. }) => {
828 out.extend(matched.iter().map(|(value, _)| (path, value)));
829 }
830 _ => {}
831 }
832 }
833}
834
835fn collect_missing(value: &Evidence, out: &mut Vec<MissingObligation>) {
836 for node in value.walk() {
837 if let EvidenceNodeRef::Failure(Witness::CountLow {
838 shape,
839 node,
840 path,
841 qualifier,
842 have,
843 min,
844 ..
845 }) = node
846 {
847 out.push(MissingObligation {
848 constraint_id: *shape,
849 node: node.clone(),
850 path: path.clone(),
851 qualifier: *qualifier,
852 observed_count: *have,
853 required_count: *min,
854 missing: min - have,
855 });
856 }
857 }
858}
859
860fn collect_offending(value: &Evidence, out: &mut Vec<Term>) {
861 for node in value.walk() {
862 match node {
863 EvidenceNodeRef::Failure(Witness::Atom { node, .. }) => out.push(node.clone()),
864 EvidenceNodeRef::Failure(Witness::Closed { offenders, .. }) => {
865 out.extend(offenders.iter().map(|(_, value)| value.clone()));
866 }
867 EvidenceNodeRef::Failure(Witness::Relational { offending, .. }) => {
868 out.extend(
869 offending
870 .iter()
871 .flat_map(|(left, right)| [left.clone(), right.clone()]),
872 );
873 }
874 EvidenceNodeRef::Failure(Witness::CountLow {
875 rejected_candidates,
876 ..
877 }) => out.extend(rejected_candidates.iter().map(|item| item.value.clone())),
878 EvidenceNodeRef::Failure(Witness::CountHigh { excess_values, .. }) => {
879 out.extend(excess_values.iter().map(|(value, _)| value.clone()));
880 }
881 _ => {}
882 }
883 }
884}
885
886#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
889pub struct ConstraintRecord {
890 pub id: ShapeId,
891 pub constraint_kind: ConstraintKind,
892 pub constraint: Shape,
893}
894
895#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
896pub struct ConstraintCatalog {
897 pub source: Vec<ConstraintRecord>,
898 pub normalized: Vec<ConstraintRecord>,
899}
900
901#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
902pub struct EvidenceSummary {
903 pub constraint_id: ShapeId,
904 pub constraint_kind: ConstraintKind,
905 pub status: EvaluationStatus,
906}
907
908#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
909pub struct ChildEvaluation {
910 pub source_constraint_ref: ShapeId,
911 pub normalized_constraint_ref: Option<ShapeId>,
912 pub status: EvaluationStatus,
913 pub evidence_summary: EvidenceSummary,
914}
915
916#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
917pub struct EvaluationProgress {
918 pub evaluated_children: Vec<ChildEvaluation>,
919}
920
921#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
924pub struct FocusEvaluation {
925 pub focus: Term,
926 pub evidence: Evidence,
927 #[serde(default, skip_serializing_if = "Option::is_none")]
928 pub progress: Option<EvaluationProgress>,
929}
930
931impl FocusEvaluation {
932 pub fn status(&self) -> EvaluationStatus {
933 self.evidence.status()
934 }
935
936 pub fn explain(&self) -> String {
937 self.evidence.explain()
938 }
939}
940
941#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
944pub struct StatementEvaluation {
945 pub source_statement_id: usize,
946 pub normalized_statement_id: Option<usize>,
947 pub source_constraint_id: ShapeId,
948 pub normalized_constraint_id: Option<ShapeId>,
949 pub constraint_kind: ConstraintKind,
950 pub constraint: Shape,
951 pub selector: Selector,
952 pub selected_foci: Vec<FocusEvaluation>,
953}
954
955impl StatementEvaluation {
956 pub fn source_constraints(&self) -> Vec<ShapeId> {
959 let mut values = vec![self.source_constraint_id];
960 for focus in &self.selected_foci {
961 if let Some(progress) = &focus.progress {
962 values.extend(
963 progress
964 .evaluated_children
965 .iter()
966 .map(|child| child.source_constraint_ref),
967 );
968 }
969 }
970 dedup_stable(values)
971 }
972}
973
974#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
976pub struct EvidenceRun {
977 pub conforms: bool,
978 pub constraints: ConstraintCatalog,
979 pub statements: Vec<StatementEvaluation>,
980}
981
982impl EvidenceRun {
983 pub fn to_json(&self) -> serde_json::Result<String> {
984 serde_json::to_string(self)
985 }
986
987 pub fn walk(&self) -> Vec<EvidenceNodeRef<'_>> {
988 self.statements
989 .iter()
990 .flat_map(|statement| &statement.selected_foci)
991 .flat_map(|focus| focus.evidence.walk())
992 .collect()
993 }
994}
995
996#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
998pub enum BlockReason {
999 OpaqueSparql,
1000 ClosedNeedsAdd,
1002 Unsupported,
1004}
1005
1006type Stack = HashSet<(ShapeId, Term)>;
1007
1008fn prepare(context: &Graph, schema: &Schema) -> Result<SparqlExecutor, NonStratifiable> {
1015 let strat = analyze(&schema.arena);
1016 if !strat.stratifiable {
1017 let components = strat
1018 .strata
1019 .iter()
1020 .filter(|s| !s.stratifiable)
1021 .map(|s| s.shapes.clone())
1022 .collect();
1023 return Err(NonStratifiable { components });
1024 }
1025
1026 let uses_shapes = uses_shapes_graph(&schema.arena);
1027 let frozen = if uses_shapes {
1028 FrozenIndexedDataset::from_graphs(context, context)
1029 } else {
1030 FrozenIndexedDataset::from_graph(context)
1031 };
1032 Ok(SparqlExecutor::from_frozen(frozen, uses_shapes))
1033}
1034
1035pub fn witness_violations(
1041 data: &Graph,
1042 context: &Graph,
1043 schema: &Schema,
1044) -> Result<Vec<FocusWitness>, NonStratifiable> {
1045 let sparql = prepare(context, schema)?;
1046 let backend = sparql
1047 .frozen()
1048 .expect("witness executor always has a frozen dataset");
1049 let mut evaluator = ShapeEvaluator::new(backend, &schema.arena, &schema.prefixes, &sparql);
1050
1051 let mut out = Vec::new();
1052 for (i, st) in schema.statements.iter().enumerate() {
1053 for v in focus_nodes_with(data, backend, &st.selector, &schema.arena, &sparql) {
1054 let mut stack = Stack::new();
1055 if let Some(failure) = witness(
1056 &mut evaluator,
1057 &v,
1058 st.shape,
1059 &Path::Id,
1060 None,
1061 &[],
1062 &mut stack,
1063 ) {
1064 out.push(FocusWitness {
1065 focus: v,
1066 statement: i,
1067 failure,
1068 });
1069 }
1070 }
1071 }
1072 Ok(out)
1073}
1074
1075pub fn shape_id_for_iri(schema: &Schema, iri: &str) -> Option<ShapeId> {
1078 schema
1079 .names
1080 .iter()
1081 .find_map(|(id, names)| names.iter().any(|name| name == iri).then_some(*id))
1082}
1083
1084pub fn witness_shape(
1090 data: &Graph,
1091 context: &Graph,
1092 schema: &Schema,
1093 shape: ShapeId,
1094) -> Result<Vec<FocusWitness>, NonStratifiable> {
1095 let sparql = prepare(context, schema)?;
1096 let backend = sparql
1097 .frozen()
1098 .expect("witness executor always has a frozen dataset");
1099 let mut evaluator = ShapeEvaluator::new(backend, &schema.arena, &schema.prefixes, &sparql);
1100
1101 let mut out = Vec::new();
1102 for (i, st) in schema.statements.iter().enumerate() {
1103 if st.shape != shape {
1104 continue;
1105 }
1106 for v in focus_nodes_with(data, backend, &st.selector, &schema.arena, &sparql) {
1107 let mut stack = Stack::new();
1108 if let Some(failure) = witness(
1109 &mut evaluator,
1110 &v,
1111 st.shape,
1112 &Path::Id,
1113 None,
1114 &[],
1115 &mut stack,
1116 ) {
1117 out.push(FocusWitness {
1118 focus: v,
1119 statement: i,
1120 failure,
1121 });
1122 }
1123 }
1124 }
1125 Ok(out)
1126}
1127
1128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1132pub struct FocusSat {
1133 pub focus: Term,
1134 pub statement: usize,
1136 pub trace: SatTrace,
1137}
1138
1139pub fn satisfy_shape(
1144 data: &Graph,
1145 context: &Graph,
1146 schema: &Schema,
1147 shape: ShapeId,
1148) -> Result<Vec<FocusSat>, NonStratifiable> {
1149 let sparql = prepare(context, schema)?;
1150 let backend = sparql
1151 .frozen()
1152 .expect("witness executor always has a frozen dataset");
1153 let mut evaluator = ShapeEvaluator::new(backend, &schema.arena, &schema.prefixes, &sparql);
1154
1155 let mut out = Vec::new();
1156 for (i, st) in schema.statements.iter().enumerate() {
1157 if st.shape != shape {
1158 continue;
1159 }
1160 for v in focus_nodes_with(data, backend, &st.selector, &schema.arena, &sparql) {
1161 let mut stack = Stack::new();
1162 if let Some(trace) =
1163 sat_trace(&mut evaluator, &v, st.shape, &Path::Id, None, &mut stack)
1164 {
1165 out.push(FocusSat {
1166 focus: v,
1167 statement: i,
1168 trace,
1169 });
1170 }
1171 }
1172 }
1173 Ok(out)
1174}
1175
1176pub fn witness_node(
1188 context: &Graph,
1189 schema: &Schema,
1190 node: &Term,
1191 shape: ShapeId,
1192) -> Result<Option<FocusWitness>, NonStratifiable> {
1193 let sparql = prepare(context, schema)?;
1194 let backend = sparql
1195 .frozen()
1196 .expect("witness executor always has a frozen dataset");
1197 let mut evaluator = ShapeEvaluator::new(backend, &schema.arena, &schema.prefixes, &sparql);
1198
1199 let mut stack = Stack::new();
1200 Ok(witness(
1201 &mut evaluator,
1202 node,
1203 shape,
1204 &Path::Id,
1205 None,
1206 &[],
1207 &mut stack,
1208 )
1209 .map(|failure| FocusWitness {
1210 focus: node.clone(),
1211 statement: usize::MAX,
1212 failure,
1213 }))
1214}
1215
1216fn witness(
1227 eval: &mut ShapeEvaluator<'_>,
1228 node: &Term,
1229 id: ShapeId,
1230 reached_by: &Path,
1231 produced_by: Option<&PathSupport>,
1232 scope: &[(Path, ShapeId)],
1233 stack: &mut Stack,
1234) -> Option<Witness> {
1235 match evaluate_node(eval, node, id, reached_by, produced_by, scope, stack) {
1236 Evidence::Failure(value) => Some(value),
1237 Evidence::Satisfaction(_) => None,
1238 }
1239}
1240
1241fn peel_annotated(arena: &ShapeArena, mut id: ShapeId) -> ShapeId {
1243 while let Shape::Annotated { shape, .. } = arena.get(id) {
1244 id = *shape;
1245 }
1246 id
1247}
1248
1249fn as_universal(arena: &ShapeArena, id: ShapeId) -> Option<(Path, ShapeId)> {
1254 let Shape::Count {
1255 path,
1256 max: Some(0),
1257 qualifier,
1258 ..
1259 } = arena.get(peel_annotated(arena, id)).clone()
1260 else {
1261 return None;
1262 };
1263 match arena.get(peel_annotated(arena, qualifier)) {
1264 Shape::Not(inner) => Some((path, *inner)),
1265 _ => None,
1266 }
1267}
1268
1269fn relational_witness(
1270 eval: &ShapeEvaluator<'_>,
1271 node: &Term,
1272 id: ShapeId,
1273 shape: &Shape,
1274) -> Witness {
1275 let g = eval.backend();
1276 let (kind, lpath, rpred) = match shape {
1277 Shape::Eq(p, q) => (RelKind::Eq, p.clone(), Some(q.clone())),
1278 Shape::Disj(p, q) => (RelKind::Disj, p.clone(), Some(q.clone())),
1279 Shape::Lt(p, q) => (RelKind::Lt, p.clone(), Some(q.clone())),
1280 Shape::Le(p, q) => (RelKind::Le, p.clone(), Some(q.clone())),
1281 Shape::UniqueLang(p) => (RelKind::UniqueLang, p.clone(), None),
1282 _ => unreachable!("relational_witness on non-relational shape"),
1283 };
1284 let with_support = |g: &dyn PathBackend, p: &Path| -> Vec<(Term, PathSupport)> {
1285 succ_with_support(g, node, p)
1286 };
1287 let lhs = with_support(g, &lpath);
1288 let rhs = match &rpred {
1289 Some(q) => with_support(g, &Path::Pred(q.clone())),
1290 None => Vec::new(),
1291 };
1292 let offending = offending_pairs(kind, &lhs, &rhs);
1293 Witness::Relational {
1294 shape: id,
1295 node: node.clone(),
1296 kind,
1297 lhs,
1298 rhs,
1299 offending,
1300 }
1301}
1302
1303fn offending_pairs(
1305 kind: RelKind,
1306 lhs: &[(Term, PathSupport)],
1307 rhs: &[(Term, PathSupport)],
1308) -> Vec<(Term, Term)> {
1309 let lvals: Vec<&Term> = lhs.iter().map(|(v, _)| v).collect();
1310 let rvals: Vec<&Term> = rhs.iter().map(|(v, _)| v).collect();
1311 match kind {
1312 RelKind::Eq => lvals
1313 .iter()
1314 .filter(|v| !rvals.contains(v))
1315 .chain(rvals.iter().filter(|v| !lvals.contains(v)))
1316 .map(|v| ((*v).clone(), (*v).clone()))
1317 .collect(),
1318 RelKind::Disj => lvals
1319 .iter()
1320 .filter(|v| rvals.contains(v))
1321 .map(|v| ((*v).clone(), (*v).clone()))
1322 .collect(),
1323 RelKind::Lt | RelKind::Le => {
1324 let mut bad = Vec::new();
1326 for a in &lvals {
1327 for b in &rvals {
1328 let ok = match crate::value::compare_terms(a, b) {
1329 Some(std::cmp::Ordering::Less) => true,
1330 Some(std::cmp::Ordering::Equal) => kind == RelKind::Le,
1331 _ => false,
1332 };
1333 if !ok {
1334 bad.push(((*a).clone(), (*b).clone()));
1335 }
1336 }
1337 }
1338 bad
1339 }
1340 RelKind::UniqueLang => {
1341 let mut bad = Vec::new();
1343 for i in 0..lvals.len() {
1344 for j in (i + 1)..lvals.len() {
1345 if let (Term::Literal(a), Term::Literal(b)) = (lvals[i], lvals[j])
1346 && let (Some(la), Some(lb)) = (a.language(), b.language())
1347 && la.eq_ignore_ascii_case(lb)
1348 {
1349 bad.push((lvals[i].clone(), lvals[j].clone()));
1350 }
1351 }
1352 }
1353 bad
1354 }
1355 }
1356}
1357
1358fn sat_trace(
1360 eval: &mut ShapeEvaluator<'_>,
1361 node: &Term,
1362 id: ShapeId,
1363 reached_by: &Path,
1364 produced_by: Option<&PathSupport>,
1365 stack: &mut Stack,
1366) -> Option<SatTrace> {
1367 match evaluate_node(eval, node, id, reached_by, produced_by, &[], stack) {
1368 Evidence::Satisfaction(value) => Some(value),
1369 Evidence::Failure(_) => None,
1370 }
1371}
1372
1373fn evaluate_node(
1377 eval: &mut ShapeEvaluator<'_>,
1378 node: &Term,
1379 id: ShapeId,
1380 reached_by: &Path,
1381 produced_by: Option<&PathSupport>,
1382 scope: &[(Path, ShapeId)],
1383 stack: &mut Stack,
1384) -> Evidence {
1385 crate::profile::record_evidence_visit();
1386 let key = (id, node.clone());
1387 if !stack.insert(key.clone()) {
1388 return Evidence::Satisfaction(SatTrace::Coinductive {
1389 shape: id,
1390 node: node.clone(),
1391 });
1392 }
1393
1394 let shape = eval.arena().get(id).clone();
1395 let evidence = match shape {
1396 Shape::Annotated { shape, .. } => {
1397 evaluate_node(eval, node, shape, reached_by, produced_by, scope, stack)
1398 }
1399 Shape::Top | Shape::Pending => Evidence::Satisfaction(SatTrace::Irrefutable { shape: id }),
1400 Shape::TestConst(_) | Shape::TestType(_) | Shape::TestKind(_) => {
1401 if eval.holds(node, id) {
1402 Evidence::Satisfaction(SatTrace::Atom {
1403 shape: id,
1404 node: node.clone(),
1405 observed: vec![node.clone()],
1406 expected: Box::new(eval.arena().get(id).clone()),
1407 reached_by: reached_by.clone(),
1408 produced_by: produced_by.cloned().unwrap_or(PathSupport::Empty),
1409 })
1410 } else {
1411 Evidence::Failure(Witness::Atom {
1412 shape: id,
1413 node: node.clone(),
1414 observed: vec![node.clone()],
1415 expected: Box::new(eval.arena().get(id).clone()),
1416 reached_by: reached_by.clone(),
1417 produced_by: produced_by.cloned(),
1418 })
1419 }
1420 }
1421 Shape::Eq(..) | Shape::Disj(..) | Shape::Lt(..) | Shape::Le(..) | Shape::UniqueLang(_) => {
1422 if eval.holds(node, id) {
1423 Evidence::Satisfaction(SatTrace::Blocked {
1424 shape: id,
1425 node: node.clone(),
1426 reason: BlockReason::Unsupported,
1427 })
1428 } else {
1429 Evidence::Failure(relational_witness(eval, node, id, &shape))
1430 }
1431 }
1432 Shape::Closed(ref allowed) => {
1433 let offenders = closed_offenders(eval.backend(), node, allowed);
1434 if offenders.is_empty() {
1435 Evidence::Satisfaction(SatTrace::Blocked {
1436 shape: id,
1437 node: node.clone(),
1438 reason: BlockReason::ClosedNeedsAdd,
1439 })
1440 } else {
1441 Evidence::Failure(Witness::Closed {
1442 shape: id,
1443 node: node.clone(),
1444 offenders,
1445 })
1446 }
1447 }
1448 Shape::Sparql(constraint) => {
1449 if eval.holds(node, id) {
1450 Evidence::Satisfaction(SatTrace::Blocked {
1451 shape: id,
1452 node: node.clone(),
1453 reason: BlockReason::OpaqueSparql,
1454 })
1455 } else {
1456 let violations = eval
1457 .sparql()
1458 .constraint_violations(&constraint, node)
1459 .unwrap_or_default();
1460 let diagnostic = eval
1461 .sparql()
1462 .constraint_diagnostic(&constraint, node, &violations)
1463 .ok();
1464 Evidence::Failure(Witness::Opaque {
1465 shape: id,
1466 node: node.clone(),
1467 messages: constraint.messages,
1468 diagnostic,
1469 })
1470 }
1471 }
1472 Shape::Expression(_) => {
1473 if eval.holds(node, id) {
1474 Evidence::Satisfaction(SatTrace::Blocked {
1475 shape: id,
1476 node: node.clone(),
1477 reason: BlockReason::Unsupported,
1478 })
1479 } else {
1480 Evidence::Failure(Witness::Opaque {
1481 shape: id,
1482 node: node.clone(),
1483 messages: Vec::new(),
1484 diagnostic: None,
1485 })
1486 }
1487 }
1488 Shape::Not(child) => {
1489 match evaluate_node(eval, node, child, reached_by, produced_by, &[], stack) {
1490 Evidence::Failure(inner_fails) => Evidence::Satisfaction(SatTrace::NotHeld {
1491 shape: id,
1492 node: node.clone(),
1493 inner_fails: Box::new(inner_fails),
1494 }),
1495 Evidence::Satisfaction(inner) => Evidence::Failure(Witness::Not {
1496 shape: id,
1497 node: node.clone(),
1498 inner: Box::new(inner),
1499 }),
1500 }
1501 }
1502 Shape::And(children) => {
1503 let mut child_scope = scope.to_vec();
1504 child_scope.extend(
1505 children
1506 .iter()
1507 .filter_map(|&child| as_universal(eval.arena(), child)),
1508 );
1509 let evaluated: Vec<Evidence> = children
1510 .iter()
1511 .map(|&child| {
1512 evaluate_node(
1513 eval,
1514 node,
1515 child,
1516 reached_by,
1517 produced_by,
1518 &child_scope,
1519 stack,
1520 )
1521 })
1522 .collect();
1523 if evaluated
1524 .iter()
1525 .all(|item| matches!(item, Evidence::Satisfaction(_)))
1526 {
1527 Evidence::Satisfaction(SatTrace::AllHeld {
1528 shape: id,
1529 node: node.clone(),
1530 children: evaluated
1531 .into_iter()
1532 .map(|item| match item {
1533 Evidence::Satisfaction(value) => value,
1534 Evidence::Failure(_) => unreachable!(),
1535 })
1536 .collect(),
1537 })
1538 } else {
1539 Evidence::Failure(Witness::All {
1540 shape: id,
1541 node: node.clone(),
1542 failed: evaluated
1543 .into_iter()
1544 .filter_map(|item| match item {
1545 Evidence::Failure(value) => Some(value),
1546 Evidence::Satisfaction(_) => None,
1547 })
1548 .collect(),
1549 })
1550 }
1551 }
1552 Shape::Or(children) => {
1553 let evaluated: Vec<Evidence> = children
1554 .iter()
1555 .map(|&child| {
1556 evaluate_node(eval, node, child, reached_by, produced_by, scope, stack)
1557 })
1558 .collect();
1559 if evaluated
1560 .iter()
1561 .any(|item| matches!(item, Evidence::Satisfaction(_)))
1562 {
1563 Evidence::Satisfaction(SatTrace::AnyHeld {
1564 shape: id,
1565 node: node.clone(),
1566 satisfied: evaluated
1567 .into_iter()
1568 .filter_map(|item| match item {
1569 Evidence::Satisfaction(value) => Some(value),
1570 Evidence::Failure(_) => None,
1571 })
1572 .collect(),
1573 })
1574 } else {
1575 Evidence::Failure(Witness::Any {
1576 shape: id,
1577 node: node.clone(),
1578 branches: evaluated
1579 .into_iter()
1580 .map(|item| match item {
1581 Evidence::Failure(value) => value,
1582 Evidence::Satisfaction(_) => unreachable!(),
1583 })
1584 .collect(),
1585 })
1586 }
1587 }
1588 Shape::Count {
1589 path,
1590 min,
1591 max,
1592 qualifier,
1593 } => {
1594 let reached = Path::seq(vec![reached_by.clone(), path.clone()]);
1595 let mut matches = Vec::new();
1596 let mut rejected = Vec::new();
1597 for (value, support) in succ_with_support(eval.backend(), node, &path) {
1598 match evaluate_node(
1599 eval,
1600 &value,
1601 qualifier,
1602 &reached,
1603 Some(&support),
1604 &[],
1605 stack,
1606 ) {
1607 Evidence::Satisfaction(satisfaction) => matches.push(QualifiedMatch {
1608 value,
1609 path_support: support,
1610 satisfaction: Box::new(satisfaction),
1611 }),
1612 Evidence::Failure(failure) => rejected.push(RejectedCandidate {
1613 value,
1614 path_support: support,
1615 failure: Box::new(failure),
1616 }),
1617 }
1618 }
1619 let observed = matches.len() as u64;
1620 if min.is_some_and(|minimum| observed < minimum) {
1621 let minimum = min.expect("checked above");
1622 let sibling_qualifiers = scope
1623 .iter()
1624 .filter(|(candidate_path, _)| candidate_path == &path)
1625 .map(|(_, inner)| *inner)
1626 .collect();
1627 Evidence::Failure(Witness::CountLow {
1628 shape: id,
1629 node: node.clone(),
1630 path,
1631 qualifier,
1632 have: observed,
1633 min: minimum,
1634 qualifying_matches: matches,
1635 rejected_candidates: rejected,
1636 sibling_qualifiers,
1637 })
1638 } else if max.is_some_and(|maximum| observed > maximum) {
1639 let maximum = max.expect("checked above");
1640 let matched: Vec<(Term, PathSupport)> = matches
1641 .iter()
1642 .map(|item| (item.value.clone(), item.path_support.clone()))
1643 .collect();
1644 let excess_values = matched.iter().skip(maximum as usize).cloned().collect();
1645 let per_value = if maximum == 0 {
1646 matches
1647 .iter()
1648 .filter_map(|item| match item.satisfaction.as_ref() {
1649 SatTrace::NotHeld { inner_fails, .. } => {
1650 Some((item.value.clone(), (**inner_fails).clone()))
1651 }
1652 _ => None,
1653 })
1654 .collect()
1655 } else {
1656 Vec::new()
1657 };
1658 Evidence::Failure(Witness::CountHigh {
1659 shape: id,
1660 node: node.clone(),
1661 path,
1662 qualifier,
1663 matched,
1664 max: maximum,
1665 excess_values,
1666 per_value,
1667 })
1668 } else if min.is_none()
1669 && max == Some(0)
1670 && let Shape::Not(inner) = eval.arena().get(qualifier)
1671 {
1672 let values = rejected
1673 .into_iter()
1674 .filter_map(|candidate| match candidate.failure.as_ref() {
1675 Witness::Not { inner: trace, .. } => {
1676 Some((candidate.value, candidate.path_support, (**trace).clone()))
1677 }
1678 _ => None,
1679 })
1680 .collect();
1681 Evidence::Satisfaction(SatTrace::ForAllHeld {
1682 shape: id,
1683 node: node.clone(),
1684 path,
1685 qualifier: *inner,
1686 values,
1687 })
1688 } else {
1689 let trace_matches = matches
1690 .into_iter()
1691 .map(|item| (item.value, item.path_support, *item.satisfaction))
1692 .collect();
1693 Evidence::Satisfaction(SatTrace::CountHeld {
1694 shape: id,
1695 node: node.clone(),
1696 path,
1697 qualifier,
1698 matches: trace_matches,
1699 observed_count: observed,
1700 min,
1701 max,
1702 })
1703 }
1704 }
1705 };
1706 stack.remove(&key);
1707 evidence
1708}
1709
1710pub(crate) fn materialize_evidence(
1711 eval: &mut ShapeEvaluator<'_>,
1712 node: &Term,
1713 shape: ShapeId,
1714) -> Evidence {
1715 evaluate_node(eval, node, shape, &Path::Id, None, &[], &mut Stack::new())
1716}
1717
1718fn closed_offenders(
1720 g: &dyn PathBackend,
1721 node: &Term,
1722 q: &BTreeSet<NamedNode>,
1723) -> Vec<(NamedNode, Term)> {
1724 let allowed: HashSet<&NamedNode> = q.iter().collect();
1725 let mut out = Vec::new();
1726 for p in g.out_predicates(node) {
1730 if !allowed.contains(&p) {
1731 let mut objects: Vec<Term> = g.objects(node, &p).into_iter().collect();
1732 objects.sort_by(compare_terms);
1733 out.extend(objects.into_iter().map(|object| (p.clone(), object)));
1734 }
1735 }
1736 out
1737}
1738
1739fn succ_with_support(g: &dyn PathBackend, from: &Term, path: &Path) -> Vec<(Term, PathSupport)> {
1765 let mut values = succ_with_support_unordered(g, from, path);
1766 values.sort_by(|(left, _), (right, _)| compare_terms(left, right));
1767 values
1768}
1769
1770fn compare_terms(left: &Term, right: &Term) -> std::cmp::Ordering {
1777 fn rank(term: &Term) -> u8 {
1778 match term {
1779 Term::NamedNode(_) => 0,
1780 Term::BlankNode(_) => 1,
1781 Term::Literal(_) => 2,
1782 }
1783 }
1784 rank(left)
1785 .cmp(&rank(right))
1786 .then_with(|| match (left, right) {
1787 (Term::NamedNode(left), Term::NamedNode(right)) => left.as_str().cmp(right.as_str()),
1788 (Term::BlankNode(left), Term::BlankNode(right)) => left.as_str().cmp(right.as_str()),
1789 (Term::Literal(left), Term::Literal(right)) => left
1790 .value()
1791 .cmp(right.value())
1792 .then_with(|| left.datatype().as_str().cmp(right.datatype().as_str()))
1793 .then_with(|| left.language().cmp(&right.language())),
1794 _ => std::cmp::Ordering::Equal,
1795 })
1796}
1797
1798fn succ_with_support_unordered(
1799 g: &dyn PathBackend,
1800 from: &Term,
1801 path: &Path,
1802) -> Vec<(Term, PathSupport)> {
1803 match path {
1804 Path::Id => vec![(from.clone(), PathSupport::Empty)],
1805 Path::Pred(q) => {
1806 let Some(subject) = node_of(from) else {
1807 return Vec::new();
1808 };
1809 g.objects(from, q)
1810 .into_iter()
1811 .map(|object| {
1812 let edge = Triple::new(subject.clone(), q.clone(), object.clone());
1813 (object, PathSupport::Edge(edge))
1814 })
1815 .collect()
1816 }
1817 Path::Inverse(inner) => match inner.as_ref() {
1820 Path::Pred(q) => g
1821 .subjects(q, from)
1822 .into_iter()
1823 .filter_map(|subject| {
1824 let node = node_of(&subject)?;
1825 let edge = Triple::new(node, q.clone(), from.clone());
1826 Some((subject, PathSupport::Edge(edge)))
1827 })
1828 .collect(),
1829 _ => pred(g, from, inner)
1830 .into_iter()
1831 .map(|value| {
1832 let support = path_support(g, from, path, &value).unwrap_or(PathSupport::Empty);
1833 (value, support)
1834 })
1835 .collect(),
1836 },
1837 Path::Alt(branches) => {
1838 let mut out = Vec::new();
1839 let mut seen = HashSet::new();
1840 for branch in branches {
1841 for (value, support) in succ_with_support(g, from, branch) {
1842 if seen.insert(value.clone()) {
1843 out.push((value, support));
1844 }
1845 }
1846 }
1847 out
1848 }
1849 Path::Seq(steps) => {
1850 let Some((first, rest)) = steps.split_first() else {
1851 return vec![(from.clone(), PathSupport::Empty)];
1852 };
1853 let mut frontier: Vec<(Term, Vec<PathSupport>)> = Vec::new();
1854 let mut seen = HashSet::new();
1855 for (value, support) in succ_with_support(g, from, first) {
1856 if seen.insert(value.clone()) {
1857 frontier.push((value, vec![support]));
1858 }
1859 }
1860 for step in rest {
1861 let mut next: Vec<(Term, Vec<PathSupport>)> = Vec::new();
1862 let mut seen = HashSet::new();
1863 for (value, chain) in &frontier {
1864 for (reached, support) in succ_with_support(g, value, step) {
1865 if seen.insert(reached.clone()) {
1866 let mut chain = chain.clone();
1867 chain.push(support);
1868 next.push((reached, chain));
1869 }
1870 }
1871 }
1872 frontier = next;
1873 }
1874 frontier
1875 .into_iter()
1876 .map(|(value, chain)| (value, flatten_chain(chain)))
1877 .collect()
1878 }
1879 Path::Star(inner) => {
1882 let mut out = vec![(from.clone(), PathSupport::Empty)];
1883 let mut visited: HashSet<Term> = HashSet::from([from.clone()]);
1884 let mut queue: VecDeque<(Term, Vec<PathSupport>)> =
1885 VecDeque::from([(from.clone(), Vec::new())]);
1886 while let Some((current, chain)) = queue.pop_front() {
1887 for (next, support) in succ_with_support(g, ¤t, inner) {
1888 if !visited.insert(next.clone()) {
1889 continue;
1890 }
1891 let mut chain = chain.clone();
1892 chain.push(support);
1893 out.push((next.clone(), PathSupport::Chain(chain.clone())));
1894 queue.push_back((next, chain));
1895 }
1896 }
1897 out
1898 }
1899 }
1900}
1901
1902fn flatten_chain(parts: Vec<PathSupport>) -> PathSupport {
1904 let mut chain = Vec::with_capacity(parts.len());
1905 for part in parts {
1906 match part {
1907 PathSupport::Empty => {}
1908 PathSupport::Chain(inner) => chain.extend(inner),
1909 other => chain.push(other),
1910 }
1911 }
1912 if chain.is_empty() {
1913 PathSupport::Empty
1914 } else {
1915 PathSupport::Chain(chain)
1916 }
1917}
1918
1919fn path_support(g: &dyn PathBackend, from: &Term, path: &Path, to: &Term) -> Option<PathSupport> {
1922 crate::profile::record_path_probe();
1923 match path {
1924 Path::Id => (from == to).then_some(PathSupport::Empty),
1925 Path::Pred(q) => {
1926 if g.contains(from, q, to) {
1930 let s = node_of(from)?;
1931 Some(PathSupport::Edge(Triple::new(s, q.clone(), to.clone())))
1932 } else {
1933 None
1934 }
1935 }
1936 Path::Inverse(p) => path_support(g, to, p, from),
1937 Path::Alt(ps) => ps.iter().find_map(|p| path_support(g, from, p, to)),
1938 Path::Seq(ps) => seq_support(g, from, ps, to),
1939 Path::Star(p) => star_support(g, from, p, to),
1940 }
1941}
1942
1943fn seq_support(g: &dyn PathBackend, from: &Term, ps: &[Path], to: &Term) -> Option<PathSupport> {
1944 let Some((first, rest)) = ps.split_first() else {
1945 return (from == to).then_some(PathSupport::Empty);
1946 };
1947 for mid in succ(g, from, first) {
1948 let Some(head) = path_support(g, from, first, &mid) else {
1949 continue;
1950 };
1951 if let Some(tail) = seq_support(g, &mid, rest, to) {
1952 let mut chain = vec![head];
1953 match tail {
1954 PathSupport::Empty => {}
1955 PathSupport::Chain(v) => chain.extend(v),
1956 other => chain.push(other),
1957 }
1958 return Some(PathSupport::Chain(chain));
1959 }
1960 }
1961 None
1962}
1963
1964fn star_support(g: &dyn PathBackend, from: &Term, p: &Path, to: &Term) -> Option<PathSupport> {
1965 if from == to {
1966 return Some(PathSupport::Empty);
1967 }
1968 let mut visited: HashSet<Term> = HashSet::from([from.clone()]);
1969 let mut queue: VecDeque<(Term, Vec<PathSupport>)> =
1970 VecDeque::from([(from.clone(), Vec::new())]);
1971 while let Some((cur, chain)) = queue.pop_front() {
1972 for next in succ(g, &cur, p) {
1973 let Some(edge) = path_support(g, &cur, p, &next) else {
1974 continue;
1975 };
1976 let mut chain2 = chain.clone();
1977 chain2.push(edge);
1978 if next == *to {
1979 return Some(PathSupport::Chain(chain2));
1980 }
1981 if visited.insert(next.clone()) {
1982 queue.push_back((next, chain2));
1983 }
1984 }
1985 }
1986 None
1987}
1988
1989#[cfg(test)]
1990mod tests {
1991 use super::*;
1992 use shifty_parse::{load_turtle, parse_turtle};
1993
1994 const PREFIXES: &str = r#"
1995 @prefix sh: <http://www.w3.org/ns/shacl#> .
1996 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
1997 @prefix ex: <http://ex/> .
1998 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1999 "#;
2000
2001 fn run(ttl: &str) -> Vec<FocusWitness> {
2002 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
2003 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
2004 witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).expect("stratifiable")
2005 }
2006
2007 fn any(w: &Witness, pred: &impl Fn(&Witness) -> bool) -> bool {
2009 if pred(w) {
2010 return true;
2011 }
2012 match w {
2013 Witness::All { failed, .. } => failed.iter().any(|c| any(c, pred)),
2014 Witness::Any { branches, .. } => branches.iter().any(|c| any(c, pred)),
2015 Witness::CountHigh { per_value, .. } => per_value.iter().any(|(_, c)| any(c, pred)),
2016 _ => false,
2017 }
2018 }
2019
2020 #[test]
2021 fn conforming_graph_yields_no_witnesses() {
2022 let ttl = format!(
2023 "{PREFIXES}
2024 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2025 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
2026 ex:x ex:p ex:y .
2027 "
2028 );
2029 assert!(run(&ttl).is_empty());
2030 }
2031
2032 #[test]
2033 fn min_count_violation_is_count_low() {
2034 let ttl = format!(
2035 "{PREFIXES}
2036 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2037 sh:property [ sh:path ex:p ; sh:minCount 2 ] .
2038 ex:x ex:p ex:y .
2039 "
2040 );
2041 let ws = run(&ttl);
2042 assert_eq!(ws.len(), 1);
2043 assert_eq!(ws[0].focus.to_string(), "<http://ex/x>");
2044 assert!(any(&ws[0].failure, &|w| matches!(
2045 w,
2046 Witness::CountLow {
2047 have: 1,
2048 min: 2,
2049 ..
2050 }
2051 )));
2052 }
2053
2054 fn count_low_siblings(w: &Witness) -> Vec<Vec<ShapeId>> {
2056 fn go(w: &Witness, out: &mut Vec<Vec<ShapeId>>) {
2057 match w {
2058 Witness::CountLow {
2059 sibling_qualifiers, ..
2060 } => out.push(sibling_qualifiers.clone()),
2061 Witness::All { failed, .. } => failed.iter().for_each(|c| go(c, out)),
2062 Witness::Any { branches, .. } => branches.iter().for_each(|c| go(c, out)),
2063 Witness::CountHigh { per_value, .. } => {
2064 per_value.iter().for_each(|(_, c)| go(c, out))
2065 }
2066 _ => {}
2067 }
2068 }
2069 let mut out = Vec::new();
2070 go(w, &mut out);
2071 out
2072 }
2073
2074 #[test]
2075 fn cross_property_universals_attach_to_count_low() {
2076 let ttl = format!(
2081 "{PREFIXES}
2082 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2083 sh:property [ sh:path ex:p ; sh:minCount 1 ] ;
2084 sh:property [ sh:path ex:p ; sh:class ex:C ] .
2085 ex:x a ex:Thing .
2086 "
2087 );
2088 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
2089 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
2090 let ws =
2091 witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).expect("stratifiable");
2092 assert_eq!(ws.len(), 1);
2093 let sibs = count_low_siblings(&ws[0].failure);
2094 assert_eq!(sibs.len(), 1, "one CountLow");
2095 assert_eq!(sibs[0].len(), 1, "the class universal is attached");
2096 assert_eq!(
2097 class_of(sibs[0][0], &parsed.schema.arena),
2098 Some("http://ex/C".to_string()),
2099 );
2100 }
2101
2102 #[test]
2103 fn disjoint_or_branch_universal_does_not_attach() {
2104 let ttl = format!(
2109 "{PREFIXES}
2110 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2111 sh:or (
2112 [ sh:path ex:p ; sh:minCount 2 ]
2113 [ sh:path ex:p ; sh:class ex:C ]
2114 ) .
2115 ex:x a ex:Thing ; ex:p ex:y .
2116 "
2117 );
2118 let ws = run(&ttl);
2119 assert_eq!(ws.len(), 1);
2120 let sibs = count_low_siblings(&ws[0].failure);
2121 assert!(!sibs.is_empty(), "the count branch yields a CountLow");
2122 assert!(
2123 sibs.iter().all(|s| s.is_empty()),
2124 "no universal leaks across the disjunction: {sibs:?}",
2125 );
2126 }
2127
2128 fn class_of(id: ShapeId, arena: &ShapeArena) -> Option<String> {
2130 match shifty_algebra::render::class_target_shape(id, arena)? {
2131 Term::NamedNode(n) => Some(n.as_str().to_string()),
2132 _ => None,
2133 }
2134 }
2135
2136 #[test]
2137 fn datatype_violation_is_an_atom_with_support() {
2138 let ttl = format!(
2139 "{PREFIXES}
2140 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2141 sh:property [ sh:path ex:p ; sh:datatype xsd:integer ] .
2142 ex:x ex:p \"hello\" .
2143 "
2144 );
2145 let ws = run(&ttl);
2146 assert_eq!(ws.len(), 1);
2147 assert!(any(&ws[0].failure, &|w| matches!(
2149 w,
2150 Witness::Atom {
2151 produced_by: Some(PathSupport::Edge(_)),
2152 ..
2153 }
2154 )));
2155 }
2156
2157 #[test]
2158 fn focus_level_nodekind_atom_has_no_support() {
2159 let ttl = format!(
2160 "{PREFIXES}
2161 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2162 sh:nodeKind sh:IRI .
2163 ex:x ex:p \"v\" .
2164 ex:y ex:q ex:x .
2165 "
2166 );
2167 let _ = ttl;
2169 let ttl2 = format!(
2170 "{PREFIXES}
2171 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2172 sh:nodeKind sh:Literal .
2173 ex:x ex:p ex:y .
2174 "
2175 );
2176 let ws = run(&ttl2);
2177 assert_eq!(ws.len(), 1);
2178 assert!(matches!(
2179 ws[0].failure,
2180 Witness::Atom {
2181 produced_by: None,
2182 ..
2183 }
2184 ));
2185 }
2186
2187 #[test]
2188 fn alternative_path_support_is_one_positive_certificate() {
2189 let ttl = format!(
2190 "{PREFIXES}
2191 ex:x ex:p ex:y ; ex:q ex:y ."
2192 );
2193 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
2194 let x = Term::NamedNode(NamedNode::new("http://ex/x").unwrap());
2195 let y = Term::NamedNode(NamedNode::new("http://ex/y").unwrap());
2196 let path = Path::Alt(vec![
2197 Path::Pred(NamedNode::new("http://ex/p").unwrap()),
2198 Path::Pred(NamedNode::new("http://ex/q").unwrap()),
2199 ]);
2200 let support = path_support(&loaded.graph, &x, &path, &y).expect("reachable");
2201 let PathSupport::Edge(edge) = support else {
2202 panic!("Alt retains one successful route, got {support:?}")
2203 };
2204 assert!(loaded.graph.contains(edge.as_ref()));
2205 }
2206
2207 fn collect_support_edges(support: &PathSupport, out: &mut Vec<Triple>) {
2208 match support {
2209 PathSupport::Edge(edge) => out.push(edge.clone()),
2210 PathSupport::Chain(parts) | PathSupport::Alt(parts) => {
2211 parts
2212 .iter()
2213 .for_each(|part| collect_support_edges(part, out));
2214 }
2215 PathSupport::Empty => {}
2216 }
2217 }
2218
2219 #[test]
2220 fn traversal_certificates_match_per_value_probing() {
2221 let ttl = format!(
2225 "{PREFIXES}
2226 ex:x ex:p ex:a ; ex:p ex:b ; ex:q ex:c .
2227 ex:a ex:r ex:d . ex:d ex:r ex:e .
2228 ex:b rdf:type ex:C .
2229 ex:C <http://www.w3.org/2000/01/rdf-schema#subClassOf> ex:D .
2230 ex:D <http://www.w3.org/2000/01/rdf-schema#subClassOf> ex:E .
2231 ex:back ex:p ex:x ."
2232 );
2233 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
2234 let graph = &loaded.graph;
2235 let x = Term::NamedNode(NamedNode::new("http://ex/x").unwrap());
2236 let named = |iri: &str| NamedNode::new(iri).unwrap();
2237 let paths = vec![
2238 Path::Id,
2239 Path::Pred(named("http://ex/p")),
2240 Path::Inverse(Box::new(Path::Pred(named("http://ex/p")))),
2241 Path::Alt(vec![
2242 Path::Pred(named("http://ex/p")),
2243 Path::Pred(named("http://ex/q")),
2244 ]),
2245 Path::Seq(vec![
2246 Path::Pred(named("http://ex/p")),
2247 Path::Pred(named("http://ex/r")),
2248 ]),
2249 Path::Star(Path::Pred(named("http://ex/r")).into()),
2250 Path::Seq(vec![
2251 Path::Pred(named("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")),
2252 Path::Star(
2253 Path::Pred(named("http://www.w3.org/2000/01/rdf-schema#subClassOf")).into(),
2254 ),
2255 ]),
2256 ];
2257
2258 for path in paths {
2259 let derived = succ_with_support(graph, &x, &path);
2260 let values: HashSet<Term> = derived.iter().map(|(v, _)| v.clone()).collect();
2261 assert_eq!(values, succ(graph, &x, &path), "values differ for {path:?}");
2262 assert_eq!(derived.len(), values.len(), "duplicate value for {path:?}");
2263 for (value, support) in &derived {
2264 assert!(
2265 path_support(graph, &x, &path, value).is_some(),
2266 "probing rejects {value:?} for {path:?}"
2267 );
2268 let mut edges = Vec::new();
2269 collect_support_edges(support, &mut edges);
2270 for edge in edges {
2271 assert!(
2272 graph.contains(edge.as_ref()),
2273 "certificate cites a missing triple for {path:?}"
2274 );
2275 }
2276 }
2277 }
2278 }
2279
2280 #[test]
2281 fn non_stratifiable_schema_is_diagnosed() {
2282 let ttl = format!(
2283 "{PREFIXES}
2284 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2285 sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ; sh:qualifiedMinCount 1 ] .
2286 ex:x ex:p ex:y .
2287 "
2288 );
2289 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
2290 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
2291 assert!(witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).is_err());
2292 }
2293
2294 #[test]
2295 fn witness_shape_and_satisfy_shape_scope_to_one_shape() {
2296 let ttl = format!(
2299 "{PREFIXES}
2300 ex:S a sh:NodeShape ; sh:targetClass ex:C ;
2301 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
2302 ex:T a sh:NodeShape ; sh:targetClass ex:D ;
2303 sh:property [ sh:path ex:q ; sh:minCount 1 ] .
2304 ex:good a ex:C ; ex:p ex:y .
2305 ex:bad a ex:C .
2306 ex:other a ex:D .
2307 "
2308 );
2309 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
2310 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
2311 let schema = &parsed.schema;
2312
2313 let s = shape_id_for_iri(schema, "http://ex/S").expect("ex:S is named");
2314 assert!(shape_id_for_iri(schema, "http://ex/missing").is_none());
2315
2316 let fails = witness_shape(&loaded.graph, &loaded.graph, schema, s).expect("stratifiable");
2318 assert_eq!(fails.len(), 1);
2319 assert_eq!(fails[0].focus.to_string(), "<http://ex/bad>");
2320
2321 let sats = satisfy_shape(&loaded.graph, &loaded.graph, schema, s).expect("stratifiable");
2323 assert_eq!(sats.len(), 1);
2324 assert_eq!(sats[0].focus.to_string(), "<http://ex/good>");
2325 assert!(any_sat(&sats[0].trace, &|t| matches!(
2327 t,
2328 SatTrace::CountHeld { matches, .. } if matches.iter().any(|(v, _, _)| v.to_string() == "<http://ex/y>")
2329 )));
2330 }
2331
2332 fn pred_path(iri: &str) -> Path {
2333 Path::Pred(NamedNode::new_unchecked(iri))
2334 }
2335
2336 fn terms(values: &[Term]) -> Vec<String> {
2337 values.iter().map(|value| value.to_string()).collect()
2338 }
2339
2340 #[test]
2341 fn satisfaction_groups_matched_values_by_the_path_they_were_counted_along() {
2342 let ttl = format!(
2343 "{PREFIXES}
2344 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2345 sh:property [ sh:path ex:p ; sh:minCount 1 ] ;
2346 sh:property [ sh:path ex:q ; sh:minCount 1 ] .
2347 ex:x ex:p ex:y1, ex:y2 ; ex:q ex:z .
2348 "
2349 );
2350 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
2351 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
2352 let schema = &parsed.schema;
2353 let s = shape_id_for_iri(schema, "http://ex/S").expect("ex:S is named");
2354 let sats = satisfy_shape(&loaded.graph, &loaded.graph, schema, s).expect("stratifiable");
2355 let evidence = Evidence::Satisfaction(sats[0].trace.clone());
2356
2357 let grouped = evidence.matched_values_by_path();
2360 assert_eq!(grouped.len(), 2);
2361 let group = |iri: &str| {
2362 grouped
2363 .iter()
2364 .find(|(path, _)| *path == pred_path(iri))
2365 .map(|(_, values)| terms(values))
2366 .expect("path was counted")
2367 };
2368 assert_eq!(group("http://ex/p"), ["<http://ex/y1>", "<http://ex/y2>"]);
2369 assert_eq!(group("http://ex/q"), ["<http://ex/z>"]);
2370
2371 assert_eq!(
2374 terms(&evidence.values_for_path(&pred_path("http://ex/p"))),
2375 ["<http://ex/y1>", "<http://ex/y2>"],
2376 );
2377 assert_eq!(
2378 terms(&evidence.values_for_path(&pred_path("http://ex/q"))),
2379 ["<http://ex/z>"],
2380 );
2381 assert_eq!(
2382 grouped
2383 .into_iter()
2384 .flat_map(|(_, values)| values)
2385 .collect::<Vec<_>>(),
2386 evidence.matched_values(),
2387 );
2388
2389 assert!(
2391 evidence
2392 .values_for_path(&pred_path("http://ex/absent"))
2393 .is_empty()
2394 );
2395 assert!(evidence.values_for_path(&Path::Id).is_empty());
2396 }
2397
2398 #[test]
2399 fn a_short_count_attributes_its_qualifying_matches_to_its_own_path() {
2400 let ttl = format!(
2403 "{PREFIXES}
2404 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2405 sh:property [ sh:path ex:p ; sh:minCount 3 ] ;
2406 sh:property [ sh:path ex:q ; sh:minCount 1 ] .
2407 ex:x ex:p ex:y1, ex:y2 ; ex:q ex:z .
2408 "
2409 );
2410 let ws = run(&ttl);
2411 assert_eq!(ws.len(), 1);
2412 let evidence = Evidence::Failure(ws[0].failure.clone());
2413
2414 assert_eq!(
2415 terms(&evidence.values_for_path(&pred_path("http://ex/p"))),
2416 ["<http://ex/y1>", "<http://ex/y2>"],
2417 );
2418 assert!(
2419 evidence
2420 .values_for_path(&pred_path("http://ex/q"))
2421 .is_empty(),
2422 "a satisfied sibling is not part of the failure's evidence",
2423 );
2424 }
2425
2426 #[test]
2427 fn a_deficit_names_the_node_path_and_qualifier_that_would_close_it() {
2428 let ttl = format!(
2431 "{PREFIXES}
2432 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
2433 sh:property [ sh:path ex:p ;
2434 sh:qualifiedValueShape [ sh:class ex:C ] ;
2435 sh:qualifiedMinCount 2 ] .
2436 ex:x ex:p ex:good, ex:near .
2437 ex:good a ex:C .
2438 "
2439 );
2440 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
2441 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
2442 let ws =
2443 witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).expect("stratifiable");
2444 let obligations = Evidence::Failure(ws[0].failure.clone()).missing_obligations();
2445
2446 let on_focus: Vec<_> = obligations
2448 .iter()
2449 .filter(|item| item.node.to_string() == "<http://ex/x>")
2450 .collect();
2451 assert_eq!(on_focus.len(), 1);
2452 assert_eq!(on_focus[0].path, pred_path("http://ex/p"));
2453 assert_eq!((on_focus[0].observed_count, on_focus[0].missing), (1, 1));
2454 assert_eq!(
2455 shifty_algebra::render::describe_shape(&parsed.schema.arena, on_focus[0].qualifier),
2456 "instance of <http://ex/C>",
2457 "the qualifier says what an added value must satisfy",
2458 );
2459
2460 assert!(
2463 obligations
2464 .iter()
2465 .any(|item| item.node.to_string() == "<http://ex/near>"),
2466 "obligations: {obligations:?}",
2467 );
2468 }
2469
2470 fn any_sat(t: &SatTrace, pred: &impl Fn(&SatTrace) -> bool) -> bool {
2472 if pred(t) {
2473 return true;
2474 }
2475 match t {
2476 SatTrace::AllHeld { children, .. } => children.iter().any(|c| any_sat(c, pred)),
2477 SatTrace::AnyHeld { satisfied, .. } => satisfied.iter().any(|c| any_sat(c, pred)),
2478 SatTrace::CountHeld { matches, .. } => matches.iter().any(|(_, _, c)| any_sat(c, pred)),
2479 SatTrace::ForAllHeld { values, .. } => values.iter().any(|(_, _, c)| any_sat(c, pred)),
2480 _ => false,
2481 }
2482 }
2483
2484 #[test]
2485 fn shared_child_grammar_is_preorder_and_crosses_polarity_at_not() {
2486 let node = Term::NamedNode(NamedNode::new_unchecked("http://ex/x"));
2487 let failure = Evidence::Failure(Witness::All {
2488 shape: ShapeId(0),
2489 node: node.clone(),
2490 failed: vec![
2491 Witness::Not {
2492 shape: ShapeId(1),
2493 node: node.clone(),
2494 inner: Box::new(SatTrace::Irrefutable { shape: ShapeId(2) }),
2495 },
2496 Witness::Opaque {
2497 shape: ShapeId(3),
2498 node: node.clone(),
2499 messages: Vec::new(),
2500 diagnostic: None,
2501 },
2502 ],
2503 });
2504
2505 let root = failure.walk()[0];
2506 assert_eq!(
2507 root.children()
2508 .into_iter()
2509 .map(EvidenceNodeRef::kind)
2510 .collect::<Vec<_>>(),
2511 ["not_failed", "opaque"],
2512 );
2513 assert_eq!(
2514 failure
2515 .walk()
2516 .into_iter()
2517 .map(|item| (item.kind(), item.status()))
2518 .collect::<Vec<_>>(),
2519 [
2520 ("all_failed", EvaluationStatus::Fail),
2521 ("not_failed", EvaluationStatus::Fail),
2522 ("irrefutable", EvaluationStatus::Pass),
2523 ("opaque", EvaluationStatus::Fail),
2524 ],
2525 );
2526
2527 let satisfaction = Evidence::Satisfaction(SatTrace::NotHeld {
2528 shape: ShapeId(4),
2529 node,
2530 inner_fails: Box::new(Witness::Opaque {
2531 shape: ShapeId(5),
2532 node: Term::NamedNode(NamedNode::new_unchecked("http://ex/x")),
2533 messages: Vec::new(),
2534 diagnostic: None,
2535 }),
2536 });
2537 assert_eq!(
2538 satisfaction
2539 .walk()
2540 .into_iter()
2541 .map(|item| (item.kind(), item.status()))
2542 .collect::<Vec<_>>(),
2543 [
2544 ("not_held", EvaluationStatus::Pass),
2545 ("opaque", EvaluationStatus::Fail),
2546 ],
2547 );
2548 }
2549
2550 #[test]
2551 fn evidence_kind_is_an_exhaustive_polarity_discriminant() {
2552 let passing = [
2553 EvidenceKind::Irrefutable,
2554 EvidenceKind::AtomHeld,
2555 EvidenceKind::AllHeld,
2556 EvidenceKind::AnyHeld,
2557 EvidenceKind::CountHeld,
2558 EvidenceKind::AllValuesHeld,
2559 EvidenceKind::NotHeld,
2560 EvidenceKind::Blocked,
2561 EvidenceKind::Coinductive,
2562 ];
2563 let failing = [
2564 EvidenceKind::AtomFailed,
2565 EvidenceKind::RelationalFailed,
2566 EvidenceKind::ClosedFailed,
2567 EvidenceKind::NotFailed,
2568 EvidenceKind::AllFailed,
2569 EvidenceKind::AnyFailed,
2570 EvidenceKind::CountLow,
2571 EvidenceKind::CountHigh,
2572 EvidenceKind::Opaque,
2573 ];
2574
2575 assert!(
2576 passing
2577 .iter()
2578 .all(|kind| kind.status() == EvaluationStatus::Pass)
2579 );
2580 assert!(
2581 failing
2582 .iter()
2583 .all(|kind| kind.status() == EvaluationStatus::Fail)
2584 );
2585 let names = passing
2586 .iter()
2587 .chain(&failing)
2588 .map(|kind| kind.as_str())
2589 .collect::<HashSet<_>>();
2590 assert_eq!(names.len(), passing.len() + failing.len());
2591 }
2592}