Skip to main content

ucp_codegraph/legacy/
validate.rs

1use std::collections::HashMap;
2
3use ucm_core::{BlockId, Document, EdgeType};
4
5use crate::model::*;
6
7use super::canonical::validate_required_metadata;
8use super::{block_logical_key, block_path, logical_key_index, node_class};
9
10pub fn validate_code_graph_profile(doc: &Document) -> CodeGraphValidationResult {
11    let mut diagnostics = Vec::new();
12
13    match doc.metadata.custom.get("profile").and_then(|v| v.as_str()) {
14        Some(CODEGRAPH_PROFILE) => {}
15        Some(other) => diagnostics.push(CodeGraphDiagnostic::error(
16            "CG1001",
17            format!(
18                "invalid profile marker '{}', expected '{}'",
19                other, CODEGRAPH_PROFILE
20            ),
21        )),
22        None => diagnostics.push(CodeGraphDiagnostic::error(
23            "CG1001",
24            "missing document metadata.custom.profile marker",
25        )),
26    }
27
28    match doc
29        .metadata
30        .custom
31        .get("profile_version")
32        .and_then(|v| v.as_str())
33    {
34        Some(CODEGRAPH_PROFILE_VERSION) => {}
35        Some(other) => diagnostics.push(CodeGraphDiagnostic::error(
36            "CG1002",
37            format!(
38                "invalid profile version '{}', expected '{}'",
39                other, CODEGRAPH_PROFILE_VERSION
40            ),
41        )),
42        None => diagnostics.push(CodeGraphDiagnostic::error(
43            "CG1002",
44            "missing document metadata.custom.profile_version marker",
45        )),
46    }
47
48    let mut logical_keys: HashMap<String, Vec<BlockId>> = HashMap::new();
49    let mut class_counts: HashMap<String, usize> = HashMap::new();
50
51    for (id, block) in &doc.blocks {
52        if *id == doc.root {
53            continue;
54        }
55
56        let class = node_class(block);
57        let Some(class_name) = class else {
58            diagnostics.push(
59                CodeGraphDiagnostic::error(
60                    "CG1010",
61                    "block missing node_class metadata (or custom semantic role)",
62                )
63                .with_path(block_path(block).unwrap_or_else(|| id.to_string())),
64            );
65            continue;
66        };
67
68        *class_counts.entry(class_name.clone()).or_default() += 1;
69
70        match block_logical_key(block) {
71            Some(logical_key) => {
72                logical_keys.entry(logical_key).or_default().push(*id);
73            }
74            None => diagnostics.push(
75                CodeGraphDiagnostic::error("CG1011", "missing required logical_key metadata")
76                    .with_path(block_path(block).unwrap_or_else(|| id.to_string())),
77            ),
78        }
79
80        validate_required_metadata(&class_name, block, &mut diagnostics);
81    }
82
83    for class in ["repository", "directory", "file", "symbol"] {
84        if class_counts.get(class).copied().unwrap_or(0) == 0 {
85            diagnostics.push(CodeGraphDiagnostic::warning(
86                "CG1012",
87                format!("profile has no '{}' nodes", class),
88            ));
89        }
90    }
91
92    for (logical_key, ids) in logical_keys {
93        if ids.len() > 1 {
94            diagnostics.push(
95                CodeGraphDiagnostic::error(
96                    "CG1013",
97                    format!(
98                        "logical_key '{}' is duplicated by {} blocks",
99                        logical_key,
100                        ids.len()
101                    ),
102                )
103                .with_logical_key(logical_key),
104            );
105        }
106    }
107
108    let logical_by_id = logical_key_index(doc);
109
110    for (source_id, block) in &doc.blocks {
111        let Some(source_class) = node_class(block) else {
112            continue;
113        };
114        for edge in &block.edges {
115            let target_block = match doc.get_block(&edge.target) {
116                Some(b) => b,
117                None => {
118                    diagnostics.push(
119                        CodeGraphDiagnostic::error(
120                            "CG1014",
121                            format!("edge references missing target block {}", edge.target),
122                        )
123                        .with_logical_key(
124                            logical_by_id
125                                .get(source_id)
126                                .cloned()
127                                .unwrap_or_else(|| source_id.to_string()),
128                        ),
129                    );
130                    continue;
131                }
132            };
133
134            let target_class = node_class(target_block).unwrap_or_default();
135
136            match &edge.edge_type {
137                EdgeType::References => {
138                    if source_class != "file" || target_class != "file" {
139                        diagnostics.push(
140                            CodeGraphDiagnostic::error(
141                                "CG1015",
142                                "references edges must connect file -> file",
143                            )
144                            .with_logical_key(
145                                logical_by_id
146                                    .get(source_id)
147                                    .cloned()
148                                    .unwrap_or_else(|| source_id.to_string()),
149                            ),
150                        );
151                    }
152                }
153                EdgeType::Custom(name) if name == "exports" => {
154                    if source_class != "file" || target_class != "symbol" {
155                        diagnostics.push(
156                            CodeGraphDiagnostic::error(
157                                "CG1016",
158                                "exports edges must connect file -> symbol",
159                            )
160                            .with_logical_key(
161                                logical_by_id
162                                    .get(source_id)
163                                    .cloned()
164                                    .unwrap_or_else(|| source_id.to_string()),
165                            ),
166                        );
167                    }
168                }
169                _ => {}
170            }
171        }
172    }
173
174    CodeGraphValidationResult {
175        valid: diagnostics
176            .iter()
177            .all(|d| d.severity != CodeGraphSeverity::Error),
178        diagnostics,
179    }
180}