1use std::sync::Arc;
4
5use axioval_ir::{Evidence, ObjectId};
6use thiserror::Error;
7
8use crate::session::{SnapshotBoundService, SourceSnapshot};
9
10#[derive(Clone, Debug, Error, PartialEq, Eq)]
12pub enum RelationshipSelectionError {
13 #[error("relationship selection request is invalid")]
15 InvalidRequest,
16 #[error("relationship selection contains a duplicate candidate")]
18 DuplicateCandidate,
19 #[error("relationship selection contains duplicate evidence")]
21 DuplicateEvidence,
22 #[error("relationship selection response does not match its request")]
24 ResponseRequestMismatch,
25 #[error("relationship selection evidence is not exact and reviewable")]
27 InexactEvidence,
28 #[error("relationship selection unavailable: {0}")]
30 Unavailable(String),
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct SemanticRelationship(String);
36
37impl SemanticRelationship {
38 pub fn try_new(value: impl Into<String>) -> Result<Self, RelationshipSelectionError> {
40 let value = value.into();
41 if value.trim().is_empty() {
42 return Err(RelationshipSelectionError::InvalidRequest);
43 }
44 Ok(Self(value))
45 }
46
47 #[must_use]
49 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
56pub enum TraversalDirection {
57 Forward,
59 Backward,
61 Either,
63}
64
65#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
67pub enum RelationshipQuery {
68 SharedGroup {
70 relationship: SemanticRelationship,
72 },
73 Related {
75 relationship: SemanticRelationship,
77 direction: TraversalDirection,
79 follow_chain: bool,
81 },
82}
83
84impl RelationshipQuery {
85 #[must_use]
87 pub fn relationship(&self) -> &SemanticRelationship {
88 match self {
89 Self::SharedGroup { relationship } | Self::Related { relationship, .. } => relationship,
90 }
91 }
92}
93
94#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
103pub enum AbsentEndPolicy {
104 #[default]
106 Refuse,
107 Skip,
109}
110
111#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
113pub struct RelationshipSelectionRequest {
114 anchor: ObjectId,
115 candidate_universe: Vec<ObjectId>,
116 query: RelationshipQuery,
117 absent_ends: AbsentEndPolicy,
118}
119
120impl RelationshipSelectionRequest {
121 pub fn try_new(
123 anchor: ObjectId,
124 mut candidate_universe: Vec<ObjectId>,
125 query: RelationshipQuery,
126 ) -> Result<Self, RelationshipSelectionError> {
127 candidate_universe.sort();
128 if candidate_universe.windows(2).any(|pair| pair[0] == pair[1]) {
129 return Err(RelationshipSelectionError::DuplicateCandidate);
130 }
131 Ok(Self {
132 anchor,
133 candidate_universe,
134 query,
135 absent_ends: AbsentEndPolicy::default(),
136 })
137 }
138
139 #[must_use]
141 pub fn with_absent_ends(mut self, policy: AbsentEndPolicy) -> Self {
142 self.absent_ends = policy;
143 self
144 }
145
146 #[must_use]
148 pub fn absent_ends(&self) -> AbsentEndPolicy {
149 self.absent_ends
150 }
151
152 #[must_use]
154 pub fn anchor(&self) -> &ObjectId {
155 &self.anchor
156 }
157
158 #[must_use]
160 pub fn candidate_universe(&self) -> &[ObjectId] {
161 &self.candidate_universe
162 }
163
164 #[must_use]
166 pub fn query(&self) -> &RelationshipQuery {
167 &self.query
168 }
169
170 fn contains_candidate(&self, candidate: &ObjectId) -> bool {
171 self.candidate_universe.binary_search(candidate).is_ok()
172 }
173}
174
175#[derive(Clone, Debug, PartialEq)]
177pub struct CompleteRelationshipSelection {
178 request: RelationshipSelectionRequest,
179 candidates: Vec<ObjectId>,
180 evidence: Vec<Evidence>,
181}
182
183impl CompleteRelationshipSelection {
184 pub fn try_new(
186 request: RelationshipSelectionRequest,
187 mut candidates: Vec<ObjectId>,
188 mut evidence: Vec<Evidence>,
189 ) -> Result<Self, RelationshipSelectionError> {
190 candidates.sort();
191 if candidates.windows(2).any(|pair| pair[0] == pair[1]) {
192 return Err(RelationshipSelectionError::DuplicateCandidate);
193 }
194 if candidates
195 .iter()
196 .any(|candidate| !request.contains_candidate(candidate))
197 {
198 return Err(RelationshipSelectionError::ResponseRequestMismatch);
199 }
200 if evidence.is_empty() || evidence.iter().any(|item| !reviewable(item)) {
201 return Err(RelationshipSelectionError::InexactEvidence);
202 }
203 evidence.sort_by(|left, right| {
204 (&left.source, &left.locator).cmp(&(&right.source, &right.locator))
205 });
206 if evidence.windows(2).any(|pair| pair[0] == pair[1]) {
207 return Err(RelationshipSelectionError::DuplicateEvidence);
208 }
209 Ok(Self {
210 request,
211 candidates,
212 evidence,
213 })
214 }
215
216 #[must_use]
218 pub fn request(&self) -> &RelationshipSelectionRequest {
219 &self.request
220 }
221
222 #[must_use]
224 pub fn candidates(&self) -> &[ObjectId] {
225 &self.candidates
226 }
227
228 #[must_use]
230 pub fn evidence(&self) -> &[Evidence] {
231 &self.evidence
232 }
233}
234
235pub trait RelationshipSelectionService: Send + Sync {
237 fn source_snapshots(&self) -> &[SourceSnapshot] {
242 &[]
243 }
244 fn select(
246 &self,
247 request: &RelationshipSelectionRequest,
248 ) -> Result<CompleteRelationshipSelection, RelationshipSelectionError>;
249}
250
251#[derive(Clone)]
253pub struct RelationshipSelectionServiceHandle(Arc<dyn RelationshipSelectionService>);
254
255impl RelationshipSelectionServiceHandle {
256 #[must_use]
258 pub fn new(service: Arc<dyn RelationshipSelectionService>) -> Self {
259 Self(service)
260 }
261
262 pub fn select(
264 &self,
265 request: &RelationshipSelectionRequest,
266 ) -> Result<CompleteRelationshipSelection, RelationshipSelectionError> {
267 validate_selection(request, self.0.select(request)?)
268 }
269}
270
271pub(crate) fn validate_selection(
274 request: &RelationshipSelectionRequest,
275 selection: CompleteRelationshipSelection,
276) -> Result<CompleteRelationshipSelection, RelationshipSelectionError> {
277 if selection.request() != request
278 || selection
279 .candidates()
280 .iter()
281 .any(|candidate| !request.contains_candidate(candidate))
282 {
283 return Err(RelationshipSelectionError::ResponseRequestMismatch);
284 }
285 if selection
286 .candidates()
287 .windows(2)
288 .any(|pair| pair[0] >= pair[1])
289 {
290 return Err(RelationshipSelectionError::DuplicateCandidate);
291 }
292 if selection.evidence().is_empty() || selection.evidence().iter().any(|item| !reviewable(item))
293 {
294 return Err(RelationshipSelectionError::InexactEvidence);
295 }
296 Ok(selection)
297}
298
299impl SnapshotBoundService for RelationshipSelectionServiceHandle {
300 fn source_snapshots(&self) -> &[SourceSnapshot] {
301 self.0.source_snapshots()
302 }
303}
304
305fn reviewable(evidence: &Evidence) -> bool {
306 evidence.exact && !evidence.locator.trim().is_empty()
307}