nichlink/registry_core/diagnostic/
topology.rs1use super::*;
2
3#[derive(Clone, Debug)]
12pub struct TopologyRecord {
13 pub id: NodeId,
16 pub parent: NodeId,
20 pub owns_registry: bool,
23 pub source: String,
26}
27
28pub fn validate_face_topology(
33 records: &mut [TopologyRecord],
34 package_root: NodeId,
35) -> BuildDiagnostics {
36 let mut errors = BuildDiagnostics::default();
37 records.sort_by_key(|record| record.id);
38
39 let ids = records
40 .iter()
41 .map(|record| record.id)
42 .collect::<std::collections::BTreeSet<_>>();
43 let owners = records
44 .iter()
45 .map(|record| (record.id, record.owns_registry))
46 .collect::<std::collections::BTreeMap<_, _>>();
47 for record in records.iter() {
48 if record.parent != package_root && !ids.contains(&record.parent) {
49 errors.push(
50 BuildDiagnostic::new("static-plan", "parent node is missing")
51 .at(record.source.clone(), 0)
52 .field("parent")
53 .expected("registered parent")
54 .actual(record.parent.to_string()),
55 );
56 } else if record.parent != package_root && owners.get(&record.parent) == Some(&false) {
57 errors.push(
58 BuildDiagnostic::new("static-plan", "parent does not own a registry")
59 .at(record.source.clone(), 0)
60 .field("parent")
61 .expected("registry owner")
62 .actual(record.parent.to_string()),
63 );
64 }
65 }
66
67 let parents = records
68 .iter()
69 .map(|record| (record.id, record.parent))
70 .collect::<std::collections::BTreeMap<_, _>>();
71 for record in records.iter() {
72 let mut current = record.id;
73 let mut seen = std::collections::BTreeSet::new();
74 while current != package_root {
75 if !seen.insert(current) {
76 errors.push(
77 BuildDiagnostic::new("static-plan", "parent cycle detected")
78 .at(record.source.clone(), 0)
79 .field("parent")
80 .actual(current.to_string()),
81 );
82 break;
83 }
84 let Some(parent) = parents.get(¤t).copied() else {
85 break;
86 };
87 current = parent;
88 }
89 }
90 errors
91}
92
93#[cfg(test)]
94mod topology_tests {
95 use super::{TopologyRecord, validate_face_topology};
96 use crate::registry_core::identity::{NodeId, ROOT_NODE_ID};
97
98 fn record(id: u8, parent: u8, owns_registry: bool) -> TopologyRecord {
99 TopologyRecord {
100 id: NodeId::from_raw([id, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
101 parent: NodeId::from_raw([parent, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
102 owns_registry,
103 source: format!("face-{id}.rs"),
104 }
105 }
106
107 #[test]
108 fn missing_parent_is_reported() {
109 let mut records = vec![record(2, 9, true)];
110 let errors = validate_face_topology(&mut records, ROOT_NODE_ID);
111 assert_eq!(errors.render().matches("parent node is missing").count(), 1);
112 }
113
114 #[test]
115 fn parent_without_registry_is_reported() {
116 let mut records = vec![record(1, 0, false), record(2, 1, true)];
117 let errors = validate_face_topology(&mut records, ROOT_NODE_ID);
118 assert_eq!(
119 errors
120 .render()
121 .matches("parent does not own a registry")
122 .count(),
123 1
124 );
125 }
126
127 #[test]
128 fn parent_cycle_is_reported() {
129 let mut records = vec![record(1, 2, true), record(2, 1, true)];
130 let errors = validate_face_topology(&mut records, ROOT_NODE_ID);
131 assert_eq!(errors.render().matches("parent cycle detected").count(), 2);
132 }
133
134 #[test]
135 fn valid_chain_is_clean() {
136 let root = ROOT_NODE_ID;
137 let mut records = vec![
138 TopologyRecord {
139 parent: root,
140 ..record(1, 0, true)
141 },
142 TopologyRecord {
143 parent: NodeId::from_raw([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
144 ..record(2, 0, true)
145 },
146 ];
147 let errors = validate_face_topology(&mut records, root);
148 assert!(errors.is_empty());
149 }
150}