Skip to main content

hydra_common/
criteria.rs

1//! Criteria contract: engine-published descriptors of the assessment
2//! standard a user asserts over simulated behaviour (spec §7).
3//!
4//! Everything here is *description*. The foundation knows no criterion
5//! vocabulary: keys are opaque, meaning travels through engine-authored
6//! text, and how a valuation shapes block production is the engine's own
7//! (spec §7.4). Valuations themselves are plain JSON objects (spec §7.3)
8//! and have no type here — they are caller-held data this layer never
9//! interprets.
10
11use crate::CategorySeverity;
12use serde::Serialize;
13
14/// Descriptor of one criterion in an engine's criteria catalog (spec §7.2).
15///
16/// `key` is stable per engine: applications persist valuations against it,
17/// so renaming one is a compatibility break.
18#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct CriterionDescriptor {
21    /// Stable criterion identifier, unique within the engine.
22    pub key: &'static str,
23    /// Human-facing name.
24    pub label: &'static str,
25    /// One or two sentences on what the criterion judges.
26    pub help: &'static str,
27    /// §5 quantity key — values are in the quantity's SI display unit —
28    /// or `None` for a dimensionless criterion.
29    pub quantity: Option<&'static str>,
30    /// Shape of the criterion's value.
31    pub kind: CriterionKind,
32    /// What each region between the cut points means, ascending — one
33    /// more entry than the criterion has cuts (spec §7.2).
34    ///
35    /// Empty for a criterion that is judged in reports but never drawn.
36    /// The engine's to state because compliance is rarely monotonic:
37    /// service pressure is worst when low, acceptable in a middle and
38    /// worth attention again when high, and nothing in the numbers says
39    /// which end is which.
40    pub severities: &'static [CategorySeverity],
41}
42
43/// Shape of one criterion's value (spec §7.2).
44#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
45#[serde(tag = "type", rename_all = "camelCase")]
46pub enum CriterionKind {
47    /// A single number.
48    Value {
49        /// The engine's conventional standard.
50        default: f64,
51    },
52    /// An ordered list of named cut points; a valuation supplies a
53    /// same-length ascending list of numbers.
54    Band {
55        /// Cut points, defaults strictly ascending.
56        cuts: &'static [BandCut],
57    },
58}
59
60/// One named cut point of a band criterion (spec §7.2).
61#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
62#[serde(rename_all = "camelCase")]
63pub struct BandCut {
64    /// Stable name of this cut within the band.
65    pub key: &'static str,
66    /// Human-facing label.
67    pub label: &'static str,
68    /// The engine's conventional standard for this cut.
69    pub default: f64,
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    /// The wire shape editors consume: camelCase, kind tagged by `type` —
77    /// the same conventions as every other descriptor in this crate.
78    #[test]
79    fn descriptors_serialize_in_wire_shape() {
80        let d = CriterionDescriptor {
81            key: "freeboard",
82            label: "Freeboard",
83            help: "Clearance kept below the rim.",
84            quantity: Some("depth"),
85            kind: CriterionKind::Value { default: 0.3 },
86            severities: &[CategorySeverity::Alarm, CategorySeverity::Nominal],
87        };
88        let json = serde_json::to_value(d).unwrap();
89        assert_eq!(json["quantity"], "depth");
90        assert_eq!(json["kind"]["type"], "value");
91        assert_eq!(json["kind"]["default"], 0.3);
92        // Two regions for one cut: below the freeboard and at or above it.
93        assert_eq!(json["severities"][0], "alarm");
94        assert_eq!(json["severities"][1], "nominal");
95
96        let band = CriterionDescriptor {
97            key: "velocity",
98            label: "Velocity",
99            help: "Self-cleansing to erosive.",
100            quantity: Some("velocity"),
101            kind: CriterionKind::Band {
102                cuts: &[
103                    BandCut {
104                        key: "selfCleansing",
105                        label: "Self-cleansing",
106                        default: 0.6,
107                    },
108                    BandCut {
109                        key: "erosive",
110                        label: "Erosive",
111                        default: 3.0,
112                    },
113                ],
114            },
115            severities: &[
116                CategorySeverity::Caution,
117                CategorySeverity::Nominal,
118                CategorySeverity::Alarm,
119            ],
120        };
121        let json = serde_json::to_value(band).unwrap();
122        assert_eq!(json["kind"]["type"], "band");
123        assert_eq!(json["kind"]["cuts"][1]["key"], "erosive");
124        assert_eq!(json["severities"][2], "alarm");
125    }
126}