Skip to main content

loopsmith_core/config/
validation.rs

1//! Section D — how each goal is checked.
2
3use super::yes;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum Mode {
9    Subjective,
10    Objective,
11    Percentage,
12}
13
14/// How a validation is actually decided. Ordered by the independence ladder
15/// from the cheat sheet: `Judge` is rung 3, everything else is rung 4.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum Detector {
19    /// Run a command; exit code 0 passes. The strongest detector available.
20    Script {
21        command: String,
22        #[serde(default)]
23        args: Vec<String>,
24        #[serde(default)]
25        expect_exit: Option<i32>,
26    },
27    /// A path must exist (optionally non-empty).
28    FileExists {
29        path: String,
30        #[serde(default)]
31        non_empty: bool,
32    },
33    /// A regex must match the named artifact.
34    RegexMatch { artifact: String, pattern: String },
35    /// A numeric metric compared against a threshold.
36    Threshold {
37        metric: String,
38        op: CompareOp,
39        value: f64,
40    },
41    /// A model verdict. Requires a judge whose provider differs from the
42    /// builder's, otherwise the gate refuses it as non-independent.
43    Judge {
44        /// Name the external standard the judge checks against. Naming a
45        /// standard is what turns an opinion into a check.
46        standard: String,
47        #[serde(default)]
48        min_score: Option<f64>,
49    },
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum CompareOp {
55    Gt,
56    Gte,
57    Lt,
58    Lte,
59    Eq,
60}
61
62impl CompareOp {
63    pub fn apply(self, lhs: f64, rhs: f64) -> bool {
64        match self {
65            CompareOp::Gt => lhs > rhs,
66            CompareOp::Gte => lhs >= rhs,
67            CompareOp::Lt => lhs < rhs,
68            CompareOp::Lte => lhs <= rhs,
69            CompareOp::Eq => (lhs - rhs).abs() < f64::EPSILON,
70        }
71    }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct Validation {
77    /// Goal name, or `overall`.
78    pub target: String,
79    pub name: String,
80    pub mode: Mode,
81    /// Natural-language statement of what is being checked.
82    pub statement: String,
83    pub detector: Detector,
84    /// A validation that must pass for the target to be satisfied. Non-blocking
85    /// validations are recorded but do not hold the gate shut.
86    #[serde(default = "yes")]
87    pub blocking: bool,
88}