1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! Source integrity: facts about a source's own consistency, reported as warnings.
//!
//! Rule evaluation answers questions about a model; integrity answers whether
//! the source is shaped the way its own schema says it must be. The two are
//! kept apart on purpose. A source irregularity is not a rule finding (no rule
//! was violated) and not a not-evaluated outcome (nothing was asked), so it
//! gets its own channel. A host shows these as warnings next to the report,
//! and a rule that depends on the affected data either refuses or, when it
//! opted in, skips the instance and cites it.
use std::sync::Arc;
use axioval_ir::{Evidence, SourceId};
use thiserror::Error;
use crate::session::{SnapshotBoundService, SourceSnapshot};
/// How much an integrity issue undermines evidence drawn from the source.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum IntegritySeverity {
/// A known, bounded deviation: rules refuse by default and may opt in to
/// skip it, e.g. a relationship end the schema requires but real exporters
/// omit.
Warning,
/// The record cannot be read at all, e.g. a reference to a missing
/// entity. No rule can opt out of this.
Error,
}
/// One irregularity the source's own schema does not allow.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IntegrityIssue {
/// Stable machine-readable code, e.g. `relationship.absent-required-end`.
pub code: String,
/// Whether the issue is skippable (warning) or corrupts evidence (error).
pub severity: IntegritySeverity,
/// Human-readable description of this occurrence.
pub message: String,
/// Exact locator of the offending source record.
pub evidence: Evidence,
}
/// Failure to produce a complete integrity report.
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum IntegrityError {
/// The service does not cover the requested source.
#[error("integrity service does not cover source `{0}`")]
UncoveredSource(SourceId),
/// An issue was reported without reviewable exact evidence.
#[error("integrity issue lacks reviewable exact evidence")]
InexactEvidence,
/// The service could not complete the scan.
#[error("integrity scan unavailable: {0}")]
Unavailable(String),
}
/// Adapter seam listing a source's integrity issues.
pub trait SourceIntegrityService: Send + Sync {
/// Exact source snapshots the scan covers.
fn source_snapshots(&self) -> &[SourceSnapshot];
/// Every issue in one covered source, in a deterministic order.
fn issues(&self, source: &SourceId) -> Result<Vec<IntegrityIssue>, IntegrityError>;
}
/// Cloneable, type-erased integrity service registered by the host.
#[derive(Clone)]
pub struct SourceIntegrityServiceHandle(Arc<dyn SourceIntegrityService>);
impl SourceIntegrityServiceHandle {
/// Wraps a trusted integrity service.
#[must_use]
pub fn new(service: Arc<dyn SourceIntegrityService>) -> Self {
Self(service)
}
/// Lists issues, validating coverage, evidence exactness and source binding.
pub fn issues(&self, source: &SourceId) -> Result<Vec<IntegrityIssue>, IntegrityError> {
if !self
.0
.source_snapshots()
.iter()
.any(|snapshot| snapshot.source() == source)
{
return Err(IntegrityError::UncoveredSource(source.clone()));
}
let mut issues = self.0.issues(source)?;
if issues.iter().any(|issue| {
!issue.evidence.exact
|| issue.evidence.locator.trim().is_empty()
|| issue.evidence.source != *source
|| issue.code.trim().is_empty()
}) {
return Err(IntegrityError::InexactEvidence);
}
// Deterministic for hosts and diffs regardless of adapter order.
issues.sort_by(|left, right| {
(
&left.evidence.locator,
&left.code,
left.severity,
&left.message,
)
.cmp(&(
&right.evidence.locator,
&right.code,
right.severity,
&right.message,
))
});
issues.dedup();
Ok(issues)
}
}
impl SnapshotBoundService for SourceIntegrityServiceHandle {
fn source_snapshots(&self) -> &[SourceSnapshot] {
self.0.source_snapshots()
}
}