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