Skip to main content

nichlink/registry_core/diagnostic/
topology.rs

1use super::*;
2
3// ---------------------------------------------------------------------------
4// Face topology validation. Pure graph checks over build-collected records;
5// the build surface collects the records, the kernel owns the rules.
6// 注册面拓扑校验。对构建期收集的记录做纯图检查;
7// 记录由 build 面收集,规则归 kernel 所有。
8
9/// One registration face participating in a topology check.
10/// 参与拓扑校验的一个注册面。
11#[derive(Clone, Debug)]
12pub struct TopologyRecord {
13    /// Identity of the face under check.
14    /// 受检注册面的身份。
15    pub id: NodeId,
16    /// Identity of the face this one registers under; the package root for a
17    /// top-level face.
18    /// 本注册面所挂载的父面身份;顶层注册面则为包根。
19    pub parent: NodeId,
20    /// Whether this face provides a registry its children may register into.
21    /// 该注册面是否提供可供子级注册的注册表。
22    pub owns_registry: bool,
23    /// Source file label the diagnostic points back to.
24    /// 诊断指回的源文件标签。
25    pub source: String,
26}
27
28/// Sort `records` by identity, then check missing parents, parents that do
29/// not own a registry, and parent cycles. Returns the collected diagnostics.
30/// 将 `records` 按身份排序,然后检查缺失的父节点、父节点不持有注册表、
31/// 以及父链成环;返回收集到的诊断。
32pub 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(&current).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}