Skip to main content

loopsmith_core/config/
validation.rs

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