Skip to main content

forjar/core/store/
validate.rs

1//! FJ-1306 / FJ-1329: Validation commands for purity and reproducibility.
2//!
3//! Implements `forjar validate --check-recipe-purity` and
4//! `forjar validate --check-reproducibility-score` logic.
5
6use 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/// A single resource's purity validation result.
13#[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/// Overall recipe purity validation result.
22#[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
31/// Validate recipe purity, optionally requiring a minimum level.
32pub 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
62/// Validate reproducibility score, optionally requiring a minimum score.
63pub 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/// Reproducibility score validation result.
76#[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
84/// Format purity validation for display.
85pub 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
106/// Format reproducibility validation for display.
107pub 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}