sara_core/validation/
validator.rs1use std::time::Instant;
4
5use crate::config::ValidationConfig;
6use crate::graph::KnowledgeGraph;
7use crate::validation::report::{ValidationReport, ValidationReportBuilder};
8use crate::validation::rules;
9
10pub struct Validator {
12 config: ValidationConfig,
14}
15
16impl Validator {
17 pub fn new(config: ValidationConfig) -> Self {
19 Self { config }
20 }
21
22 pub fn with_defaults() -> Self {
24 Self::new(ValidationConfig::default())
25 }
26
27 pub fn validate(&self, graph: &KnowledgeGraph) -> ValidationReport {
29 let start = Instant::now();
30 let mut builder = ValidationReportBuilder::new()
31 .items_checked(graph.item_count())
32 .relationships_checked(graph.relationship_count());
33
34 let broken_refs = rules::check_broken_references(graph);
38 builder = builder.errors(broken_refs);
39
40 let orphans = rules::check_orphans(graph, self.config.strict_orphans);
43 if self.config.strict_orphans {
44 builder = builder.errors(orphans);
45 } else {
46 builder = builder.warnings(orphans);
47 }
48
49 let duplicates = rules::check_duplicates(graph);
52 builder = builder.errors(duplicates);
53
54 let cycles = rules::check_cycles(graph);
56 builder = builder.errors(cycles);
57
58 let metadata_errors = rules::check_metadata(graph, &self.config.allowed_custom_fields);
60 builder = builder.errors(metadata_errors);
61
62 let relationship_errors = rules::check_relationships(graph);
64 builder = builder.errors(relationship_errors);
65
66 let redundant = rules::check_redundant_relationships(graph);
68 builder = builder.warnings(redundant);
69
70 builder.duration(start.elapsed()).build()
71 }
72}
73
74impl Default for Validator {
75 fn default() -> Self {
76 Self::with_defaults()
77 }
78}
79
80pub fn validate(graph: &KnowledgeGraph) -> ValidationReport {
82 Validator::with_defaults().validate(graph)
83}
84
85pub fn validate_strict(graph: &KnowledgeGraph) -> ValidationReport {
87 let config = ValidationConfig {
88 strict_orphans: true,
89 ..Default::default()
90 };
91 Validator::new(config).validate(graph)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::graph::GraphBuilder;
98 use crate::model::{
99 ItemBuilder, ItemId, ItemType, RelationshipType, SourceLocation, UpstreamRefs,
100 };
101 use std::path::PathBuf;
102
103 fn create_item(
104 id: &str,
105 item_type: ItemType,
106 upstream: Option<UpstreamRefs>,
107 ) -> crate::model::Item {
108 let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id));
109 let mut builder = ItemBuilder::new()
110 .id(ItemId::new_unchecked(id))
111 .item_type(item_type)
112 .name(format!("Test {}", id))
113 .source(source);
114
115 if let Some(up) = upstream {
116 builder = builder.upstream(up);
117 }
118
119 if item_type.requires_specification() {
120 builder = builder.specification("Test spec");
121 }
122
123 builder.build().unwrap()
124 }
125
126 #[test]
127 fn test_valid_graph() {
128 let graph = GraphBuilder::new()
129 .add_item(create_item("SOL-001", ItemType::Solution, None))
130 .add_item(create_item(
131 "UC-001",
132 ItemType::UseCase,
133 Some(UpstreamRefs {
134 refines: vec![ItemId::new_unchecked("SOL-001")],
135 ..Default::default()
136 }),
137 ))
138 .build()
139 .unwrap();
140
141 let report = validate(&graph);
142 assert!(report.is_valid(), "Valid graph should pass validation");
143 assert_eq!(report.error_count(), 0);
144 }
145
146 #[test]
147 fn test_broken_reference() {
148 let graph = GraphBuilder::new()
149 .add_item(create_item(
150 "UC-001",
151 ItemType::UseCase,
152 Some(UpstreamRefs {
153 refines: vec![ItemId::new_unchecked("SOL-MISSING")],
154 ..Default::default()
155 }),
156 ))
157 .build()
158 .unwrap();
159
160 let report = validate(&graph);
161 assert!(!report.is_valid());
162 assert!(report.error_count() > 0);
163 }
164
165 #[test]
166 fn test_orphan_warning() {
167 let graph = GraphBuilder::new()
168 .add_item(create_item("UC-001", ItemType::UseCase, None))
169 .build()
170 .unwrap();
171
172 let report = validate(&graph);
174 assert!(
175 report.is_valid(),
176 "Orphan should be warning in non-strict mode"
177 );
178 assert_eq!(report.warning_count(), 1);
179 }
180
181 #[test]
182 fn test_orphan_error_strict() {
183 let graph = GraphBuilder::new()
184 .add_item(create_item("UC-001", ItemType::UseCase, None))
185 .build()
186 .unwrap();
187
188 let report = validate_strict(&graph);
190 assert!(!report.is_valid(), "Orphan should be error in strict mode");
191 assert_eq!(report.error_count(), 1);
192 }
193
194 #[test]
195 fn test_cycle_detection() {
196 let mut graph = KnowledgeGraph::new(false);
197
198 let scen1 = create_item(
200 "SCEN-001",
201 ItemType::Scenario,
202 Some(UpstreamRefs {
203 refines: vec![ItemId::new_unchecked("SCEN-002")],
204 ..Default::default()
205 }),
206 );
207 let scen2 = create_item(
208 "SCEN-002",
209 ItemType::Scenario,
210 Some(UpstreamRefs {
211 refines: vec![ItemId::new_unchecked("SCEN-001")],
212 ..Default::default()
213 }),
214 );
215
216 graph.add_item(scen1);
217 graph.add_item(scen2);
218
219 graph.add_relationship(
220 &ItemId::new_unchecked("SCEN-001"),
221 &ItemId::new_unchecked("SCEN-002"),
222 RelationshipType::Refines,
223 );
224 graph.add_relationship(
225 &ItemId::new_unchecked("SCEN-002"),
226 &ItemId::new_unchecked("SCEN-001"),
227 RelationshipType::Refines,
228 );
229
230 let report = validate(&graph);
231 assert!(!report.is_valid(), "Cycle should be detected");
232 }
233
234 #[test]
235 fn test_invalid_relationship() {
236 let mut graph = KnowledgeGraph::new(false);
237
238 graph.add_item(create_item("SOL-001", ItemType::Solution, None));
240 graph.add_item(create_item(
241 "SCEN-001",
242 ItemType::Scenario,
243 Some(UpstreamRefs {
244 refines: vec![ItemId::new_unchecked("SOL-001")],
245 ..Default::default()
246 }),
247 ));
248
249 let report = validate(&graph);
250 assert!(
251 !report.is_valid(),
252 "Invalid relationship should be detected"
253 );
254 }
255}