Skip to main content

axioval_engine/
topology.rs

1//! Deterministic source-neutral connectivity and route contracts.
2//!
3//! Geometry adapters may nominate edges only after proving the connection at
4//! the declared evidence exactness. This module never infers connectivity from
5//! source-format relationships or geometry backend handles.
6
7use std::collections::{BTreeMap, BTreeSet, VecDeque};
8
9use axioval_ir::{Evidence, ObjectId};
10use thiserror::Error;
11
12/// A fail-closed topology construction or query error.
13#[derive(Clone, Debug, Error, PartialEq, Eq)]
14pub enum TopologyError {
15    /// The declared node universe contains the same source-qualified identity twice.
16    #[error("duplicate topology node `{0}`")]
17    DuplicateNode(Box<ObjectId>),
18    /// A connection references an object outside the declared node universe.
19    #[error("connection endpoint `{0}` is outside the declared topology universe")]
20    UnknownEndpoint(Box<ObjectId>),
21    /// A query references an object outside the declared node universe.
22    #[error("topology query references unknown node `{0}`")]
23    UnknownNode(Box<ObjectId>),
24    /// A connection was asserted without exact adapter evidence.
25    #[error("connectivity evidence is not exact")]
26    InexactConnection,
27    /// A connection joins an object to itself.
28    #[error("self connections are invalid for `{0}`")]
29    SelfConnection(Box<ObjectId>),
30    /// A clear width or query threshold was non-finite or negative.
31    #[error("invalid clear width `{0}`")]
32    InvalidWidth(String),
33    /// The same undirected connection was supplied more than once.
34    #[error("duplicate connection between `{left}` and `{right}`")]
35    DuplicateConnection {
36        left: Box<ObjectId>,
37        right: Box<ObjectId>,
38    },
39    /// Exact evidence did not carry a usable provenance locator.
40    #[error("connectivity evidence locator must not be blank")]
41    BlankEvidenceLocator,
42    /// The adapter could not prove the declared topology universe complete.
43    #[error("topology coverage evidence is not exact")]
44    InexactTopologyCoverage,
45}
46
47/// Exact adapter evidence that all nodes and candidate transitions in scope were assessed.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct CompleteTopologyEvidence(Evidence);
50
51impl CompleteTopologyEvidence {
52    /// Promotes adapter evidence only when it explicitly proves exact coverage.
53    pub fn try_new(evidence: Evidence) -> Result<Self, TopologyError> {
54        if !evidence.exact {
55            return Err(TopologyError::InexactTopologyCoverage);
56        }
57        validate_evidence_locator(&evidence)?;
58        Ok(Self(evidence))
59    }
60
61    /// Provenance for the complete topology projection.
62    pub fn evidence(&self) -> &Evidence {
63        &self.0
64    }
65}
66
67/// A connection asserted exact by a trusted host adapter.
68#[derive(Clone, Debug, PartialEq)]
69pub struct VerifiedConnection {
70    left: ObjectId,
71    right: ObjectId,
72    clear_width_metres: f64,
73    evidence: Evidence,
74}
75
76impl VerifiedConnection {
77    /// Creates a source-neutral exact connection.
78    pub fn try_new(
79        left: ObjectId,
80        right: ObjectId,
81        clear_width_metres: f64,
82        evidence: Evidence,
83    ) -> Result<Self, TopologyError> {
84        if left == right {
85            return Err(TopologyError::SelfConnection(Box::new(left)));
86        }
87        validate_width(clear_width_metres)?;
88        if !evidence.exact {
89            return Err(TopologyError::InexactConnection);
90        }
91        validate_evidence_locator(&evidence)?;
92        let (left, right) = ordered_pair(left, right);
93        Ok(Self {
94            left,
95            right,
96            clear_width_metres,
97            evidence,
98        })
99    }
100
101    /// First endpoint in source-qualified identity order.
102    pub fn left(&self) -> &ObjectId {
103        &self.left
104    }
105
106    /// Second endpoint in source-qualified identity order.
107    pub fn right(&self) -> &ObjectId {
108        &self.right
109    }
110
111    /// Exact clear width in metres.
112    pub fn clear_width_metres(&self) -> f64 {
113        self.clear_width_metres
114    }
115
116    /// Adapter evidence proving this connection.
117    pub fn evidence(&self) -> &Evidence {
118        &self.evidence
119    }
120}
121
122/// Result of a deterministic shortest-hop route query.
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub enum RouteOutcome {
125    /// Ordered nodes from origin through destination, including both endpoints.
126    Route(Vec<ObjectId>),
127    /// Both nodes are known and proven disconnected at the requested clear width.
128    Unreachable,
129}
130
131/// Complete topology over a declared source-qualified object universe.
132#[derive(Clone, Debug, PartialEq)]
133pub struct ConnectivityGraph {
134    nodes: BTreeSet<ObjectId>,
135    adjacency: BTreeMap<ObjectId, BTreeMap<ObjectId, VerifiedConnection>>,
136    coverage: CompleteTopologyEvidence,
137}
138
139impl ConnectivityGraph {
140    /// Constructs a graph, rejecting ambiguous nodes and partial connection endpoints.
141    pub fn try_new(
142        nodes: impl IntoIterator<Item = ObjectId>,
143        connections: impl IntoIterator<Item = VerifiedConnection>,
144        coverage: CompleteTopologyEvidence,
145    ) -> Result<Self, TopologyError> {
146        let mut node_set = BTreeSet::new();
147        for node in nodes {
148            if !node_set.insert(node.clone()) {
149                return Err(TopologyError::DuplicateNode(Box::new(node)));
150            }
151        }
152        let mut adjacency = node_set
153            .iter()
154            .cloned()
155            .map(|node| (node, BTreeMap::new()))
156            .collect::<BTreeMap<_, _>>();
157        for connection in connections {
158            add_connection(&node_set, &mut adjacency, connection)?;
159        }
160        Ok(Self {
161            nodes: node_set,
162            adjacency,
163            coverage,
164        })
165    }
166
167    /// Evidence proving that the graph is a complete projection for its declared scope.
168    pub fn coverage(&self) -> &CompleteTopologyEvidence {
169        &self.coverage
170    }
171
172    /// Returns every node reachable through edges meeting the clear-width threshold.
173    pub fn reachable_from(
174        &self,
175        origin: &ObjectId,
176        minimum_clear_width_metres: f64,
177    ) -> Result<Vec<ObjectId>, TopologyError> {
178        self.require_node(origin)?;
179        validate_width(minimum_clear_width_metres)?;
180        let mut seen = BTreeSet::from([origin.clone()]);
181        let mut queue = VecDeque::from([origin.clone()]);
182        while let Some(current) = queue.pop_front() {
183            for (neighbor, edge) in &self.adjacency[&current] {
184                if edge.clear_width_metres >= minimum_clear_width_metres
185                    && seen.insert(neighbor.clone())
186                {
187                    queue.push_back(neighbor.clone());
188                }
189            }
190        }
191        Ok(seen.into_iter().collect())
192    }
193
194    /// Finds the deterministic shortest-hop route meeting the clear-width threshold.
195    pub fn route(
196        &self,
197        origin: &ObjectId,
198        destination: &ObjectId,
199        minimum_clear_width_metres: f64,
200    ) -> Result<RouteOutcome, TopologyError> {
201        self.require_node(origin)?;
202        self.require_node(destination)?;
203        validate_width(minimum_clear_width_metres)?;
204        if origin == destination {
205            return Ok(RouteOutcome::Route(vec![origin.clone()]));
206        }
207        let parents = self.search(origin, destination, minimum_clear_width_metres);
208        if !parents.contains_key(destination) {
209            return Ok(RouteOutcome::Unreachable);
210        }
211        Ok(RouteOutcome::Route(reconstruct_route(
212            origin,
213            destination,
214            &parents,
215        )))
216    }
217
218    fn require_node(&self, node: &ObjectId) -> Result<(), TopologyError> {
219        if self.nodes.contains(node) {
220            Ok(())
221        } else {
222            Err(TopologyError::UnknownNode(Box::new(node.clone())))
223        }
224    }
225
226    fn search(
227        &self,
228        origin: &ObjectId,
229        destination: &ObjectId,
230        minimum_clear_width_metres: f64,
231    ) -> BTreeMap<ObjectId, ObjectId> {
232        let mut parents = BTreeMap::new();
233        let mut seen = BTreeSet::from([origin.clone()]);
234        let mut queue = VecDeque::from([origin.clone()]);
235        while let Some(current) = queue.pop_front() {
236            for (neighbor, edge) in &self.adjacency[&current] {
237                if edge.clear_width_metres < minimum_clear_width_metres
238                    || !seen.insert(neighbor.clone())
239                {
240                    continue;
241                }
242                parents.insert(neighbor.clone(), current.clone());
243                if neighbor == destination {
244                    return parents;
245                }
246                queue.push_back(neighbor.clone());
247            }
248        }
249        parents
250    }
251}
252
253fn add_connection(
254    nodes: &BTreeSet<ObjectId>,
255    adjacency: &mut BTreeMap<ObjectId, BTreeMap<ObjectId, VerifiedConnection>>,
256    connection: VerifiedConnection,
257) -> Result<(), TopologyError> {
258    for endpoint in [&connection.left, &connection.right] {
259        if !nodes.contains(endpoint) {
260            return Err(TopologyError::UnknownEndpoint(Box::new(endpoint.clone())));
261        }
262    }
263    if adjacency[&connection.left].contains_key(&connection.right) {
264        return Err(TopologyError::DuplicateConnection {
265            left: Box::new(connection.left),
266            right: Box::new(connection.right),
267        });
268    }
269    adjacency
270        .get_mut(&connection.left)
271        .expect("validated node")
272        .insert(connection.right.clone(), connection.clone());
273    adjacency
274        .get_mut(&connection.right)
275        .expect("validated node")
276        .insert(connection.left.clone(), connection);
277    Ok(())
278}
279
280fn reconstruct_route(
281    origin: &ObjectId,
282    destination: &ObjectId,
283    parents: &BTreeMap<ObjectId, ObjectId>,
284) -> Vec<ObjectId> {
285    let mut route = vec![destination.clone()];
286    let mut current = destination;
287    while current != origin {
288        current = &parents[current];
289        route.push(current.clone());
290    }
291    route.reverse();
292    route
293}
294
295fn ordered_pair(left: ObjectId, right: ObjectId) -> (ObjectId, ObjectId) {
296    if left < right {
297        (left, right)
298    } else {
299        (right, left)
300    }
301}
302
303fn validate_width(width: f64) -> Result<(), TopologyError> {
304    if width.is_finite() && width >= 0.0 {
305        Ok(())
306    } else {
307        Err(TopologyError::InvalidWidth(width.to_string()))
308    }
309}
310
311fn validate_evidence_locator(evidence: &Evidence) -> Result<(), TopologyError> {
312    if evidence.locator.trim().is_empty() {
313        Err(TopologyError::BlankEvidenceLocator)
314    } else {
315        Ok(())
316    }
317}