axioval_engine/
relationships.rs

1//! Exact source-neutral relationship-selection host-service contracts.
2
3use std::sync::Arc;
4
5use axioval_ir::{Evidence, ObjectId};
6use thiserror::Error;
7
8/// Failure to select comparison candidates conclusively.
9#[derive(Clone, Debug, Error, PartialEq, Eq)]
10pub enum RelationshipSelectionError {
11    /// The requested relationship or candidate universe is malformed.
12    #[error("relationship selection request is invalid")]
13    InvalidRequest,
14    /// The candidate universe or response contains the same object more than once.
15    #[error("relationship selection contains a duplicate candidate")]
16    DuplicateCandidate,
17    /// A response repeats one evidence locator.
18    #[error("relationship selection contains duplicate evidence")]
19    DuplicateEvidence,
20    /// Returned data belongs to another request or escapes its candidate universe.
21    #[error("relationship selection response does not match its request")]
22    ResponseRequestMismatch,
23    /// A conclusive selection lacks exact, reviewable completeness evidence.
24    #[error("relationship selection evidence is not exact and reviewable")]
25    InexactEvidence,
26    /// The source cannot currently provide a conclusive selection.
27    #[error("relationship selection unavailable: {0}")]
28    Unavailable(String),
29}
30
31/// A host-registered semantic relationship or grouping identity.
32#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct SemanticRelationship(String);
34
35impl SemanticRelationship {
36    /// Creates a non-empty source-neutral relationship identity.
37    pub fn try_new(value: impl Into<String>) -> Result<Self, RelationshipSelectionError> {
38        let value = value.into();
39        if value.trim().is_empty() {
40            return Err(RelationshipSelectionError::InvalidRequest);
41        }
42        Ok(Self(value))
43    }
44
45    /// Returns the declared semantic identity.
46    #[must_use]
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52/// Direction used when traversing a directed semantic relationship.
53#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub enum TraversalDirection {
55    /// Follow edges from source to target.
56    Forward,
57    /// Follow edges from target to source.
58    Backward,
59    /// Follow edges in either direction.
60    Either,
61}
62
63/// Source-neutral relationship operation used to select candidates.
64#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
65pub enum RelationshipQuery {
66    /// Select members sharing at least one complete semantic group with the anchor.
67    SharedGroup {
68        /// Host-registered grouping identity such as a spatial or assembly context.
69        relationship: SemanticRelationship,
70    },
71    /// Traverse a directed semantic relationship from the anchor.
72    Related {
73        /// Host-registered relationship identity.
74        relationship: SemanticRelationship,
75        /// Requested traversal direction.
76        direction: TraversalDirection,
77        /// Whether traversal continues beyond immediate neighbors.
78        follow_chain: bool,
79    },
80}
81
82/// Request for relationship-selected objects within a caller-bound universe.
83#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
84pub struct RelationshipSelectionRequest {
85    anchor: ObjectId,
86    candidate_universe: Vec<ObjectId>,
87    query: RelationshipQuery,
88}
89
90impl RelationshipSelectionRequest {
91    /// Creates a request with a canonical, duplicate-free candidate universe.
92    pub fn try_new(
93        anchor: ObjectId,
94        mut candidate_universe: Vec<ObjectId>,
95        query: RelationshipQuery,
96    ) -> Result<Self, RelationshipSelectionError> {
97        candidate_universe.sort();
98        if candidate_universe.windows(2).any(|pair| pair[0] == pair[1]) {
99            return Err(RelationshipSelectionError::DuplicateCandidate);
100        }
101        Ok(Self {
102            anchor,
103            candidate_universe,
104            query,
105        })
106    }
107
108    /// Anchor whose relationships determine the selection.
109    #[must_use]
110    pub fn anchor(&self) -> &ObjectId {
111        &self.anchor
112    }
113
114    /// Complete caller-approved universe from which candidates may be returned.
115    #[must_use]
116    pub fn candidate_universe(&self) -> &[ObjectId] {
117        &self.candidate_universe
118    }
119
120    /// Requested relationship operation.
121    #[must_use]
122    pub fn query(&self) -> &RelationshipQuery {
123        &self.query
124    }
125
126    fn contains_candidate(&self, candidate: &ObjectId) -> bool {
127        self.candidate_universe.binary_search(candidate).is_ok()
128    }
129}
130
131/// Complete exact candidate selection bound to the request that produced it.
132#[derive(Clone, Debug, PartialEq)]
133pub struct CompleteRelationshipSelection {
134    request: RelationshipSelectionRequest,
135    candidates: Vec<ObjectId>,
136    evidence: Vec<Evidence>,
137}
138
139impl CompleteRelationshipSelection {
140    /// Creates a request-bound complete selection with canonical candidate ordering.
141    pub fn try_new(
142        request: RelationshipSelectionRequest,
143        mut candidates: Vec<ObjectId>,
144        mut evidence: Vec<Evidence>,
145    ) -> Result<Self, RelationshipSelectionError> {
146        candidates.sort();
147        if candidates.windows(2).any(|pair| pair[0] == pair[1]) {
148            return Err(RelationshipSelectionError::DuplicateCandidate);
149        }
150        if candidates
151            .iter()
152            .any(|candidate| !request.contains_candidate(candidate))
153        {
154            return Err(RelationshipSelectionError::ResponseRequestMismatch);
155        }
156        if evidence.is_empty() || evidence.iter().any(|item| !reviewable(item)) {
157            return Err(RelationshipSelectionError::InexactEvidence);
158        }
159        evidence.sort_by(|left, right| {
160            (&left.source, &left.locator).cmp(&(&right.source, &right.locator))
161        });
162        if evidence.windows(2).any(|pair| pair[0] == pair[1]) {
163            return Err(RelationshipSelectionError::DuplicateEvidence);
164        }
165        Ok(Self {
166            request,
167            candidates,
168            evidence,
169        })
170    }
171
172    /// Complete request, including anchor, universe, and query.
173    #[must_use]
174    pub fn request(&self) -> &RelationshipSelectionRequest {
175        &self.request
176    }
177
178    /// Canonically ordered selected candidates.
179    #[must_use]
180    pub fn candidates(&self) -> &[ObjectId] {
181        &self.candidates
182    }
183
184    /// Exact reviewable evidence proving the selection is complete.
185    #[must_use]
186    pub fn evidence(&self) -> &[Evidence] {
187        &self.evidence
188    }
189}
190
191/// Trusted adapter seam for complete relationship-based candidate selection.
192pub trait RelationshipSelectionService: Send + Sync {
193    /// Selects candidates or reports why the result is not conclusive.
194    fn select(
195        &self,
196        request: &RelationshipSelectionRequest,
197    ) -> Result<CompleteRelationshipSelection, RelationshipSelectionError>;
198}
199
200/// Cloneable, type-erased relationship service registered by the host.
201#[derive(Clone)]
202pub struct RelationshipSelectionServiceHandle(Arc<dyn RelationshipSelectionService>);
203
204impl RelationshipSelectionServiceHandle {
205    /// Wraps a trusted relationship-selection service.
206    #[must_use]
207    pub fn new(service: Arc<dyn RelationshipSelectionService>) -> Self {
208        Self(service)
209    }
210
211    /// Selects and validates complete request binding and evidence exactness.
212    pub fn select(
213        &self,
214        request: &RelationshipSelectionRequest,
215    ) -> Result<CompleteRelationshipSelection, RelationshipSelectionError> {
216        let selection = self.0.select(request)?;
217        if selection.request() != request
218            || selection
219                .candidates()
220                .iter()
221                .any(|candidate| !request.contains_candidate(candidate))
222        {
223            return Err(RelationshipSelectionError::ResponseRequestMismatch);
224        }
225        if selection
226            .candidates()
227            .windows(2)
228            .any(|pair| pair[0] >= pair[1])
229        {
230            return Err(RelationshipSelectionError::DuplicateCandidate);
231        }
232        if selection.evidence().is_empty()
233            || selection.evidence().iter().any(|item| !reviewable(item))
234        {
235            return Err(RelationshipSelectionError::InexactEvidence);
236        }
237        Ok(selection)
238    }
239}
240
241fn reviewable(evidence: &Evidence) -> bool {
242    evidence.exact && !evidence.locator.trim().is_empty()
243}