axioval_engine/
metric_routing.rs

1//! Source-neutral metric-routing evidence and host-service contracts.
2//!
3//! Geometry algorithms do not live here. A trusted Axiolid or alternate backend
4//! supplies this interface after adapting its native geometry into validated,
5//! source-qualified evidence.
6
7use std::sync::Arc;
8
9use axioval_ir::{Evidence, ObjectId};
10use thiserror::Error;
11
12/// Fail-closed metric routing errors.
13#[derive(Clone, Debug, Error, PartialEq, Eq)]
14pub enum MetricRoutingError {
15    /// A coordinate was NaN or infinite.
16    #[error("metric point coordinates must be finite")]
17    InvalidCoordinate,
18    /// A scalar length was negative, non-finite, or had reversed bounds.
19    #[error("metric length interval is invalid")]
20    InvalidLengthInterval,
21    /// A mobility dimension was negative or non-finite.
22    #[error("mobility profile contains an invalid dimension")]
23    InvalidMobilityProfile,
24    /// A route response omitted its path or traversed-object evidence.
25    #[error("metric route evidence is empty")]
26    EmptyRouteEvidence,
27    /// Route provenance was approximate or blank.
28    #[error("metric route provenance is not exact and reviewable")]
29    InexactRouteEvidence,
30    /// A blocked verdict did not prove complete obstacle/topology coverage.
31    #[error("metric evidence is incomplete")]
32    IncompleteMetricEvidence,
33    /// A backend returned a route for different endpoints than requested.
34    #[error("metric routing backend returned mismatched endpoints")]
35    ResponseEndpointMismatch,
36    /// Required geometry was not available for the named object.
37    #[error("metric geometry is unavailable for `{0}`")]
38    MissingGeometry(Box<ObjectId>),
39    /// The backend deliberately refused an unsupported or partial query.
40    #[error("metric routing query unavailable: {0}")]
41    Unavailable(String),
42}
43
44/// Three-valued result for comparing bounded evidence with a policy threshold.
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum ThresholdVerdict {
47    /// Every value in the interval meets the maximum.
48    Satisfied,
49    /// Every value in the interval exceeds the maximum.
50    Violated,
51    /// Bounds straddle the maximum, so policy evaluation must not guess.
52    Indeterminate,
53}
54
55/// Conservative bounds for a non-negative metric length in metres.
56#[derive(Clone, Copy, Debug, PartialEq)]
57pub struct LengthInterval {
58    lower_metres: f64,
59    upper_metres: f64,
60}
61
62impl LengthInterval {
63    /// Validates inclusive lower and upper distance bounds.
64    pub fn try_new(lower_metres: f64, upper_metres: f64) -> Result<Self, MetricRoutingError> {
65        if !valid_non_negative(lower_metres)
66            || !valid_non_negative(upper_metres)
67            || lower_metres > upper_metres
68        {
69            return Err(MetricRoutingError::InvalidLengthInterval);
70        }
71        Ok(Self {
72            lower_metres,
73            upper_metres,
74        })
75    }
76
77    /// Creates a zero-error interval.
78    pub fn exact(metres: f64) -> Result<Self, MetricRoutingError> {
79        Self::try_new(metres, metres)
80    }
81
82    /// Inclusive lower bound in metres.
83    pub fn lower_metres(&self) -> f64 {
84        self.lower_metres
85    }
86
87    /// Inclusive upper bound in metres.
88    pub fn upper_metres(&self) -> f64 {
89        self.upper_metres
90    }
91
92    /// Whether the interval proves one exact value.
93    #[allow(clippy::float_cmp)]
94    pub fn is_exact(&self) -> bool {
95        // The exact constructor writes the same validated scalar to both fields;
96        // this tests evidence identity, not numerical convergence.
97        self.lower_metres == self.upper_metres
98    }
99
100    /// Compares this interval to an inclusive maximum without collapsing uncertainty.
101    pub fn compare_maximum(
102        &self,
103        maximum_metres: f64,
104    ) -> Result<ThresholdVerdict, MetricRoutingError> {
105        if !valid_non_negative(maximum_metres) {
106            return Err(MetricRoutingError::InvalidLengthInterval);
107        }
108        if self.upper_metres <= maximum_metres {
109            Ok(ThresholdVerdict::Satisfied)
110        } else if self.lower_metres > maximum_metres {
111            Ok(ThresholdVerdict::Violated)
112        } else {
113            Ok(ThresholdVerdict::Indeterminate)
114        }
115    }
116}
117
118/// A source-qualified object-grounded point expressed in canonical metres.
119#[derive(Clone, Debug, PartialEq)]
120pub struct MetricPoint {
121    subject: ObjectId,
122    coordinates_metres: [f64; 3],
123}
124
125impl MetricPoint {
126    /// Validates a model-grounded point.
127    pub fn try_new(
128        subject: ObjectId,
129        coordinates_metres: [f64; 3],
130    ) -> Result<Self, MetricRoutingError> {
131        if !coordinates_metres.iter().all(|value| value.is_finite()) {
132            return Err(MetricRoutingError::InvalidCoordinate);
133        }
134        Ok(Self {
135            subject,
136            coordinates_metres,
137        })
138    }
139
140    /// Object grounding this point.
141    pub fn subject(&self) -> &ObjectId {
142        &self.subject
143    }
144
145    /// Canonical coordinates in metres.
146    pub fn coordinates_metres(&self) -> [f64; 3] {
147        self.coordinates_metres
148    }
149}
150
151/// Geometry-independent mobility envelope used by route providers.
152#[derive(Clone, Copy, Debug, PartialEq)]
153pub struct MobilityProfile {
154    radius_metres: f64,
155    height_metres: f64,
156    maximum_step_metres: f64,
157    maximum_slope: f64,
158}
159
160impl MobilityProfile {
161    /// Validates non-negative finite mobility dimensions.
162    pub fn try_new(
163        radius_metres: f64,
164        height_metres: f64,
165        maximum_step_metres: f64,
166        maximum_slope: f64,
167    ) -> Result<Self, MetricRoutingError> {
168        if ![
169            radius_metres,
170            height_metres,
171            maximum_step_metres,
172            maximum_slope,
173        ]
174        .into_iter()
175        .all(valid_non_negative)
176        {
177            return Err(MetricRoutingError::InvalidMobilityProfile);
178        }
179        Ok(Self {
180            radius_metres,
181            height_metres,
182            maximum_step_metres,
183            maximum_slope,
184        })
185    }
186
187    /// Agent radius in metres.
188    pub fn radius_metres(&self) -> f64 {
189        self.radius_metres
190    }
191
192    /// Required clear height in metres.
193    pub fn height_metres(&self) -> f64 {
194        self.height_metres
195    }
196
197    /// Maximum traversable step in metres.
198    pub fn maximum_step_metres(&self) -> f64 {
199        self.maximum_step_metres
200    }
201
202    /// Maximum dimensionless slope ratio.
203    pub fn maximum_slope(&self) -> f64 {
204        self.maximum_slope
205    }
206}
207
208/// One source-neutral metric routing request.
209#[derive(Clone, Debug, PartialEq)]
210pub struct MetricRouteRequest {
211    origin: MetricPoint,
212    destination: MetricPoint,
213    profile: MobilityProfile,
214}
215
216impl MetricRouteRequest {
217    /// Creates a request from already validated values.
218    pub fn new(origin: MetricPoint, destination: MetricPoint, profile: MobilityProfile) -> Self {
219        Self {
220            origin,
221            destination,
222            profile,
223        }
224    }
225
226    /// Route origin.
227    pub fn origin(&self) -> &MetricPoint {
228        &self.origin
229    }
230
231    /// Route destination.
232    pub fn destination(&self) -> &MetricPoint {
233        &self.destination
234    }
235
236    /// Mobility envelope.
237    pub fn profile(&self) -> MobilityProfile {
238        self.profile
239    }
240}
241
242/// Provenance proving complete topology and obstacle coverage for a negative verdict.
243#[derive(Clone, Debug, PartialEq, Eq)]
244pub struct CompleteMetricEvidence(Evidence);
245
246impl CompleteMetricEvidence {
247    /// Promotes only exact, reviewable completeness evidence.
248    pub fn try_new(evidence: Evidence) -> Result<Self, MetricRoutingError> {
249        if !reviewable_exact_evidence(&evidence) {
250            return Err(MetricRoutingError::IncompleteMetricEvidence);
251        }
252        Ok(Self(evidence))
253    }
254
255    /// Completeness provenance.
256    pub fn evidence(&self) -> &Evidence {
257        &self.0
258    }
259}
260
261/// A negative route verdict bound to the exact request and complete evidence.
262#[derive(Clone, Debug, PartialEq)]
263pub struct BlockedMetricRouteEvidence {
264    request: MetricRouteRequest,
265    completeness: CompleteMetricEvidence,
266}
267
268impl BlockedMetricRouteEvidence {
269    /// Binds complete topology and obstacle evidence to one request.
270    pub fn new(request: MetricRouteRequest, completeness: CompleteMetricEvidence) -> Self {
271        Self {
272            request,
273            completeness,
274        }
275    }
276
277    /// Request proven blocked.
278    pub fn request(&self) -> &MetricRouteRequest {
279        &self.request
280    }
281
282    /// Exact completeness provenance.
283    pub fn completeness(&self) -> &CompleteMetricEvidence {
284        &self.completeness
285    }
286}
287
288/// A known route and conservative shortest-distance bounds.
289#[derive(Clone, Debug, PartialEq)]
290pub struct MetricRouteEvidence {
291    shortest_distance: LengthInterval,
292    waypoints: Vec<MetricPoint>,
293    traversed_objects: Vec<ObjectId>,
294    evidence: Evidence,
295}
296
297impl MetricRouteEvidence {
298    /// Validates known-route evidence without upgrading bounded distance to exact.
299    pub fn try_new(
300        shortest_distance: LengthInterval,
301        waypoints: Vec<MetricPoint>,
302        traversed_objects: Vec<ObjectId>,
303        evidence: Evidence,
304    ) -> Result<Self, MetricRoutingError> {
305        if waypoints.is_empty() || traversed_objects.is_empty() {
306            return Err(MetricRoutingError::EmptyRouteEvidence);
307        }
308        if !reviewable_exact_evidence(&evidence) {
309            return Err(MetricRoutingError::InexactRouteEvidence);
310        }
311        Ok(Self {
312            shortest_distance,
313            waypoints,
314            traversed_objects,
315            evidence,
316        })
317    }
318
319    /// Conservative shortest-distance bounds.
320    pub fn shortest_distance(&self) -> &LengthInterval {
321        &self.shortest_distance
322    }
323
324    /// Object-grounded route points in traversal order.
325    pub fn waypoints(&self) -> &[MetricPoint] {
326        &self.waypoints
327    }
328
329    /// Source-qualified objects traversed by the route.
330    pub fn traversed_objects(&self) -> &[ObjectId] {
331        &self.traversed_objects
332    }
333
334    /// Route computation provenance.
335    pub fn evidence(&self) -> &Evidence {
336        &self.evidence
337    }
338}
339
340/// Evaluated route result. Backend incompleteness is an error, not a third verdict.
341#[derive(Clone, Debug, PartialEq)]
342pub enum MetricRouteOutcome {
343    /// At least one route exists; the distance may remain conservatively bounded.
344    Reachable(MetricRouteEvidence),
345    /// No route exists under exact, complete topology and obstacle evidence.
346    Blocked(BlockedMetricRouteEvidence),
347}
348
349/// Backend-neutral metric routing interface implemented by trusted host code.
350pub trait MetricRoutingService: Send + Sync + 'static {
351    /// Evaluates one route request or explicitly refuses unavailable evidence.
352    fn route(&self, request: &MetricRouteRequest)
353    -> Result<MetricRouteOutcome, MetricRoutingError>;
354}
355
356/// Concrete type-indexable wrapper around a metric routing service.
357#[derive(Clone)]
358pub struct MetricRoutingServiceHandle(Arc<dyn MetricRoutingService>);
359
360impl MetricRoutingServiceHandle {
361    /// Wraps an Axiolid or alternate backend implementation for service registration.
362    pub fn new(service: Arc<dyn MetricRoutingService>) -> Self {
363        Self(service)
364    }
365
366    /// Executes and validates endpoint identity in the backend response.
367    pub fn route(
368        &self,
369        request: &MetricRouteRequest,
370    ) -> Result<MetricRouteOutcome, MetricRoutingError> {
371        let outcome = self.0.route(request)?;
372        if let MetricRouteOutcome::Reachable(route) = &outcome {
373            let (Some(first), Some(last)) = (route.waypoints.first(), route.waypoints.last())
374            else {
375                return Err(MetricRoutingError::EmptyRouteEvidence);
376            };
377            if first != request.origin() || last != request.destination() {
378                return Err(MetricRoutingError::ResponseEndpointMismatch);
379            }
380        } else if let MetricRouteOutcome::Blocked(blocked) = &outcome
381            && blocked.request() != request
382        {
383            return Err(MetricRoutingError::ResponseEndpointMismatch);
384        }
385        Ok(outcome)
386    }
387}
388
389fn valid_non_negative(value: f64) -> bool {
390    value.is_finite() && value >= 0.0
391}
392
393fn reviewable_exact_evidence(evidence: &Evidence) -> bool {
394    evidence.exact && !evidence.locator.trim().is_empty()
395}