1use crate::focus::{FocusNodes, FocusScope, IndexedFocus};
22use crate::frozen::FrozenIndexedDataset;
23use crate::sparql::SparqlExecutor;
24use crate::validate::{
25 NonStratifiable, ShapeEvaluator, ValidationGraphMode, ValidationOptions,
26 entry_shape_any_name_selected, focus_nodes_with_evaluator, graph_union,
27 prefetch_sparql_constraints, uses_shapes_graph,
28};
29use crate::witness::{
30 ChildEvaluation, ConstraintCatalog, ConstraintRecord, EvaluationProgress, EvaluationStatus,
31 Evidence, EvidenceRun, EvidenceSummary, FocusEvaluation, StatementEvaluation, Witness,
32 materialize_evidence,
33};
34use oxrdf::Graph;
35use shifty_algebra::{ConstraintKind, Schema, Severity, Shape, ShapeArena, ShapeId};
36use shifty_opt::{analyze, normalize_with_mapping};
37use std::cell::OnceCell;
38use std::collections::{HashMap, HashSet};
39use std::sync::Arc;
40
41pub struct PreparedEvidenceValidator {
48 data: Option<Arc<Graph>>,
52 indexed_focus: Option<FocusScope>,
53 compatibility_union: bool,
54 union_projection: OnceCell<Graph>,
55 data_projection: OnceCell<Arc<Graph>>,
56 raw_schema: Arc<Schema>,
60 schema: Arc<Schema>,
64 raw_by_normalized: Arc<Vec<Vec<usize>>>,
69 shape_map: Arc<Vec<Option<ShapeId>>>,
72 sparql: SparqlExecutor,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct ConformanceRun {
82 pub conforms: bool,
83 pub selected_pairs: usize,
84 pub passed: usize,
85 pub failed: usize,
86}
87
88#[derive(Debug, Clone, Default, PartialEq, Eq)]
93pub struct ConformanceOptions {
94 pub entry_shape_names: Vec<String>,
95}
96
97impl From<&ValidationOptions> for ConformanceOptions {
98 fn from(options: &ValidationOptions) -> Self {
99 Self {
100 entry_shape_names: options.entry_shape_names.clone(),
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
113pub struct SelectedPair {
114 normalized_statement: usize,
115 focus: oxrdf::Term,
116 pub(crate) public_focus: Option<oxrdf::Term>,
117 source_statements: Vec<usize>,
118 pub(crate) snapshot: Option<Arc<()>>,
119}
120
121impl PartialEq for SelectedPair {
122 fn eq(&self, other: &Self) -> bool {
123 self.normalized_statement == other.normalized_statement
124 && self.focus == other.focus
125 && self.source_statements == other.source_statements
126 && match (&self.snapshot, &other.snapshot) {
127 (Some(left), Some(right)) => Arc::ptr_eq(left, right),
128 (None, None) => true,
129 _ => false,
130 }
131 }
132}
133
134impl Eq for SelectedPair {}
135
136impl SelectedPair {
137 fn new(normalized_statement: usize, focus: oxrdf::Term, source_statements: Vec<usize>) -> Self {
138 Self {
139 normalized_statement,
140 focus,
141 public_focus: None,
142 source_statements,
143 snapshot: None,
144 }
145 }
146
147 pub fn normalized_statement(&self) -> usize {
148 self.normalized_statement
149 }
150
151 pub fn focus(&self) -> &oxrdf::Term {
152 self.public_focus.as_ref().unwrap_or(&self.focus)
153 }
154
155 pub fn source_statements(&self) -> &[usize] {
156 &self.source_statements
157 }
158}
159
160impl PreparedEvidenceValidator {
161 pub(crate) fn property_witnesses(
162 &self,
163 source: &shifty_parse::Loaded,
164 key_path: Option<&shifty_algebra::Path>,
165 options: &ValidationOptions,
166 ) -> Vec<crate::report::PropertyWitness> {
167 self.with_focus(|focus| {
168 crate::report::property_witnesses_prepared(
169 source,
170 focus,
171 &self.sparql,
172 key_path,
173 options,
174 )
175 })
176 }
177
178 pub(crate) fn report(
179 &self,
180 source: &shifty_parse::Loaded,
181 options: &ValidationOptions,
182 ) -> crate::report::ValidationReport {
183 self.with_focus(|focus| {
184 crate::report::validate_report_prepared(source, focus, &self.sparql, options)
185 })
186 }
187
188 pub(crate) fn validate_findings(
189 &self,
190 plan: &shifty_opt::PhysicalPlan,
191 options: &ValidationOptions,
192 ) -> crate::validate::ValidationOutcome {
193 let selected: Vec<bool> = self
194 .raw_by_normalized
195 .iter()
196 .map(|sources| {
197 sources.iter().copied().any(|source| {
198 self.source_statement_selected(source, &options.entry_shape_names)
199 })
200 })
201 .collect();
202 self.with_focus(|focus| {
203 crate::validate::validate_plan_with_executor(
204 focus,
205 plan,
206 &self.sparql,
207 options,
208 Some(&selected),
209 )
210 })
211 }
212
213 pub fn new(data: &Graph, schema: &Schema) -> Result<Self, NonStratifiable> {
215 let uses_shapes = uses_shapes_graph(&schema.arena);
216 let frozen = if uses_shapes {
217 FrozenIndexedDataset::from_graphs(data, data)
218 } else {
219 FrozenIndexedDataset::from_graph(data)
220 };
221 Self::from_frozen(data.clone(), schema, frozen, uses_shapes)
222 }
223
224 pub fn with_context(
226 data: &Graph,
227 context: &Graph,
228 schema: &Schema,
229 ) -> Result<Self, NonStratifiable> {
230 let uses_shapes = uses_shapes_graph(&schema.arena);
231 let frozen = if uses_shapes {
232 FrozenIndexedDataset::from_graphs(context, context)
233 } else {
234 FrozenIndexedDataset::from_graph(context)
235 };
236 Self::from_frozen(data.clone(), schema, frozen, uses_shapes)
237 }
238
239 pub fn with_graphs(
241 data: &Graph,
242 shapes: &Graph,
243 schema: &Schema,
244 mode: ValidationGraphMode,
245 ) -> Result<Self, NonStratifiable> {
246 let uses_shapes = uses_shapes_graph(&schema.arena);
247 match mode {
248 ValidationGraphMode::Data => {
249 let frozen = if uses_shapes {
250 FrozenIndexedDataset::from_graphs(data, shapes)
251 } else {
252 FrozenIndexedDataset::from_graph(data)
253 };
254 Self::from_frozen(data.clone(), schema, frozen, uses_shapes)
255 }
256 ValidationGraphMode::Union => {
257 let frozen = if uses_shapes {
258 FrozenIndexedDataset::from_graph_union_with_shapes(data, shapes)
259 } else {
260 FrozenIndexedDataset::from_graph_union(data, shapes)
261 };
262 Self::from_frozen(data.clone(), schema, frozen, uses_shapes)
263 }
264 ValidationGraphMode::UnionAll => {
265 let union = graph_union(data, shapes);
266 let frozen = if uses_shapes {
267 FrozenIndexedDataset::from_graphs(&union, &union)
268 } else {
269 FrozenIndexedDataset::from_graph(&union)
270 };
271 Self::from_frozen(union, schema, frozen, uses_shapes)
272 }
273 }
274 }
275
276 fn from_frozen(
277 data: Graph,
278 raw_schema: &Schema,
279 frozen: FrozenIndexedDataset,
280 has_shapes_graph: bool,
281 ) -> Result<Self, NonStratifiable> {
282 let normalized = normalize_with_mapping(raw_schema);
287 let stratification = analyze(&normalized.schema.arena);
288 if !stratification.stratifiable {
289 let components = stratification
290 .strata
291 .iter()
292 .filter(|stratum| !stratum.stratifiable)
293 .map(|stratum| stratum.shapes.clone())
294 .collect();
295 return Err(NonStratifiable { components });
296 }
297
298 let mut raw_by_normalized = vec![Vec::new(); normalized.schema.statements.len()];
303 for (raw, normalized_id) in normalized.statement_map.iter().copied().enumerate() {
304 raw_by_normalized[normalized_id].push(raw);
305 }
306
307 Ok(Self {
308 data: Some(Arc::new(data)),
309 indexed_focus: None,
310 compatibility_union: false,
311 union_projection: OnceCell::new(),
312 data_projection: OnceCell::new(),
313 raw_schema: Arc::new(raw_schema.clone()),
314 schema: Arc::new(normalized.schema),
315 raw_by_normalized: Arc::new(raw_by_normalized),
316 shape_map: Arc::new(normalized.shape_map),
317 sparql: SparqlExecutor::from_frozen(frozen, has_shapes_graph),
318 })
319 }
320
321 pub(crate) fn from_compiled(
322 data: Option<Arc<Graph>>,
323 compiled: &crate::compiled::CompiledShapes,
324 frozen: FrozenIndexedDataset,
325 has_shapes_graph: bool,
326 policy: crate::validate::UnsupportedPolicy,
327 focus_scope: FocusScope,
328 compatibility_union: bool,
329 ) -> Self {
330 let mut sparql = SparqlExecutor::from_frozen_with_parsed(
331 frozen,
332 has_shapes_graph,
333 Some(compiled.parsed_queries()),
334 );
335 sparql.set_functions(compiled.inner.functions.clone(), policy);
336 Self {
337 data,
338 indexed_focus: Some(focus_scope),
339 compatibility_union,
340 union_projection: OnceCell::new(),
341 data_projection: OnceCell::new(),
342 raw_schema: Arc::clone(&compiled.inner.authored),
343 schema: Arc::clone(&compiled.inner.normalized),
344 raw_by_normalized: Arc::clone(&compiled.inner.raw_by_normalized),
345 shape_map: Arc::clone(&compiled.inner.shape_map),
346 sparql,
347 }
348 }
349
350 pub fn data(&self) -> &Graph {
356 if self.compatibility_union {
357 self.union_projection.get_or_init(|| {
358 self.sparql
359 .frozen()
360 .expect("prepared dataset")
361 .default_graph_projection()
362 })
363 } else if let Some(data) = &self.data {
364 data
365 } else {
366 self.data_projection
367 .get_or_init(|| Arc::new(self.data_graph_projection()))
368 }
369 }
370
371 fn data_graph_projection(&self) -> Graph {
372 self.sparql
373 .frozen()
374 .expect("compiled preparation has a dataset")
375 .data_graph_projection()
376 }
377
378 pub(crate) fn data_graph_projection_mapped(
379 &self,
380 blank_nodes: &HashMap<oxrdf::BlankNode, oxrdf::BlankNode>,
381 ) -> Graph {
382 self.sparql
383 .frozen()
384 .expect("compiled preparation has a dataset")
385 .data_graph_projection_mapped(blank_nodes)
386 }
387
388 pub(crate) fn data_graph_shared(&self) -> Arc<Graph> {
389 if let Some(data) = &self.data {
390 Arc::clone(data)
391 } else {
392 Arc::clone(
393 self.data_projection
394 .get_or_init(|| Arc::new(self.data_graph_projection())),
395 )
396 }
397 }
398
399 fn with_focus<R>(&self, f: impl FnOnce(&dyn FocusNodes) -> R) -> R {
400 if let Some(scope) = self.indexed_focus {
401 let indexed = IndexedFocus::new(self.sparql.frozen().expect("prepared dataset"), scope);
402 f(&indexed)
403 } else {
404 f(self
405 .data
406 .as_deref()
407 .expect("legacy preparation has a graph"))
408 }
409 }
410
411 pub fn raw_constraint_holds(&self, focus: &oxrdf::Term, constraint: ShapeId) -> Option<bool> {
418 if (constraint.0 as usize) >= self.raw_schema.arena.len() {
419 return None;
420 }
421 let backend = self
422 .sparql
423 .frozen()
424 .expect("prepared evidence validator always owns a frozen dataset");
425 let mut evaluator = ShapeEvaluator::new(
426 backend,
427 &self.raw_schema.arena,
428 &self.raw_schema.prefixes,
429 &self.sparql,
430 );
431 Some(evaluator.holds(focus, constraint))
432 }
433
434 pub fn validate_conformance(&self, options: &ConformanceOptions) -> ConformanceRun {
446 self.scan_conformance(options, |_, _, _| {})
447 }
448
449 pub fn find_failures(
458 &self,
459 options: &ConformanceOptions,
460 ) -> (ConformanceRun, Vec<SelectedPair>) {
461 let selected_sources: Vec<Vec<usize>> = self
462 .raw_by_normalized
463 .iter()
464 .map(|sources| {
465 sources
466 .iter()
467 .copied()
468 .filter(|&source| {
469 self.source_statement_selected(source, &options.entry_shape_names)
470 })
471 .collect()
472 })
473 .collect();
474 let mut failures = Vec::new();
475 let run = self.scan_conformance(options, |statement, focus, holds| {
476 if !holds {
477 failures.push(SelectedPair::new(
478 statement,
479 focus.clone(),
480 selected_sources[statement].clone(),
481 ));
482 }
483 });
484 (run, failures)
485 }
486
487 fn scan_conformance(
494 &self,
495 options: &ConformanceOptions,
496 mut observe: impl FnMut(usize, &oxrdf::Term, bool),
497 ) -> ConformanceRun {
498 let backend = self
499 .sparql
500 .frozen()
501 .expect("prepared evidence validator always owns a frozen dataset");
502 let mut evaluator = ShapeEvaluator::new(
503 backend,
504 &self.schema.arena,
505 &self.schema.prefixes,
506 &self.sparql,
507 );
508 let mut run = ConformanceRun {
509 conforms: true,
510 selected_pairs: 0,
511 passed: 0,
512 failed: 0,
513 };
514
515 for (statement_id, statement) in self.schema.statements.iter().enumerate() {
519 if !self.raw_by_normalized[statement_id]
520 .iter()
521 .copied()
522 .any(|source| self.source_statement_selected(source, &options.entry_shape_names))
523 {
524 continue;
525 }
526
527 let foci = self.with_focus(|focus| {
528 focus_nodes_with_evaluator(focus, &statement.selector, &mut evaluator)
529 });
530 prefetch_sparql_constraints(&self.schema.arena, statement.shape, &foci, &self.sparql);
531
532 for focus in foci {
533 run.selected_pairs += 1;
534 let holds = evaluator.holds(&focus, statement.shape);
535 if holds {
536 run.passed += 1;
537 } else {
538 run.failed += 1;
539 run.conforms = false;
540 }
541 observe(statement_id, &focus, holds);
542 }
543 }
544
545 run
546 }
547
548 pub fn explain(&self, pair: &SelectedPair) -> Vec<StatementEvaluation> {
567 self.explain_with_progress(pair, true)
568 }
569
570 pub fn explain_canonical(&self, pair: &SelectedPair) -> Vec<StatementEvaluation> {
576 self.explain_with_progress(pair, false)
577 }
578
579 fn explain_with_progress(
580 &self,
581 pair: &SelectedPair,
582 include_progress: bool,
583 ) -> Vec<StatementEvaluation> {
584 let Some(statement) = self.schema.statements.get(pair.normalized_statement) else {
585 return Vec::new();
586 };
587 let backend = self
588 .sparql
589 .frozen()
590 .expect("prepared evidence validator always owns a frozen dataset");
591 let mut evaluator = ShapeEvaluator::new(
592 backend,
593 &self.schema.arena,
594 &self.schema.prefixes,
595 &self.sparql,
596 );
597 prefetch_sparql_constraints(
598 &self.schema.arena,
599 statement.shape,
600 std::slice::from_ref(&pair.focus),
601 &self.sparql,
602 );
603 let evidence = materialize_evidence(&mut evaluator, &pair.focus, statement.shape);
604
605 self.raw_by_normalized[pair.normalized_statement]
606 .iter()
607 .filter(|raw_statement| pair.source_statements.contains(raw_statement))
608 .map(|&raw_statement| {
609 let raw = &self.raw_schema.statements[raw_statement];
610 let progress = include_progress
611 .then(|| {
612 source_progress(
613 &mut evaluator,
614 &self.raw_schema.arena,
615 raw.shape,
616 &self.shape_map,
617 &pair.focus,
618 )
619 })
620 .flatten();
621 let mut evaluation = statement_evaluation(
622 &self.raw_schema,
623 &self.schema,
624 raw_statement,
625 pair.normalized_statement,
626 );
627 evaluation.selected_foci.push(FocusEvaluation {
628 focus: pair.focus.clone(),
629 evidence: evidence.clone(),
630 progress,
631 });
632 evaluation
633 })
634 .collect()
635 }
636
637 fn source_statement_selected(
644 &self,
645 raw_statement: usize,
646 entry_shape_names: &[String],
647 ) -> bool {
648 let raw = &self.raw_schema.statements[raw_statement];
649 entry_shape_any_name_selected(entry_shape_names, self.raw_schema.names_of(raw.shape))
650 }
651
652 pub fn source_statements(&self, normalized: usize) -> &[usize] {
659 self.raw_by_normalized
660 .get(normalized)
661 .map_or(&[], Vec::as_slice)
662 }
663
664 pub fn explain_constraint(&self, focus: &oxrdf::Term, constraint: ShapeId) -> Option<Evidence> {
677 if (constraint.0 as usize) >= self.schema.arena.len() {
678 return None;
679 }
680 let backend = self
681 .sparql
682 .frozen()
683 .expect("prepared evidence validator always owns a frozen dataset");
684 let mut evaluator = ShapeEvaluator::new(
685 backend,
686 &self.schema.arena,
687 &self.schema.prefixes,
688 &self.sparql,
689 );
690 prefetch_sparql_constraints(
691 &self.schema.arena,
692 constraint,
693 std::slice::from_ref(focus),
694 &self.sparql,
695 );
696 Some(materialize_evidence(&mut evaluator, focus, constraint))
697 }
698
699 pub fn constraints(&self) -> ConstraintCatalog {
705 ConstraintCatalog {
706 source: constraint_catalog(&self.raw_schema.arena),
707 normalized: constraint_catalog(&self.schema.arena),
708 }
709 }
710
711 pub fn validate(&self, options: &ValidationOptions) -> EvidenceRun {
713 self.validate_with_progress(options, true)
714 }
715
716 pub fn validate_canonical(&self, options: &ValidationOptions) -> EvidenceRun {
721 self.validate_with_progress(options, false)
722 }
723
724 fn validate_with_progress(
725 &self,
726 options: &ValidationOptions,
727 include_progress: bool,
728 ) -> EvidenceRun {
729 let backend = self
730 .sparql
731 .frozen()
732 .expect("prepared evidence validator always owns a frozen dataset");
733 let mut evaluator = ShapeEvaluator::new(
734 backend,
735 &self.schema.arena,
736 &self.schema.prefixes,
737 &self.sparql,
738 );
739 let mut statements: Vec<Option<StatementEvaluation>> =
740 vec![None; self.raw_schema.statements.len()];
741 let mut conforms = true;
742 let profiling = crate::profile::is_enabled();
745
746 for (statement_id, statement) in self.schema.statements.iter().enumerate() {
747 if !self.raw_by_normalized[statement_id]
748 .iter()
749 .copied()
750 .any(|source| self.source_statement_selected(source, &options.entry_shape_names))
751 {
752 continue;
753 }
754
755 let label = profiling.then(|| {
756 self.schema
757 .name_of(statement.shape)
758 .map(str::to_string)
759 .unwrap_or_else(|| format!("@{}", statement.shape.0))
760 });
761 let selection_start = profiling.then(web_time::Instant::now);
762 let foci = self.with_focus(|focus| {
763 focus_nodes_with_evaluator(focus, &statement.selector, &mut evaluator)
764 });
765 prefetch_sparql_constraints(&self.schema.arena, statement.shape, &foci, &self.sparql);
766 if let (Some(start), Some(label)) = (selection_start, label.as_deref()) {
767 crate::profile::record_shape_work(
768 &format!("select:{label}"),
769 start.elapsed().as_micros() as u64,
770 0,
771 );
772 }
773
774 for focus in foci {
775 let pair_start = profiling
776 .then(|| (web_time::Instant::now(), crate::profile::evidence_visits()));
777 let evidence = materialize_evidence(&mut evaluator, &focus, statement.shape);
778 if let (Some((start, visits_before)), Some(label)) = (pair_start, label.as_deref())
779 {
780 crate::profile::record_shape_work(
781 label,
782 start.elapsed().as_micros() as u64,
783 crate::profile::evidence_visits().saturating_sub(visits_before),
784 );
785 }
786 if let Evidence::Failure(failure) = &evidence
787 && failure_meets_threshold(
788 &self.schema.arena,
789 statement.shape,
790 failure,
791 &options.minimum_severity,
792 )
793 {
794 conforms = false;
795 }
796
797 for &raw_statement in &self.raw_by_normalized[statement_id] {
798 if !self.source_statement_selected(raw_statement, &options.entry_shape_names) {
799 continue;
800 }
801 let raw = &self.raw_schema.statements[raw_statement];
802 let progress = include_progress
803 .then(|| {
804 source_progress(
805 &mut evaluator,
806 &self.raw_schema.arena,
807 raw.shape,
808 &self.shape_map,
809 &focus,
810 )
811 })
812 .flatten();
813 statements[raw_statement]
814 .get_or_insert_with(|| {
815 statement_evaluation(
816 &self.raw_schema,
817 &self.schema,
818 raw_statement,
819 statement_id,
820 )
821 })
822 .selected_foci
823 .push(FocusEvaluation {
824 focus: focus.clone(),
825 evidence: evidence.clone(),
826 progress,
827 });
828 }
829 }
830
831 for &raw_statement in &self.raw_by_normalized[statement_id] {
833 if !self.source_statement_selected(raw_statement, &options.entry_shape_names) {
834 continue;
835 }
836 statements[raw_statement].get_or_insert_with(|| {
837 statement_evaluation(
838 &self.raw_schema,
839 &self.schema,
840 raw_statement,
841 statement_id,
842 )
843 });
844 }
845 }
846
847 let mut statements: Vec<StatementEvaluation> = statements.into_iter().flatten().collect();
848 if options.sort_results {
849 statements.sort_by_key(|statement| statement.source_statement_id);
850 for statement in &mut statements {
851 statement
852 .selected_foci
853 .sort_by_key(|focus| focus.focus.to_string());
854 }
855 }
856
857 EvidenceRun {
861 conforms,
862 constraints: ConstraintCatalog {
863 source: constraint_catalog(&self.raw_schema.arena),
864 normalized: constraint_catalog(&self.schema.arena),
865 },
866 statements,
867 }
868 }
869
870 pub fn schema(&self) -> &Schema {
872 &self.schema
873 }
874}
875
876fn constraint_catalog(arena: &ShapeArena) -> Vec<ConstraintRecord> {
877 (0..arena.len())
878 .map(|index| {
879 let id = ShapeId(index as u32);
880 ConstraintRecord {
881 id,
882 constraint_kind: ConstraintKind::of(arena, id),
883 constraint: arena.get(id).clone(),
884 }
885 })
886 .collect()
887}
888
889fn statement_evaluation(
890 raw_schema: &Schema,
891 normalized_schema: &Schema,
892 source_statement_id: usize,
893 normalized_statement_id: usize,
894) -> StatementEvaluation {
895 let source = &raw_schema.statements[source_statement_id];
896 let normalized = &normalized_schema.statements[normalized_statement_id];
897 StatementEvaluation {
898 source_statement_id,
899 normalized_statement_id: Some(normalized_statement_id),
900 source_constraint_id: source.shape,
901 normalized_constraint_id: Some(normalized.shape),
902 constraint_kind: ConstraintKind::of(&normalized_schema.arena, normalized.shape),
903 constraint: normalized_schema.arena.get(normalized.shape).clone(),
904 selector: source.selector.clone(),
905 selected_foci: Vec::new(),
906 }
907}
908
909fn source_children(arena: &ShapeArena, mut id: ShapeId) -> Vec<ShapeId> {
910 while let Shape::Annotated { shape, .. } = arena.get(id) {
911 id = *shape;
912 }
913 match arena.get(id) {
914 Shape::And(children) | Shape::Or(children) => children.clone(),
915 Shape::Not(child) => vec![*child],
916 _ => Vec::new(),
917 }
918}
919
920fn source_progress(
921 evaluator: &mut ShapeEvaluator<'_>,
922 raw_arena: &ShapeArena,
923 raw_shape: ShapeId,
924 shape_map: &[Option<ShapeId>],
925 focus: &oxrdf::Term,
926) -> Option<EvaluationProgress> {
927 let children = source_children(raw_arena, raw_shape);
928 if children.is_empty() {
929 return None;
930 }
931 let evaluated_children = children
937 .into_iter()
938 .map(|source_constraint_ref| {
939 let normalized_constraint_ref = shape_map
940 .get(source_constraint_ref.0 as usize)
941 .copied()
942 .flatten();
943 let status = normalized_constraint_ref
944 .map(|id| {
945 if evaluator.holds(focus, id) {
946 EvaluationStatus::Pass
947 } else {
948 EvaluationStatus::Fail
949 }
950 })
951 .unwrap_or(EvaluationStatus::Pass);
952 let summary_id = normalized_constraint_ref.unwrap_or(source_constraint_ref);
953 let summary_kind = normalized_constraint_ref.map_or_else(
954 || ConstraintKind::of(raw_arena, source_constraint_ref),
955 |id| ConstraintKind::of(evaluator.arena(), id),
956 );
957 ChildEvaluation {
958 source_constraint_ref,
959 normalized_constraint_ref,
960 status,
961 evidence_summary: EvidenceSummary {
962 constraint_id: summary_id,
963 constraint_kind: summary_kind,
964 status,
965 },
966 }
967 })
968 .collect();
969 Some(EvaluationProgress { evaluated_children })
970}
971
972fn witness_shape_ids(witness: &Witness, out: &mut HashSet<ShapeId>) {
973 let shape = match witness {
974 Witness::Atom { shape, .. }
975 | Witness::Relational { shape, .. }
976 | Witness::Closed { shape, .. }
977 | Witness::Not { shape, .. }
978 | Witness::All { shape, .. }
979 | Witness::Any { shape, .. }
980 | Witness::CountLow { shape, .. }
981 | Witness::CountHigh { shape, .. }
982 | Witness::Opaque { shape, .. } => *shape,
983 };
984 out.insert(shape);
985 match witness {
986 Witness::All { failed, .. } => failed
987 .iter()
988 .for_each(|child| witness_shape_ids(child, out)),
989 Witness::Any { branches, .. } => branches
990 .iter()
991 .for_each(|child| witness_shape_ids(child, out)),
992 Witness::CountHigh { per_value, .. } => per_value
993 .iter()
994 .for_each(|(_, child)| witness_shape_ids(child, out)),
995 _ => {}
996 }
997}
998
999fn failure_meets_threshold(
1000 arena: &ShapeArena,
1001 root: ShapeId,
1002 witness: &Witness,
1003 minimum: &Severity,
1004) -> bool {
1005 let mut failed = HashSet::new();
1006 witness_shape_ids(witness, &mut failed);
1007 let mut active = HashSet::new();
1008 failure_shape_meets(
1009 arena,
1010 root,
1011 &Severity::Violation,
1012 minimum,
1013 &failed,
1014 &mut active,
1015 )
1016}
1017
1018fn failure_shape_meets(
1019 arena: &ShapeArena,
1020 id: ShapeId,
1021 inherited: &Severity,
1022 minimum: &Severity,
1023 failed: &HashSet<ShapeId>,
1024 active: &mut HashSet<ShapeId>,
1025) -> bool {
1026 if !active.insert(id) {
1027 return false;
1028 }
1029 let result = match arena.get(id) {
1030 Shape::Annotated {
1031 severity, shape, ..
1032 } => failure_shape_meets(arena, *shape, severity, minimum, failed, active),
1033 Shape::And(children) => children
1034 .iter()
1035 .any(|child| failure_shape_meets(arena, *child, inherited, minimum, failed, active)),
1036 Shape::Or(children) => {
1037 (failed.contains(&id) && inherited.meets(minimum))
1038 || children.iter().any(|child| {
1039 failure_shape_meets(arena, *child, inherited, minimum, failed, active)
1040 })
1041 }
1042 Shape::Count { qualifier, .. } => {
1043 (failed.contains(&id) && inherited.meets(minimum))
1044 || failure_shape_meets(arena, *qualifier, inherited, minimum, failed, active)
1045 }
1046 Shape::Not(_) => failed.contains(&id) && inherited.meets(minimum),
1047 _ => failed.contains(&id) && inherited.meets(minimum),
1048 };
1049 active.remove(&id);
1050 result
1051}
1052
1053pub fn validate_with_evidence(
1054 data: &Graph,
1055 schema: &Schema,
1056) -> Result<EvidenceRun, NonStratifiable> {
1057 validate_with_evidence_and_options(data, schema, &ValidationOptions::default())
1058}
1059
1060pub fn validate_with_evidence_and_options(
1061 data: &Graph,
1062 schema: &Schema,
1063 options: &ValidationOptions,
1064) -> Result<EvidenceRun, NonStratifiable> {
1065 Ok(PreparedEvidenceValidator::new(data, schema)?.validate(options))
1066}
1067
1068pub fn validate_with_context_and_evidence(
1069 data: &Graph,
1070 context: &Graph,
1071 schema: &Schema,
1072) -> Result<EvidenceRun, NonStratifiable> {
1073 validate_with_context_and_evidence_and_options(
1074 data,
1075 context,
1076 schema,
1077 &ValidationOptions::default(),
1078 )
1079}
1080
1081pub fn validate_with_context_and_evidence_and_options(
1082 data: &Graph,
1083 context: &Graph,
1084 schema: &Schema,
1085 options: &ValidationOptions,
1086) -> Result<EvidenceRun, NonStratifiable> {
1087 Ok(PreparedEvidenceValidator::with_context(data, context, schema)?.validate(options))
1088}
1089
1090pub fn validate_graphs_with_evidence(
1091 data: &Graph,
1092 shapes: &Graph,
1093 schema: &Schema,
1094) -> Result<EvidenceRun, NonStratifiable> {
1095 validate_graphs_with_evidence_and_mode_and_options(
1096 data,
1097 shapes,
1098 schema,
1099 ValidationGraphMode::default(),
1100 &ValidationOptions::default(),
1101 )
1102}
1103
1104pub fn validate_graphs_with_evidence_and_mode(
1105 data: &Graph,
1106 shapes: &Graph,
1107 schema: &Schema,
1108 mode: ValidationGraphMode,
1109) -> Result<EvidenceRun, NonStratifiable> {
1110 validate_graphs_with_evidence_and_mode_and_options(
1111 data,
1112 shapes,
1113 schema,
1114 mode,
1115 &ValidationOptions::default(),
1116 )
1117}
1118
1119pub fn validate_graphs_with_evidence_and_mode_and_options(
1120 data: &Graph,
1121 shapes: &Graph,
1122 schema: &Schema,
1123 mode: ValidationGraphMode,
1124 options: &ValidationOptions,
1125) -> Result<EvidenceRun, NonStratifiable> {
1126 Ok(PreparedEvidenceValidator::with_graphs(data, shapes, schema, mode)?.validate(options))
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131 use super::*;
1132 use crate::{PathSupport, SatTrace};
1133 use oxrdf::Triple;
1134 use shifty_parse::{load_turtle, parse_turtle};
1135
1136 const PREFIXES: &str = r#"
1137 @prefix sh: <http://www.w3.org/ns/shacl#> .
1138 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
1139 @prefix ex: <http://ex/> .
1140 "#;
1141
1142 fn run(ttl: &str) -> (Graph, Schema, EvidenceRun) {
1143 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1144 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1145 let outcome = validate_with_evidence(&loaded.graph, &parsed.schema).unwrap();
1146 (loaded.graph, parsed.schema, outcome)
1147 }
1148
1149 const COLLAPSING: &str = r#"
1151 ex:S1 a sh:NodeShape ; sh:targetClass ex:T ;
1152 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
1153 ex:S2 a sh:NodeShape ; sh:targetClass ex:T ;
1154 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
1155 ex:bad a ex:T .
1156 "#;
1157
1158 fn scoped_to(schema: &Schema, data: &Graph, name: &str) -> Vec<String> {
1159 let prepared = PreparedEvidenceValidator::new(data, schema).expect("stratifiable");
1160 let options = ValidationOptions {
1161 entry_shape_names: vec![name.to_string()],
1162 ..ValidationOptions::default()
1163 };
1164 prepared
1165 .validate(&options)
1166 .statements
1167 .iter()
1168 .map(|statement| {
1169 schema
1170 .name_of(schema.statements[statement.source_statement_id].shape)
1171 .unwrap_or("<blank>")
1172 .to_string()
1173 })
1174 .collect()
1175 }
1176
1177 #[test]
1178 fn entry_shape_names_reach_a_shape_that_cse_collapsed_onto_another() {
1179 let ttl = format!("{PREFIXES}{COLLAPSING}");
1183 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1184 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1185
1186 assert_eq!(
1187 scoped_to(&parsed.schema, &loaded.graph, "http://ex/S1"),
1188 ["http://ex/S1"],
1189 );
1190 assert_eq!(
1191 scoped_to(&parsed.schema, &loaded.graph, "http://ex/S2"),
1192 ["http://ex/S2"],
1193 );
1194 let prepared =
1196 PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).expect("stratifiable");
1197 assert_eq!(
1198 prepared
1199 .validate(&ValidationOptions::default())
1200 .statements
1201 .len(),
1202 2,
1203 );
1204 }
1205
1206 #[test]
1207 fn scoped_failure_handles_explain_only_the_selected_authored_statement() {
1208 let ttl = format!("{PREFIXES}{COLLAPSING}");
1209 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1210 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1211 let prepared =
1212 PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).expect("stratifiable");
1213 let options = ConformanceOptions {
1214 entry_shape_names: vec!["http://ex/S1".to_string()],
1215 };
1216
1217 let (_, failures) = prepared.find_failures(&options);
1218 assert_eq!(failures.len(), 1);
1219 assert_eq!(failures[0].source_statements(), [0]);
1220 assert_eq!(
1221 prepared
1222 .explain(&failures[0])
1223 .iter()
1224 .map(|statement| statement.source_statement_id)
1225 .collect::<Vec<_>>(),
1226 [0],
1227 );
1228 }
1229
1230 #[test]
1231 fn a_collapsed_shape_answers_to_every_name_that_reached_it() {
1232 let ttl = format!("{PREFIXES}{COLLAPSING}");
1233 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1234 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1235 let prepared =
1236 PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).expect("stratifiable");
1237
1238 let normalized = prepared.schema();
1240 assert_eq!(normalized.statements.len(), 1);
1241 assert_eq!(
1242 normalized.names_of(normalized.statements[0].shape),
1243 ["http://ex/S1".to_string(), "http://ex/S2".to_string()],
1244 );
1245 assert_eq!(
1248 normalized.name_of(normalized.statements[0].shape),
1249 Some("http://ex/S1"),
1250 );
1251 assert_eq!(
1253 crate::shape_id_for_iri(normalized, "http://ex/S1"),
1254 crate::shape_id_for_iri(normalized, "http://ex/S2"),
1255 );
1256 assert!(crate::shape_id_for_iri(normalized, "http://ex/S2").is_some());
1257 }
1258
1259 fn foci(outcome: &EvidenceRun) -> Vec<&FocusEvaluation> {
1260 outcome
1261 .statements
1262 .iter()
1263 .flat_map(|statement| &statement.selected_foci)
1264 .collect()
1265 }
1266
1267 fn traces(trace: &SatTrace, pred: &impl Fn(&SatTrace) -> bool) -> bool {
1268 if pred(trace) {
1269 return true;
1270 }
1271 match trace {
1272 SatTrace::AllHeld { children, .. } => children.iter().any(|t| traces(t, pred)),
1273 SatTrace::AnyHeld { satisfied, .. } => satisfied.iter().any(|t| traces(t, pred)),
1274 SatTrace::CountHeld { matches, .. } => matches.iter().any(|(_, _, t)| traces(t, pred)),
1275 SatTrace::ForAllHeld { values, .. } => values.iter().any(|(_, _, t)| traces(t, pred)),
1276 _ => false,
1277 }
1278 }
1279
1280 fn support_edges(support: &PathSupport, out: &mut Vec<Triple>) {
1281 match support {
1282 PathSupport::Edge(edge) => out.push(edge.clone()),
1283 PathSupport::Chain(parts) | PathSupport::Alt(parts) => {
1284 parts.iter().for_each(|part| support_edges(part, out));
1285 }
1286 PathSupport::Empty => {}
1287 }
1288 }
1289
1290 #[test]
1291 fn conformance_only_run_agrees_with_evidence_run() {
1292 let ttl = format!(
1293 "{PREFIXES}
1294 ex:S a sh:NodeShape ; sh:targetClass ex:T ;
1295 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
1296 ex:T2 a sh:NodeShape ; sh:targetClass ex:T ;
1297 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
1298 ex:good a ex:T ; ex:p ex:value .
1299 ex:bad a ex:T .
1300 ex:unselected ex:p ex:value ."
1301 );
1302 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1303 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1304 let prepared = PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).unwrap();
1305 let options = ValidationOptions::default();
1306 let conformance = prepared.validate_conformance(&ConformanceOptions::from(&options));
1307 let evidence = prepared.validate(&options);
1308
1309 assert_eq!(conformance.conforms, evidence.conforms);
1310 assert_eq!(conformance.selected_pairs, 2);
1311 assert_eq!(conformance.passed, 1);
1312 assert_eq!(conformance.failed, 1);
1313 assert_eq!(foci(&evidence).len(), 4);
1316 }
1317
1318 #[test]
1319 fn selected_pairs_are_partitioned_and_unselected_pairs_are_absent() {
1320 let ttl = format!(
1321 "{PREFIXES}
1322 ex:S a sh:NodeShape ; sh:targetClass ex:T ;
1323 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
1324 ex:good a ex:T ; ex:p ex:value .
1325 ex:bad a ex:T .
1326 ex:unselected ex:p ex:value ."
1327 );
1328 let (_, _, outcome) = run(&ttl);
1329 let results = foci(&outcome);
1330 assert!(!outcome.conforms);
1331 assert_eq!(results.len(), 2);
1332 assert_eq!(
1333 results
1334 .iter()
1335 .filter(|r| matches!(r.evidence, Evidence::Satisfaction(_)))
1336 .count(),
1337 1
1338 );
1339 assert_eq!(
1340 results
1341 .iter()
1342 .filter(|r| matches!(r.evidence, Evidence::Failure(_)))
1343 .count(),
1344 1
1345 );
1346 assert!(
1347 results
1348 .iter()
1349 .all(|r| !r.focus.to_string().contains("unselected"))
1350 );
1351 }
1352
1353 #[test]
1354 fn raw_statements_survive_normalized_deduplication() {
1355 let ttl = format!(
1356 "{PREFIXES}
1357 ex:S1 a sh:NodeShape ; sh:targetNode ex:x ; sh:nodeKind sh:IRI .
1358 ex:S2 a sh:NodeShape ; sh:targetNode ex:x ; sh:nodeKind sh:IRI ."
1359 );
1360 let (_, raw, outcome) = run(&ttl);
1361 assert_eq!(raw.statements.len(), 2);
1362 assert_eq!(outcome.statements.len(), 2);
1363 assert_eq!(outcome.statements[0].source_statement_id, 0);
1364 assert_eq!(outcome.statements[1].source_statement_id, 1);
1365 assert_eq!(
1366 outcome.statements[0].normalized_statement_id,
1367 outcome.statements[1].normalized_statement_id
1368 );
1369 assert_eq!(
1370 outcome.statements[0].normalized_constraint_id,
1371 outcome.statements[1].normalized_constraint_id
1372 );
1373 }
1374
1375 #[test]
1376 fn selector_matching_nothing_has_empty_statement_row() {
1377 let ttl = format!(
1378 "{PREFIXES}
1379 ex:S a sh:NodeShape ; sh:targetClass ex:Missing ; sh:nodeKind sh:IRI .
1380 ex:x a ex:Other ."
1381 );
1382 let (_, _, outcome) = run(&ttl);
1383 assert!(outcome.conforms);
1384 assert_eq!(outcome.statements.len(), 1);
1385 assert!(outcome.statements[0].selected_foci.is_empty());
1386 }
1387
1388 #[test]
1389 fn negation_crosses_to_the_opposite_evidence_polarity() {
1390 let passing = format!(
1391 "{PREFIXES}
1392 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1393 sh:not [ sh:class ex:C ] .
1394 ex:x a ex:Other ."
1395 );
1396 let (_, _, pass) = run(&passing);
1397 let pass_foci = foci(&pass);
1398 let Evidence::Satisfaction(trace) = &pass_foci[0].evidence else {
1399 panic!("expected satisfaction")
1400 };
1401 assert!(traces(trace, &|t| matches!(t, SatTrace::NotHeld { .. })));
1402
1403 let failing = format!(
1404 "{PREFIXES}
1405 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1406 sh:not [ sh:class ex:C ] .
1407 ex:x a ex:C ."
1408 );
1409 let (_, _, fail) = run(&failing);
1410 let fail_foci = foci(&fail);
1411 assert!(matches!(
1412 fail_foci[0].evidence,
1413 Evidence::Failure(Witness::Not { .. }) | Evidence::Failure(Witness::All { .. })
1414 ));
1415 fn contains_not(w: &Witness) -> bool {
1416 match w {
1417 Witness::Not { .. } => true,
1418 Witness::All { failed, .. } => failed.iter().any(contains_not),
1419 Witness::Any { branches, .. } => branches.iter().any(contains_not),
1420 _ => false,
1421 }
1422 }
1423 let Evidence::Failure(witness) = &fail_foci[0].evidence else {
1424 unreachable!()
1425 };
1426 assert!(contains_not(witness));
1427 }
1428
1429 #[test]
1430 fn sequence_path_satisfaction_carries_only_real_graph_edges() {
1431 let ttl = format!(
1432 "{PREFIXES}
1433 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1434 sh:property [ sh:path (ex:p ex:q) ; sh:minCount 1 ] .
1435 ex:x ex:p ex:y . ex:y ex:q ex:z ."
1436 );
1437 let (graph, _, outcome) = run(&ttl);
1438 let results = foci(&outcome);
1439 let Evidence::Satisfaction(trace) = &results[0].evidence else {
1440 panic!("expected satisfaction")
1441 };
1442 let mut edges = Vec::new();
1443 fn gather(trace: &SatTrace, edges: &mut Vec<Triple>) {
1444 match trace {
1445 SatTrace::CountHeld { matches, .. } => {
1446 for (_, support, child) in matches {
1447 support_edges(support, edges);
1448 gather(child, edges);
1449 }
1450 }
1451 SatTrace::ForAllHeld { values, .. } => {
1452 for (_, support, child) in values {
1453 support_edges(support, edges);
1454 gather(child, edges);
1455 }
1456 }
1457 SatTrace::AllHeld { children, .. } => {
1458 children.iter().for_each(|child| gather(child, edges))
1459 }
1460 SatTrace::AnyHeld { satisfied, .. } => {
1461 satisfied.iter().for_each(|child| gather(child, edges))
1462 }
1463 _ => {}
1464 }
1465 }
1466 gather(trace, &mut edges);
1467 assert_eq!(edges.len(), 2);
1468 assert!(edges.iter().all(|edge| graph.contains(edge.as_ref())));
1469 }
1470
1471 #[test]
1472 fn count_evidence_retains_matches_bounds_qualifiers_and_support() {
1473 let low_ttl = format!(
1474 "{PREFIXES}
1475 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1476 sh:property [ sh:path ex:p ; sh:minCount 2 ] ;
1477 sh:property [ sh:path ex:p ; sh:class ex:C ] .
1478 ex:x ex:p ex:one . ex:one a ex:C ."
1479 );
1480 let (_, _, low) = run(&low_ttl);
1481 let low_foci = foci(&low);
1482 let Evidence::Failure(low_witness) = &low_foci[0].evidence else {
1483 panic!("expected low-count failure")
1484 };
1485 fn find_low(witness: &Witness) -> Option<(u64, u64, ShapeId, &[ShapeId])> {
1486 match witness {
1487 Witness::CountLow {
1488 have,
1489 min,
1490 qualifier,
1491 sibling_qualifiers,
1492 ..
1493 } => Some((*have, *min, *qualifier, sibling_qualifiers)),
1494 Witness::All { failed, .. } => failed.iter().find_map(find_low),
1495 Witness::Any { branches, .. } => branches.iter().find_map(find_low),
1496 _ => None,
1497 }
1498 }
1499 let (have, min, qualifier, _siblings) = find_low(low_witness).expect("CountLow");
1500 assert_eq!((have, min), (1, 2));
1501 assert_ne!(qualifier, ShapeId(u32::MAX));
1502
1503 let high_ttl = format!(
1504 "{PREFIXES}
1505 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1506 sh:property [ sh:path ex:p ; sh:maxCount 1 ] .
1507 ex:x ex:p ex:one, ex:two ."
1508 );
1509 let (graph, _, high) = run(&high_ttl);
1510 let high_foci = foci(&high);
1511 let Evidence::Failure(high_witness) = &high_foci[0].evidence else {
1512 panic!("expected high-count failure")
1513 };
1514 fn find_high(witness: &Witness) -> Option<(&[(oxrdf::Term, PathSupport)], u64)> {
1515 match witness {
1516 Witness::CountHigh { matched, max, .. } => Some((matched, *max)),
1517 Witness::All { failed, .. } => failed.iter().find_map(find_high),
1518 Witness::Any { branches, .. } => branches.iter().find_map(find_high),
1519 _ => None,
1520 }
1521 }
1522 let (matched, max) = find_high(high_witness).expect("CountHigh");
1523 assert_eq!(max, 1);
1524 assert_eq!(matched.len(), 2);
1525 let excess = high_foci[0]
1526 .evidence
1527 .walk()
1528 .into_iter()
1529 .find_map(|node| match node {
1530 crate::EvidenceNodeRef::Failure(Witness::CountHigh { excess_values, .. }) => {
1531 Some(excess_values)
1532 }
1533 _ => None,
1534 })
1535 .expect("CountHigh excess values");
1536 assert_eq!(excess.len(), 1);
1537 let mut edges = Vec::new();
1538 matched
1539 .iter()
1540 .for_each(|(_, support)| support_edges(support, &mut edges));
1541 assert_eq!(edges.len(), 2);
1542 assert!(edges.iter().all(|edge| graph.contains(edge.as_ref())));
1543 }
1544
1545 #[test]
1546 fn failed_conjunction_progress_retains_successful_siblings() {
1547 let ttl = format!(
1548 "{PREFIXES}
1549 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1550 sh:nodeKind sh:IRI ;
1551 sh:property [ sh:path ex:p ; sh:minCount 1 ] ."
1552 );
1553 let (_, _, outcome) = run(&ttl);
1554 let results = foci(&outcome);
1555 let focus = results[0];
1556 assert_eq!(focus.status(), EvaluationStatus::Fail);
1557 let progress = focus.progress.as_ref().expect("conjunction progress");
1558 assert_eq!(progress.evaluated_children.len(), 2);
1559 assert_eq!(
1560 progress
1561 .evaluated_children
1562 .iter()
1563 .map(|child| child.status)
1564 .collect::<Vec<_>>(),
1565 vec![EvaluationStatus::Pass, EvaluationStatus::Fail]
1566 );
1567 let Evidence::Failure(Witness::All { failed, .. }) = &focus.evidence else {
1568 panic!("expected compact conjunction failure")
1569 };
1570 assert_eq!(failed.len(), 1);
1571 }
1572
1573 #[test]
1574 fn canonical_interfaces_omit_progress_without_changing_evidence() {
1575 let ttl = format!(
1576 "{PREFIXES}
1577 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1578 sh:nodeKind sh:IRI ;
1579 sh:property [ sh:path ex:p ; sh:minCount 1 ] ."
1580 );
1581 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1582 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1583 let prepared = PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).unwrap();
1584 let options = ValidationOptions::default();
1585
1586 let complete = prepared.validate(&options);
1587 let canonical = prepared.validate_canonical(&options);
1588 assert_eq!(complete.conforms, canonical.conforms);
1589 assert_eq!(complete.constraints, canonical.constraints);
1590 assert_eq!(complete.statements.len(), canonical.statements.len());
1591
1592 for (with_progress, without_progress) in complete
1593 .statements
1594 .iter()
1595 .flat_map(|statement| &statement.selected_foci)
1596 .zip(
1597 canonical
1598 .statements
1599 .iter()
1600 .flat_map(|statement| &statement.selected_foci),
1601 )
1602 {
1603 assert!(with_progress.progress.is_some());
1604 assert!(without_progress.progress.is_none());
1605 assert_eq!(with_progress.focus, without_progress.focus);
1606 assert_eq!(with_progress.evidence, without_progress.evidence);
1607 }
1608
1609 let (_, failures) = prepared.find_failures(&ConformanceOptions::from(&options));
1610 let explained = prepared.explain_canonical(&failures[0]);
1611 assert!(
1612 explained
1613 .iter()
1614 .flat_map(|statement| &statement.selected_foci)
1615 .all(|focus| focus.progress.is_none())
1616 );
1617 }
1618
1619 #[test]
1620 fn qualified_count_low_partitions_matches_and_rejected_candidates() {
1621 let ttl = format!(
1622 "{PREFIXES}
1623 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1624 sh:property [
1625 sh:path ex:p ;
1626 sh:qualifiedValueShape [ sh:class ex:C ] ;
1627 sh:qualifiedMinCount 2
1628 ] .
1629 ex:x ex:p ex:good, ex:near . ex:good a ex:C ."
1630 );
1631 let (graph, _, outcome) = run(&ttl);
1632 let results = foci(&outcome);
1633 let evidence = &results[0].evidence;
1634 let low = evidence.walk().into_iter().find_map(|node| match node {
1635 crate::EvidenceNodeRef::Failure(value @ Witness::CountLow { .. }) => Some(value),
1636 _ => None,
1637 });
1638 let Some(Witness::CountLow {
1639 qualifying_matches,
1640 rejected_candidates,
1641 have,
1642 min,
1643 ..
1644 }) = low
1645 else {
1646 panic!("expected CountLow")
1647 };
1648 assert_eq!((*have, *min), (1, 2));
1649 assert_eq!(qualifying_matches.len(), 1);
1650 assert_eq!(rejected_candidates.len(), 1);
1651 assert_eq!(evidence.missing_obligations()[0].missing, 1);
1652 assert!(
1653 evidence
1654 .supporting_triples()
1655 .iter()
1656 .all(|triple| graph.contains(triple.as_ref()))
1657 );
1658 assert_eq!(
1659 evidence.to_json().unwrap(),
1660 evidence.to_json().unwrap(),
1661 "serialization must be deterministic"
1662 );
1663 }
1664
1665 #[test]
1666 fn positive_recursion_is_coinductive_and_negative_recursion_is_rejected() {
1667 let positive = format!(
1668 "{PREFIXES}
1669 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1670 sh:property [ sh:path ex:p ; sh:node ex:S ] .
1671 ex:x ex:p ex:x ."
1672 );
1673 let (_, _, outcome) = run(&positive);
1674 let results = foci(&outcome);
1675 let Evidence::Satisfaction(trace) = &results[0].evidence else {
1676 panic!("expected coinductive satisfaction")
1677 };
1678 assert!(
1679 traces(trace, &|t| matches!(t, SatTrace::Coinductive { .. })),
1680 "trace did not retain recursion: {trace:?}"
1681 );
1682 let positive_loaded = load_turtle(positive.as_bytes(), None).unwrap();
1683 let positive_parsed = parse_turtle(positive.as_bytes(), None).unwrap();
1684 let ordinary = crate::validate(
1685 &positive_loaded.graph,
1686 &shifty_opt::normalize(&positive_parsed.schema),
1687 )
1688 .unwrap();
1689 assert_eq!(outcome.conforms, ordinary.conforms);
1690
1691 let negative = format!(
1692 "{PREFIXES}
1693 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1694 sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ;
1695 sh:qualifiedMinCount 1 ] .
1696 ex:x ex:p ex:x ."
1697 );
1698 let parsed = parse_turtle(negative.as_bytes(), None).unwrap();
1699 let loaded = load_turtle(negative.as_bytes(), None).unwrap();
1700 assert!(validate_with_evidence(&loaded.graph, &parsed.schema).is_err());
1701 }
1702
1703 #[test]
1704 fn outcome_serializes_and_conforms_matches_ordinary_validation() {
1705 let ttl = format!(
1706 "{PREFIXES}
1707 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1708 sh:property [ sh:path ex:p ; sh:minCount 1 ] ."
1709 );
1710 let (graph, raw, outcome) = run(&ttl);
1711 let normalized = shifty_opt::normalize(&raw);
1712 let ordinary = crate::validate(&graph, &normalized).unwrap();
1713 assert_eq!(outcome.conforms, ordinary.conforms);
1714 let encoded = serde_json::to_string(&outcome).unwrap();
1715 let decoded: EvidenceRun = serde_json::from_str(&encoded).unwrap();
1716 assert_eq!(decoded, outcome);
1717
1718 let sparql = format!(
1719 "{PREFIXES}
1720 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1721 sh:sparql [ sh:select \"SELECT $this WHERE {{ FILTER NOT EXISTS {{ $this <http://ex/p> ?value }} }}\" ] ."
1722 );
1723 let (graph, raw, evidence) = run(&sparql);
1724 let ordinary = crate::validate(&graph, &shifty_opt::normalize(&raw)).unwrap();
1725 assert_eq!(evidence.conforms, ordinary.conforms);
1726 assert!(matches!(foci(&evidence)[0].evidence, Evidence::Failure(_)));
1727 }
1728
1729 const ON_DEMAND: &str = r#"
1733 ex:S a sh:NodeShape ; sh:targetClass ex:T ;
1734 sh:property [ sh:path ex:p ; sh:minCount 1 ; sh:class ex:C ] .
1735 ex:S2 a sh:NodeShape ; sh:targetClass ex:T ;
1736 sh:property [ sh:path ex:p ; sh:minCount 1 ; sh:class ex:C ] .
1737 ex:U a sh:NodeShape ; sh:targetClass ex:T ;
1738 sh:property [ sh:path ex:q ; sh:maxCount 1 ] .
1739 ex:good a ex:T ; ex:p ex:c1 ; ex:q ex:z .
1740 ex:bad a ex:T ; ex:q ex:z ; ex:q ex:y .
1741 ex:also a ex:T ; ex:p ex:missing .
1742 ex:c1 a ex:C .
1743 "#;
1744
1745 fn prepared(ttl: &str) -> PreparedEvidenceValidator {
1746 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1747 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1748 PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).unwrap()
1749 }
1750
1751 #[test]
1752 fn find_failures_agrees_with_the_full_run() {
1753 let validator = prepared(&format!("{PREFIXES}{ON_DEMAND}"));
1754 let options = ValidationOptions::default();
1755 let full = validator.validate(&options);
1756 let conformance_options = ConformanceOptions::from(&options);
1757 let (conformance, failures) = validator.find_failures(&conformance_options);
1758
1759 assert_eq!(conformance.conforms, full.conforms);
1760 assert!(!failures.is_empty(), "fixture has no failing pair");
1761 assert_eq!(conformance.failed, failures.len());
1762 assert_eq!(
1764 conformance,
1765 validator.validate_conformance(&conformance_options)
1766 );
1767
1768 for pair in &failures {
1771 for evaluation in validator.explain(pair) {
1772 assert!(matches!(
1773 evaluation.selected_foci[0].evidence,
1774 Evidence::Failure(_)
1775 ));
1776 }
1777 }
1778 let full_failures: HashSet<(usize, String)> = full
1779 .statements
1780 .iter()
1781 .flat_map(|statement| {
1782 statement.selected_foci.iter().map(|focus| {
1783 (
1784 statement.normalized_statement_id.unwrap(),
1785 focus.focus.to_string(),
1786 )
1787 })
1788 })
1789 .filter(|_| true)
1790 .collect();
1791 for pair in &failures {
1792 assert!(
1793 full_failures.contains(&(pair.normalized_statement(), pair.focus().to_string())),
1794 "failure {pair:?} was not a pair of the full run"
1795 );
1796 }
1797 }
1798
1799 #[test]
1800 fn explaining_an_unknown_statement_is_empty() {
1801 let validator = prepared(&format!("{PREFIXES}{ON_DEMAND}"));
1802 let pair = SelectedPair::new(
1803 usize::MAX,
1804 oxrdf::NamedNode::new("http://ex/good").unwrap().into(),
1805 vec![],
1806 );
1807 assert!(validator.explain(&pair).is_empty());
1808 }
1809
1810 #[test]
1811 fn the_catalog_is_the_one_a_full_run_carries() {
1812 let validator = prepared(&format!("{PREFIXES}{ON_DEMAND}"));
1813 let full = validator.validate(&ValidationOptions::default());
1814 assert_eq!(validator.constraints(), full.constraints);
1815 }
1816 #[test]
1823 fn two_full_runs_agree() {
1824 let validator = prepared(&format!("{PREFIXES}{ON_DEMAND}"));
1825 let options = ValidationOptions::default();
1826 let first = validator.validate(&options);
1827 let second = validator.validate(&options);
1828 assert_eq!(first, second, "two runs over the same snapshot disagree");
1829 }
1830
1831 #[test]
1834 fn two_independent_validators_agree() {
1835 let options = ValidationOptions::default();
1836 let first = prepared(&format!("{PREFIXES}{ON_DEMAND}")).validate(&options);
1837 let second = prepared(&format!("{PREFIXES}{ON_DEMAND}")).validate(&options);
1838 assert_eq!(first, second, "two independent validators disagree");
1839 }
1840
1841 #[test]
1844 fn explaining_a_pair_matches_the_full_run() {
1845 let validator = prepared(&format!("{PREFIXES}{ON_DEMAND}"));
1846 let full = validator.validate(&ValidationOptions::default());
1847
1848 let mut checked = 0;
1849 for statement in &full.statements {
1850 let normalized = statement.normalized_statement_id.unwrap();
1851 for focus in &statement.selected_foci {
1852 let pair = SelectedPair::new(
1853 normalized,
1854 focus.focus.clone(),
1855 vec![statement.source_statement_id],
1856 );
1857 let explained = validator.explain(&pair);
1858 let matching = explained
1859 .iter()
1860 .find(|candidate| {
1861 candidate.source_statement_id == statement.source_statement_id
1862 })
1863 .expect("explain covers every authored statement of the pair");
1864 assert_eq!(&matching.selected_foci[0], focus);
1865 checked += 1;
1866 }
1867 }
1868 assert!(checked > 0, "fixture selected no pairs");
1869 }
1870}