Skip to main content

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