1use std::collections::HashSet;
2
3use crate::error::{Severity, Violation};
4use crate::schema::types::{Contract, ContractKind};
5
6pub fn validate_contract(contract: &Contract) -> Vec<Violation> {
16 let mut violations = Vec::new();
17
18 validate_metadata(contract, &mut violations);
19
20 if contract.kind() == ContractKind::Kernel && !contract.is_registry() {
23 validate_equations(contract, &mut violations);
24 validate_provability_invariant(contract, &mut violations);
25 validate_proof_obligations(contract, &mut violations);
26 validate_falsification_tests(contract, &mut violations);
27 validate_kani_harnesses(contract, &mut violations);
28 validate_qa_gate(contract, &mut violations);
29 } else {
30 validate_proof_obligations(contract, &mut violations);
34 validate_falsification_tests(contract, &mut violations);
35 validate_kani_harnesses(contract, &mut violations);
36 }
37
38 if contract.kind() == ContractKind::BeatBenchmark {
42 validate_beat_benchmark(contract, &mut violations);
43 }
44
45 violations
46}
47
48const BEAT_INCUMBENTS: [&str; 5] = ["scikit-learn", "pytorch", "unsloth", "ollama", "llama.cpp"];
51
52fn validate_beat_benchmark(contract: &Contract, violations: &mut Vec<Violation>) {
56 let push = |violations: &mut Vec<Violation>, rule: &str, message: String, field: &str| {
57 violations.push(Violation {
58 severity: Severity::Error,
59 rule: rule.to_string(),
60 message,
61 location: Some(format!("beat.{field}")),
62 });
63 };
64
65 let Some(beat) = contract.beat.as_ref() else {
66 violations.push(Violation {
67 severity: Severity::Error,
68 rule: "BEAT-001".to_string(),
69 message: "beat-benchmark contract must define a `beat:` block \
70 (incumbent, metric, direction, beat_threshold, ci_gate_name)"
71 .to_string(),
72 location: Some("beat".to_string()),
73 });
74 return;
75 };
76
77 let incumbent = beat.incumbent.trim().to_lowercase();
79 if incumbent.is_empty() {
80 push(
81 violations,
82 "BEAT-002",
83 "beat.incumbent must not be empty".to_string(),
84 "incumbent",
85 );
86 } else if !BEAT_INCUMBENTS.iter().any(|p| incumbent.contains(p)) {
87 push(
88 violations,
89 "BEAT-002",
90 format!(
91 "beat.incumbent {:?} must name one of the four pillars ({})",
92 beat.incumbent,
93 BEAT_INCUMBENTS.join(", ")
94 ),
95 "incumbent",
96 );
97 }
98
99 if beat.metric.trim().is_empty() {
101 push(
102 violations,
103 "BEAT-003",
104 "beat.metric must name the measured quantity (e.g. accuracy, wall_clock_ms, \
105 tokens_per_sec)"
106 .to_string(),
107 "metric",
108 );
109 }
110
111 match beat.direction.trim() {
113 "higher_is_better" | "lower_is_better" => {}
114 other => push(
115 violations,
116 "BEAT-004",
117 format!(
118 "beat.direction must be `higher_is_better` or `lower_is_better`, got {other:?}"
119 ),
120 "direction",
121 ),
122 }
123
124 match beat.beat_threshold {
126 None => push(
127 violations,
128 "BEAT-005",
129 "beat.beat_threshold is required — the pinned value CI fails below".to_string(),
130 "beat_threshold",
131 ),
132 Some(t) if !t.is_finite() => push(
133 violations,
134 "BEAT-005",
135 format!("beat.beat_threshold must be finite, got {t}"),
136 "beat_threshold",
137 ),
138 Some(_) => {}
139 }
140
141 if beat.ci_gate_name.trim().is_empty() {
143 push(
144 violations,
145 "BEAT-006",
146 "beat.ci_gate_name must name the CI test that enforces this gate".to_string(),
147 "ci_gate_name",
148 );
149 }
150
151 match beat
154 .approved_compute
155 .as_deref()
156 .map(|c| c.trim().to_uppercase())
157 {
158 None => push(
159 violations,
160 "BEAT-007",
161 "beat.approved_compute is required — must be `CPU` or `GPU`".to_string(),
162 "approved_compute",
163 ),
164 Some(ref c) if c != "CPU" && c != "GPU" => push(
165 violations,
166 "BEAT-007",
167 format!(
168 "beat.approved_compute must be `CPU` or `GPU`, got {:?}",
169 beat.approved_compute
170 ),
171 "approved_compute",
172 ),
173 Some(_) => {}
174 }
175}
176
177fn validate_provability_invariant(contract: &Contract, violations: &mut Vec<Violation>) {
180 for v in contract.provability_violations() {
181 violations.push(Violation {
182 severity: Severity::Error,
183 rule: "PROVABILITY-001".to_string(),
184 message: v,
185 location: None,
186 });
187 }
188}
189
190fn validate_metadata(contract: &Contract, violations: &mut Vec<Violation>) {
191 if contract.metadata.references.is_empty() {
192 violations.push(Violation {
193 severity: Severity::Error,
194 rule: "SCHEMA-001".to_string(),
195 message: "metadata.references must not be empty — \
196 every contract must cite its source paper(s)"
197 .to_string(),
198 location: Some("metadata.references".to_string()),
199 });
200 }
201
202 if contract.metadata.version.is_empty() {
203 violations.push(Violation {
204 severity: Severity::Error,
205 rule: "SCHEMA-002".to_string(),
206 message: "metadata.version must not be empty".to_string(),
207 location: Some("metadata.version".to_string()),
208 });
209 }
210}
211
212fn validate_equations(contract: &Contract, violations: &mut Vec<Violation>) {
213 if contract.equations.is_empty() {
214 violations.push(Violation {
215 severity: Severity::Error,
216 rule: "SCHEMA-003".to_string(),
217 message: "equations must contain at least one equation".to_string(),
218 location: Some("equations".to_string()),
219 });
220 }
221
222 for (name, eq) in &contract.equations {
223 if eq.formula.is_empty() {
224 violations.push(Violation {
225 severity: Severity::Error,
226 rule: "SCHEMA-004".to_string(),
227 message: format!("equations.{name}.formula must not be empty"),
228 location: Some(format!("equations.{name}.formula")),
229 });
230 }
231 }
232}
233
234fn validate_proof_obligations(contract: &Contract, violations: &mut Vec<Violation>) {
235 use crate::schema::types::ObligationType;
236
237 let mut seen_ids = HashSet::new();
238 for (i, ob) in contract.proof_obligations.iter().enumerate() {
239 if ob.property.is_empty() {
240 violations.push(Violation {
241 severity: Severity::Error,
242 rule: "SCHEMA-005".to_string(),
243 message: format!("proof_obligations[{i}].property must not be empty"),
244 location: Some(format!("proof_obligations[{i}].property")),
245 });
246 }
247 if let Some(ref formal) = ob.formal {
248 if !seen_ids.insert(formal.clone()) {
249 violations.push(Violation {
250 severity: Severity::Warning,
251 rule: "SCHEMA-006".to_string(),
252 message: format!("Duplicate formal predicate: {formal}"),
253 location: Some(format!("proof_obligations[{i}].formal")),
254 });
255 }
256 }
257
258 if ob.requires.is_some() && ob.obligation_type != ObligationType::Postcondition {
260 violations.push(Violation {
261 severity: Severity::Error,
262 rule: "SCHEMA-014".to_string(),
263 message: format!(
264 "proof_obligations[{i}].requires is only valid on \
265 postcondition obligations (found on {})",
266 ob.obligation_type
267 ),
268 location: Some(format!("proof_obligations[{i}].requires")),
269 });
270 }
271
272 if ob.applies_to_phase.is_some()
273 && ob.obligation_type != ObligationType::LoopInvariant
274 && ob.obligation_type != ObligationType::LoopVariant
275 {
276 violations.push(Violation {
277 severity: Severity::Error,
278 rule: "SCHEMA-015".to_string(),
279 message: format!(
280 "proof_obligations[{i}].applies_to_phase is only valid on \
281 loop_invariant or loop_variant obligations (found on {})",
282 ob.obligation_type
283 ),
284 location: Some(format!("proof_obligations[{i}].applies_to_phase")),
285 });
286 }
287
288 if ob.parent_contract.is_some() && ob.obligation_type != ObligationType::Subcontract {
289 violations.push(Violation {
290 severity: Severity::Error,
291 rule: "SCHEMA-016".to_string(),
292 message: format!(
293 "proof_obligations[{i}].parent_contract is only valid on \
294 subcontract obligations (found on {})",
295 ob.obligation_type
296 ),
297 location: Some(format!("proof_obligations[{i}].parent_contract")),
298 });
299 }
300
301 if let Some(ref parent) = ob.parent_contract {
303 if ob.obligation_type == ObligationType::Subcontract
304 && !contract.metadata.depends_on.contains(parent)
305 {
306 violations.push(Violation {
307 severity: Severity::Error,
308 rule: "SCHEMA-017".to_string(),
309 message: format!(
310 "proof_obligations[{i}].parent_contract \"{parent}\" \
311 must be listed in metadata.depends_on"
312 ),
313 location: Some(format!("proof_obligations[{i}].parent_contract")),
314 });
315 }
316 }
317 }
318}
319
320fn validate_falsification_tests(contract: &Contract, violations: &mut Vec<Violation>) {
321 let mut ids = HashSet::new();
322 for test in &contract.falsification_tests {
323 if !ids.insert(&test.id) {
324 violations.push(Violation {
325 severity: Severity::Error,
326 rule: "SCHEMA-007".to_string(),
327 message: format!("Duplicate falsification test ID: {}", test.id),
328 location: Some(format!("falsification_tests.{}", test.id)),
329 });
330 }
331 if test.prediction.is_empty() {
332 violations.push(Violation {
333 severity: Severity::Error,
334 rule: "SCHEMA-008".to_string(),
335 message: format!(
336 "falsification_tests.{}.prediction must not be empty — \
337 every test must make a falsifiable prediction",
338 test.id
339 ),
340 location: Some(format!("falsification_tests.{}.prediction", test.id)),
341 });
342 }
343 if test.if_fails.is_empty() {
344 violations.push(Violation {
345 severity: Severity::Warning,
346 rule: "SCHEMA-009".to_string(),
347 message: format!(
348 "falsification_tests.{}.if_fails is empty — \
349 should describe root cause diagnosis",
350 test.id
351 ),
352 location: Some(format!("falsification_tests.{}.if_fails", test.id)),
353 });
354 }
355 }
356}
357
358fn validate_kani_harnesses(contract: &Contract, violations: &mut Vec<Violation>) {
359 let mut ids = HashSet::new();
360 for harness in &contract.kani_harnesses {
361 if !ids.insert(&harness.id) {
362 violations.push(Violation {
363 severity: Severity::Error,
364 rule: "SCHEMA-010".to_string(),
365 message: format!("Duplicate Kani harness ID: {}", harness.id),
366 location: Some(format!("kani_harnesses.{}", harness.id)),
367 });
368 }
369 if harness.obligation.is_empty() {
370 violations.push(Violation {
371 severity: Severity::Error,
372 rule: "SCHEMA-011".to_string(),
373 message: format!(
374 "kani_harnesses.{}.obligation must not be empty — \
375 every harness must reference a proof obligation",
376 harness.id
377 ),
378 location: Some(format!("kani_harnesses.{}.obligation", harness.id)),
379 });
380 }
381 if harness.bound.is_none() {
382 violations.push(Violation {
383 severity: Severity::Warning,
384 rule: "SCHEMA-012".to_string(),
385 message: format!(
386 "kani_harnesses.{}.bound not specified — \
387 Kani requires an unwind bound",
388 harness.id
389 ),
390 location: Some(format!("kani_harnesses.{}.bound", harness.id)),
391 });
392 }
393 }
394}
395
396fn validate_qa_gate(contract: &Contract, violations: &mut Vec<Violation>) {
397 if contract.qa_gate.is_none() {
398 violations.push(Violation {
399 severity: Severity::Warning,
400 rule: "SCHEMA-013".to_string(),
401 message: "No qa_gate defined — contract should define a \
402 certeza quality gate"
403 .to_string(),
404 location: Some("qa_gate".to_string()),
405 });
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 include!("validator_tests.rs");
412}