ucp_codegraph/legacy/
validate.rs1use 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}