forjar/core/store/
validate.rs1use super::purity::{
7 classify, level_label, recipe_purity, PurityLevel, PurityResult, PuritySignals,
8};
9use super::repro_score::{compute_score, grade, ReproInput, ReproScore};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct ResourcePurityReport {
15 pub name: String,
16 pub level: PurityLevel,
17 pub label: String,
18 pub reasons: Vec<String>,
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct PurityValidation {
24 pub resources: Vec<ResourcePurityReport>,
25 pub recipe_purity: PurityLevel,
26 pub recipe_label: String,
27 pub pass: bool,
28 pub required_level: Option<PurityLevel>,
29}
30
31pub fn validate_purity(
33 signals: &[(&str, &PuritySignals)],
34 min_level: Option<PurityLevel>,
35) -> PurityValidation {
36 let mut resources = Vec::new();
37 let mut levels = Vec::new();
38
39 for (name, sig) in signals {
40 let result: PurityResult = classify(name, sig);
41 levels.push(result.level);
42 resources.push(ResourcePurityReport {
43 name: name.to_string(),
44 level: result.level,
45 label: level_label(result.level).to_string(),
46 reasons: result.reasons,
47 });
48 }
49
50 let overall = recipe_purity(&levels);
51 let pass = min_level.is_none_or(|min| purity_ord(overall) <= purity_ord(min));
52
53 PurityValidation {
54 resources,
55 recipe_purity: overall,
56 recipe_label: level_label(overall).to_string(),
57 pass,
58 required_level: min_level,
59 }
60}
61
62pub fn validate_repro_score(inputs: &[ReproInput], min_score: Option<f64>) -> ReproValidation {
64 let score = compute_score(inputs);
65 let pass = min_score.is_none_or(|min| score.composite >= min);
66
67 ReproValidation {
68 score: score.clone(),
69 grade: grade(score.composite).to_string(),
70 pass,
71 required_min: min_score,
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct ReproValidation {
78 pub score: ReproScore,
79 pub grade: String,
80 pub pass: bool,
81 pub required_min: Option<f64>,
82}
83
84pub fn format_purity_report(validation: &PurityValidation) -> String {
86 let mut lines = Vec::new();
87 lines.push(format!(
88 "Recipe purity: {} ({})",
89 validation.recipe_label,
90 if validation.pass { "PASS" } else { "FAIL" }
91 ));
92 for r in &validation.resources {
93 lines.push(format!(
94 " {}: {} — {}",
95 r.name,
96 r.label,
97 r.reasons.join("; ")
98 ));
99 }
100 if let Some(required) = validation.required_level {
101 lines.push(format!(" Required: {} or better", level_label(required)));
102 }
103 lines.join("\n")
104}
105
106pub fn format_repro_report(validation: &ReproValidation) -> String {
108 let mut lines = Vec::new();
109 lines.push(format!(
110 "Reproducibility: {:.1}/100 (Grade {}) — {}",
111 validation.score.composite,
112 validation.grade,
113 if validation.pass { "PASS" } else { "FAIL" }
114 ));
115 lines.push(format!(" Purity: {:.1}", validation.score.purity_score));
116 lines.push(format!(" Store: {:.1}", validation.score.store_score));
117 lines.push(format!(" Lock: {:.1}", validation.score.lock_score));
118 if let Some(min) = validation.required_min {
119 lines.push(format!(" Required: >= {:.1}", min));
120 }
121 lines.join("\n")
122}
123
124fn purity_ord(level: PurityLevel) -> u8 {
125 match level {
126 PurityLevel::Pure => 0,
127 PurityLevel::Pinned => 1,
128 PurityLevel::Constrained => 2,
129 PurityLevel::Impure => 3,
130 }
131}