1use crate::compiled::CompiledShapes;
4use crate::context;
5use crate::evidence::{
6 ConformanceOptions, ConformanceRun, PreparedEvidenceValidator, SelectedPair,
7};
8use crate::frozen::FrozenIndexedDataset;
9use crate::gate::RepairOutcome;
10use crate::sparql::SparqlDiagnostic;
11use crate::validate::{
12 EngineOptions, Reason, UnsupportedPolicy, ValidationGraphMode, ValidationOptions,
13 ValidationOutcome, Violation,
14};
15use crate::witness::{EvidenceRun, StatementEvaluation};
16use oxrdf::{BlankNode, Graph, NamedOrBlankNode, Term, Triple};
17use shifty_algebra::Severity;
18use shifty_parse::{DiagLevel, Diagnostic};
19use shifty_repair::GraphDelta;
20use std::cell::{OnceCell, RefCell};
21use std::collections::{HashMap, HashSet};
22use std::fmt;
23use std::sync::Arc;
24
25#[expect(
26 clippy::large_enum_variant,
27 reason = "the documented public API consumes an owned Graph"
28)]
29pub enum SessionData {
30 Separate(Graph),
31 Embedded,
32}
33
34#[derive(Debug, Clone, Copy, Default)]
35pub struct SessionOptions {
36 pub graph_mode: ValidationGraphMode,
37 pub inference: bool,
38 pub engine: EngineOptions,
39}
40
41#[derive(Debug, Clone)]
42pub struct FindingOptions {
43 pub entry_shape_names: Vec<String>,
44 pub minimum_severity: Severity,
45 pub sort_results: bool,
46}
47
48impl Default for FindingOptions {
49 fn default() -> Self {
50 Self {
51 entry_shape_names: Vec::new(),
52 minimum_severity: Severity::Info,
53 sort_results: true,
54 }
55 }
56}
57
58impl FindingOptions {
59 fn validation(&self, engine: EngineOptions) -> ValidationOptions {
60 ValidationOptions {
61 entry_shape_names: self.entry_shape_names.clone(),
62 minimum_severity: self.minimum_severity.clone(),
63 sort_results: self.sort_results,
64 engine,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Default)]
70pub struct EvidenceOptions {
71 pub findings: FindingOptions,
72 pub include_progress: bool,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ExecutionDiagnostic {
77 pub message: String,
78}
79
80#[derive(Debug)]
81pub enum SessionError {
82 EmptyShapes,
83 UnsupportedFeatures(Vec<Diagnostic>),
84 StrictInference(Vec<ExecutionDiagnostic>),
85}
86
87impl fmt::Display for SessionError {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 match self {
90 Self::EmptyShapes => write!(f, "separate input requires a nonempty shapes graph"),
91 Self::UnsupportedFeatures(diagnostics) => write!(
92 f,
93 "unsupported shapes features: {}",
94 diagnostics
95 .iter()
96 .map(ToString::to_string)
97 .collect::<Vec<_>>()
98 .join("; ")
99 ),
100 Self::StrictInference(diagnostics) => write!(
101 f,
102 "strict inference failed: {}",
103 diagnostics
104 .iter()
105 .map(|diagnostic| diagnostic.message.as_str())
106 .collect::<Vec<_>>()
107 .join("; ")
108 ),
109 }
110 }
111}
112
113impl std::error::Error for SessionError {}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum EvaluationError {
119 ForeignPair,
120}
121
122impl fmt::Display for EvaluationError {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 match self {
125 Self::ForeignPair => write!(f, "selected pair belongs to another session"),
126 }
127 }
128}
129
130impl std::error::Error for EvaluationError {}
131
132pub struct EvaluationSession {
133 compiled: CompiledShapes,
134 asserted: Arc<Graph>,
135 evaluated: OnceCell<Arc<Graph>>,
136 separate: bool,
137 options: SessionOptions,
138 inferred: Vec<Triple>,
139 public_identity: OnceCell<PublicIdentity>,
140 public_data: OnceCell<Arc<Graph>>,
141 public_inferred: OnceCell<Vec<Triple>>,
142 inference_ran: bool,
143 diagnostics: Vec<ExecutionDiagnostic>,
144 inference_dataset: RefCell<Option<FrozenIndexedDataset>>,
145 prepared: OnceCell<PreparedEvidenceValidator>,
146 snapshot: Arc<()>,
147}
148
149struct PublicIdentity {
154 labels: HashMap<BlankNode, BlankNode>,
155 internal_labels: HashMap<BlankNode, BlankNode>,
156 data_aliases: HashMap<BlankNode, BlankNode>,
157 source_collisions: HashSet<BlankNode>,
158}
159
160#[derive(PartialEq, Eq, Hash)]
161enum ScopedTermKey {
162 Other(Term),
163 DataBlank(BlankNode),
164 ShapesBlank(BlankNode),
165}
166
167impl PublicIdentity {
168 fn from_dataset(dataset: &FrozenIndexedDataset) -> Self {
169 let data_aliases = dataset.data_blank_node_aliases();
170 let source_collisions = data_aliases.values().cloned().collect();
171 let labels = dataset.external_blank_node_map();
172 let internal_labels = labels.iter().map(|(a, b)| (b.clone(), a.clone())).collect();
173 Self {
174 labels,
175 internal_labels,
176 data_aliases,
177 source_collisions,
178 }
179 }
180
181 fn term(&self, term: &Term) -> Term {
182 match term {
183 Term::BlankNode(node) => Term::BlankNode(
184 self.labels
185 .get(node)
186 .cloned()
187 .unwrap_or_else(|| node.clone()),
188 ),
189 _ => term.clone(),
190 }
191 }
192
193 fn internal_term(&self, term: &Term) -> Term {
194 match term {
195 Term::BlankNode(node) => Term::BlankNode(
196 self.internal_labels
197 .get(node)
198 .cloned()
199 .unwrap_or_else(|| node.clone()),
200 ),
201 _ => term.clone(),
202 }
203 }
204
205 fn triple(&self, triple: &Triple) -> Triple {
206 let subject = match &triple.subject {
207 NamedOrBlankNode::BlankNode(node) => NamedOrBlankNode::BlankNode(
208 self.labels
209 .get(node)
210 .cloned()
211 .unwrap_or_else(|| node.clone()),
212 ),
213 named => named.clone(),
214 };
215 Triple::new(subject, triple.predicate.clone(), self.term(&triple.object))
216 }
217
218 fn key(&self, term: &Term) -> ScopedTermKey {
219 match term {
220 Term::BlankNode(node) if self.data_aliases.contains_key(node) => {
221 ScopedTermKey::DataBlank(self.data_aliases[node].clone())
222 }
223 Term::BlankNode(node) if self.source_collisions.contains(node) => {
224 ScopedTermKey::ShapesBlank(node.clone())
225 }
226 _ => ScopedTermKey::Other(term.clone()),
227 }
228 }
229
230 fn diagnostic(&self, diagnostic: &mut SparqlDiagnostic) {
231 for (_, term) in &mut diagnostic.bindings {
232 *term = self.term(term);
233 }
234 for row in &mut diagnostic.results {
235 for (_, term) in row {
236 *term = self.term(term);
237 }
238 }
239 }
240
241 fn reason(&self, reason: &mut Reason) {
242 reason.value = self.term(&reason.value);
243 if let Some(diagnostic) = &mut reason.sparql_diagnostic {
244 self.diagnostic(diagnostic);
245 }
246 for child in &mut reason.sub_reasons {
247 self.reason(child);
248 }
249 }
250
251 fn violation(&self, violation: &mut Violation) {
252 violation.focus = self.term(&violation.focus);
253 for reason in &mut violation.reasons {
254 self.reason(reason);
255 }
256 }
257
258 fn validation(&self, outcome: &mut ValidationOutcome) {
259 for violation in &mut outcome.violations {
260 self.violation(violation);
261 }
262 }
263
264 fn report(&self, report: &mut crate::report::ValidationReport) {
265 for result in &mut report.results {
266 result.focus = self.term(&result.focus);
267 result.path = result.path.as_ref().map(|term| self.term(term));
268 result.value = result.value.as_ref().map(|term| self.term(term));
269 result.source_shape = self.term(&result.source_shape);
270 for message in &mut result.messages {
271 *message = self.term(message);
272 }
273 if let Some(diagnostic) = &mut result.sparql_diagnostic {
274 self.diagnostic(diagnostic);
275 }
276 }
277 }
278
279 fn remap_evidence<T>(&self, value: T) -> T
280 where
281 T: serde::Serialize + serde::de::DeserializeOwned,
282 {
283 if self.labels.is_empty() {
284 return value;
285 }
286 let mut encoded = serde_json::to_value(value).expect("evidence serializes");
290 fn rewrite(value: &mut serde_json::Value, labels: &HashMap<String, String>) {
291 match value {
292 serde_json::Value::Object(object) => {
293 if object.get("type").and_then(serde_json::Value::as_str) == Some("bnode")
294 && let Some(serde_json::Value::String(label)) = object.get_mut("value")
295 && let Some(mapped) = labels.get(label)
296 {
297 *label = mapped.clone();
298 return;
299 }
300 for child in object.values_mut() {
301 rewrite(child, labels);
302 }
303 }
304 serde_json::Value::Array(items) => {
305 for child in items {
306 rewrite(child, labels);
307 }
308 }
309 _ => {}
310 }
311 }
312 let labels = self
313 .labels
314 .iter()
315 .map(|(from, to)| (from.as_str().to_owned(), to.as_str().to_owned()))
316 .collect();
317 rewrite(&mut encoded, &labels);
318 serde_json::from_value(encoded).expect("rewritten evidence deserializes")
319 }
320}
321
322impl CompiledShapes {
323 pub fn session(
324 &self,
325 data: SessionData,
326 options: SessionOptions,
327 ) -> Result<EvaluationSession, SessionError> {
328 let (asserted, separate) = match data {
329 SessionData::Separate(data) => {
330 if self.source().graph.is_empty() {
331 return Err(SessionError::EmptyShapes);
332 }
333 (data, true)
334 }
335 SessionData::Embedded => (self.source().graph.clone(), false),
336 };
337 self.session_from_asserted(asserted, separate, options)
338 }
339
340 fn session_from_asserted(
341 &self,
342 asserted: Graph,
343 separate: bool,
344 options: SessionOptions,
345 ) -> Result<EvaluationSession, SessionError> {
346 if options.engine.unsupported == UnsupportedPolicy::Error {
347 let unsupported: Vec<_> = self
348 .diagnostics()
349 .iter()
350 .filter(|diagnostic| diagnostic.level == DiagLevel::Unsupported)
351 .cloned()
352 .collect();
353 if !unsupported.is_empty() {
354 return Err(SessionError::UnsupportedFeatures(unsupported));
355 }
356 }
357 let asserted = Arc::new(asserted);
358 let (evaluated, inferred, diagnostics, inference_dataset, public_identity) =
362 if options.inference && !self.inner.rules.is_empty() {
363 let run = crate::infer::infer_with_compiled_functions(
364 &asserted,
365 &self.inner.normalized,
366 &options.engine,
367 &self.inner.functions,
368 &self.inner.rules,
369 (self.source_storage(), separate, self.parsed_queries()),
370 );
371 let diagnostics: Vec<_> = run
372 .diagnostics
373 .into_iter()
374 .map(|message| ExecutionDiagnostic { message })
375 .collect();
376 if options.engine.unsupported == UnsupportedPolicy::Error && !diagnostics.is_empty()
377 {
378 return Err(SessionError::StrictInference(diagnostics));
379 }
380 let public_identity = Some(PublicIdentity::from_dataset(&run.dataset));
381 let mut inference_dataset = Some(run.dataset);
382 if separate
383 && options.graph_mode == ValidationGraphMode::Data
384 && let Some(dataset) = &mut inference_dataset
385 {
386 dataset.select_data_view();
387 }
388 (
389 None,
390 run.inferred,
391 diagnostics,
392 inference_dataset,
393 public_identity,
394 )
395 } else {
396 (
397 Some(Arc::clone(&asserted)),
398 Vec::new(),
399 Vec::new(),
400 None,
401 None,
402 )
403 };
404 let evaluated_cell = OnceCell::new();
405 if let Some(graph) = evaluated {
406 evaluated_cell.set(graph).expect("new evaluated graph cell");
407 }
408 let identity_cell = OnceCell::new();
409 if let Some(identity) = public_identity {
410 identity_cell.set(identity).ok();
411 }
412 Ok(EvaluationSession {
413 compiled: self.clone(),
414 asserted,
415 evaluated: evaluated_cell,
416 separate,
417 options,
418 inferred,
419 public_identity: identity_cell,
420 public_data: OnceCell::new(),
421 public_inferred: OnceCell::new(),
422 inference_ran: options.inference && !self.inner.rules.is_empty(),
423 diagnostics,
424 inference_dataset: RefCell::new(inference_dataset),
425 prepared: OnceCell::new(),
426 snapshot: Arc::new(()),
427 })
428 }
429}
430
431impl EvaluationSession {
432 pub fn internal_term(&self, term: &Term) -> Term {
435 self.identity().internal_term(term)
436 }
437
438 #[cfg(test)]
439 pub(crate) fn has_prepared_dataset(&self) -> bool {
440 self.prepared.get().is_some()
441 }
442
443 fn prepared(&self) -> &PreparedEvidenceValidator {
444 self.prepared.get_or_init(|| {
445 let frozen = self
446 .inference_dataset
447 .borrow_mut()
448 .take()
449 .unwrap_or_else(|| {
450 context::frozen(
451 self.evaluated.get().expect("dataset or evaluated graph"),
452 self.compiled.source_storage(),
453 self.separate,
454 self.options.graph_mode,
455 )
456 });
457 self.public_identity
458 .get_or_init(|| PublicIdentity::from_dataset(&frozen));
459 PreparedEvidenceValidator::from_compiled(
460 self.evaluated.get().cloned(),
461 &self.compiled,
462 frozen,
463 true,
464 self.options.engine.unsupported,
465 if self.separate && self.options.graph_mode != ValidationGraphMode::UnionAll {
466 crate::focus::FocusScope::Data
467 } else {
468 crate::focus::FocusScope::Default
469 },
470 self.separate && self.options.graph_mode == ValidationGraphMode::UnionAll,
471 )
472 })
473 }
474
475 fn identity(&self) -> &PublicIdentity {
476 if self.public_identity.get().is_none() {
477 self.prepared();
480 }
481 self.public_identity
482 .get()
483 .expect("prepared session has blank-node identity")
484 }
485
486 fn evaluated_graph(&self) -> &Arc<Graph> {
487 self.evaluated.get_or_init(|| {
488 if let Some(dataset) = self.inference_dataset.borrow().as_ref() {
489 return Arc::new(dataset.data_graph_projection());
490 }
491 self.prepared().data_graph_shared()
492 })
493 }
494
495 pub fn prepared_evidence(&self) -> &PreparedEvidenceValidator {
497 self.prepared()
498 }
499
500 fn validate_internal(&self, options: &FindingOptions) -> ValidationOutcome {
501 self.prepared().validate_findings(
502 self.compiled.physical_plan(),
503 &options.validation(self.options.engine),
504 )
505 }
506
507 pub fn validate(&self, options: &FindingOptions) -> ValidationOutcome {
508 let mut outcome = self.validate_internal(options);
509 self.identity().validation(&mut outcome);
510 outcome
511 }
512
513 pub fn report(&self, options: &FindingOptions) -> crate::report::ValidationReport {
514 let mut report = self.prepared().report(
515 self.compiled.source(),
516 &options.validation(self.options.engine),
517 );
518 self.identity().report(&mut report);
519 report
520 }
521
522 pub fn property_witnesses(
523 &self,
524 key_path: Option<&shifty_algebra::Path>,
525 options: &FindingOptions,
526 ) -> Vec<crate::report::PropertyWitness> {
527 let mut witnesses = self.prepared().property_witnesses(
528 self.compiled.source(),
529 key_path,
530 &options.validation(self.options.engine),
531 );
532 let identity = self.identity();
533 for witness in &mut witnesses {
534 witness.focus = identity.term(&witness.focus);
535 witness.shape = identity.term(&witness.shape);
536 witness.key = identity.term(&witness.key);
537 for value in &mut witness.values {
538 *value = identity.term(value);
539 }
540 }
541 witnesses
542 }
543
544 pub fn evidence(&self, options: &EvidenceOptions) -> EvidenceRun {
545 let validation = options.findings.validation(self.options.engine);
546 self.evidence_with_validation(&validation, options.include_progress)
547 }
548
549 pub fn evidence_with_validation(
550 &self,
551 validation: &ValidationOptions,
552 include_progress: bool,
553 ) -> EvidenceRun {
554 let run = if include_progress {
555 self.prepared().validate(validation)
556 } else {
557 self.prepared().validate_canonical(validation)
558 };
559 self.identity().remap_evidence(run)
560 }
561
562 pub fn conformance(&self, options: &ConformanceOptions) -> ConformanceRun {
563 self.prepared().validate_conformance(options)
564 }
565
566 pub fn find_failures(
567 &self,
568 options: &ConformanceOptions,
569 ) -> (ConformanceRun, Vec<SelectedPair>) {
570 let (run, mut pairs) = self.prepared().find_failures(options);
571 let identity = self.identity();
572 for pair in &mut pairs {
573 if !identity.labels.is_empty() {
574 pair.public_focus = Some(identity.term(pair.focus()));
575 }
576 pair.snapshot = Some(Arc::clone(&self.snapshot));
577 }
578 (run, pairs)
579 }
580
581 pub fn explain(
582 &self,
583 pair: &SelectedPair,
584 ) -> Result<Vec<StatementEvaluation>, EvaluationError> {
585 self.check_pair(pair)?;
586 Ok(self
587 .identity()
588 .remap_evidence(self.prepared().explain(pair)))
589 }
590
591 pub fn explain_canonical(
592 &self,
593 pair: &SelectedPair,
594 ) -> Result<Vec<StatementEvaluation>, EvaluationError> {
595 self.check_pair(pair)?;
596 Ok(self
597 .identity()
598 .remap_evidence(self.prepared().explain_canonical(pair)))
599 }
600
601 fn check_pair(&self, pair: &SelectedPair) -> Result<(), EvaluationError> {
602 if !pair
603 .snapshot
604 .as_ref()
605 .is_some_and(|snapshot| Arc::ptr_eq(snapshot, &self.snapshot))
606 {
607 return Err(EvaluationError::ForeignPair);
608 }
609 Ok(())
610 }
611
612 pub fn with_delta(&self, delta: &GraphDelta) -> Result<Self, SessionError> {
613 let patched = crate::gate::apply(&self.asserted, delta);
614 self.compiled
615 .session_from_asserted(patched, self.separate, self.options)
616 }
617
618 pub fn with_delta_and_inference(
622 &self,
623 delta: &GraphDelta,
624 inference: bool,
625 ) -> Result<Self, SessionError> {
626 if inference == self.options.inference {
627 return self.with_delta(delta);
628 }
629 let baseline = if inference {
630 self.asserted.as_ref()
631 } else {
632 self.evaluated_graph()
633 };
634 let patched = crate::gate::apply(baseline, delta);
635 let mut options = self.options;
636 options.inference = inference;
637 self.compiled
638 .session_from_asserted(patched, self.separate, options)
639 }
640
641 pub fn data(&self) -> &Graph {
642 self.public_data()
643 }
644
645 pub fn data_shared(&self) -> Arc<Graph> {
647 Arc::clone(self.public_data())
648 }
649
650 pub fn public_shapes_graph_if_relabelled(&self) -> Option<Graph> {
653 let identity = self.identity();
654 if identity.labels.is_empty() {
655 return None;
656 }
657 let mut graph = Graph::new();
658 for triple in self.compiled.source().graph.iter() {
659 graph.insert(&identity.triple(&triple.into_owned()));
660 }
661 Some(graph)
662 }
663
664 pub fn public_schema(
667 &self,
668 schema: Arc<shifty_algebra::Schema>,
669 ) -> Arc<shifty_algebra::Schema> {
670 if self.identity().labels.is_empty() {
671 schema
672 } else {
673 Arc::new(self.identity().remap_evidence(schema.as_ref().clone()))
674 }
675 }
676
677 fn public_data(&self) -> &Arc<Graph> {
678 if !self.inference_ran || self.identity().labels.is_empty() {
679 return self.evaluated_graph();
680 }
681 self.public_data.get_or_init(|| {
682 let identity = self.identity();
683 if let Some(dataset) = self.inference_dataset.borrow().as_ref() {
684 return Arc::new(dataset.data_graph_projection_mapped(&identity.labels));
685 }
686 Arc::new(
687 self.prepared()
688 .data_graph_projection_mapped(&identity.labels),
689 )
690 })
691 }
692
693 pub fn asserted_shared(&self) -> Arc<Graph> {
695 Arc::clone(&self.asserted)
696 }
697
698 pub fn inferred(&self) -> &[Triple] {
699 if self.identity().labels.is_empty() {
700 return &self.inferred;
701 }
702 self.public_inferred.get_or_init(|| {
703 self.inferred
704 .iter()
705 .map(|triple| self.identity().triple(triple))
706 .collect()
707 })
708 }
709
710 pub fn inferred_count(&self) -> usize {
712 self.inferred.len()
713 }
714
715 pub fn inferred_for_write_back(&self) -> Vec<Triple> {
718 self.inferred().to_vec()
719 }
720
721 pub fn diagnostics(&self) -> &[ExecutionDiagnostic] {
722 &self.diagnostics
723 }
724
725 pub fn gate(&self, delta: &GraphDelta) -> Result<RepairOutcome, SessionError> {
726 let baseline = self.validate_internal(&FindingOptions::default());
727 let additions: HashSet<_> = delta.add.iter().collect();
728 let changed = additions
729 .iter()
730 .any(|triple| !self.asserted.contains(*triple))
731 || delta
732 .delete
733 .iter()
734 .any(|triple| !additions.contains(triple) && self.asserted.contains(triple));
735 if !changed {
736 let mut remaining = baseline.violations;
737 for violation in &mut remaining {
738 self.identity().violation(violation);
739 }
740 return Ok(RepairOutcome {
741 remaining,
742 ..RepairOutcome::default()
743 });
744 }
745 let candidate = self.with_delta(delta)?;
746 let patched = candidate.validate_internal(&FindingOptions::default());
747 let baseline_keys: HashSet<_> = baseline
748 .violations
749 .iter()
750 .map(|violation| (self.identity().key(&violation.focus), violation.statement))
751 .collect();
752 let patched_keys: HashSet<_> = patched
753 .violations
754 .iter()
755 .map(|violation| {
756 (
757 candidate.identity().key(&violation.focus),
758 violation.statement,
759 )
760 })
761 .collect();
762 let mut outcome = RepairOutcome::default();
763 for mut violation in baseline.violations {
764 if !patched_keys.contains(&(self.identity().key(&violation.focus), violation.statement))
765 {
766 self.identity().violation(&mut violation);
767 outcome.fixed.push(violation);
768 }
769 }
770 for mut violation in patched.violations {
771 let old = baseline_keys.contains(&(
772 candidate.identity().key(&violation.focus),
773 violation.statement,
774 ));
775 candidate.identity().violation(&mut violation);
776 if old {
777 outcome.remaining.push(violation);
778 } else {
779 outcome.introduced.push(violation);
780 }
781 }
782 Ok(outcome)
783 }
784}