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 serde::Serialize;
12
13/// Descriptor of one criterion in an engine's criteria catalog (spec §7.2).
14///
15/// `key` is stable per engine: applications persist valuations against it,
16/// so renaming one is a compatibility break.
17#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct CriterionDescriptor {
20    /// Stable criterion identifier, unique within the engine.
21    pub key: &'static str,
22    /// Human-facing name.
23    pub label: &'static str,
24    /// One or two sentences on what the criterion judges.
25    pub help: &'static str,
26    /// §5 quantity key — values are in the quantity's SI display unit —
27    /// or `None` for a dimensionless criterion.
28    pub quantity: Option<&'static str>,
29    /// Shape of the criterion's value.
30    pub kind: CriterionKind,
31}
32
33/// Shape of one criterion's value (spec §7.2).
34#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
35#[serde(tag = "type", rename_all = "camelCase")]
36pub enum CriterionKind {
37    /// A single number.
38    Value {
39        /// The engine's conventional standard.
40        default: f64,
41    },
42    /// An ordered list of named cut points; a valuation supplies a
43    /// same-length ascending list of numbers.
44    Band {
45        /// Cut points, defaults strictly ascending.
46        cuts: &'static [BandCut],
47    },
48}
49
50/// One named cut point of a band criterion (spec §7.2).
51#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
52#[serde(rename_all = "camelCase")]
53pub struct BandCut {
54    /// Stable name of this cut within the band.
55    pub key: &'static str,
56    /// Human-facing label.
57    pub label: &'static str,
58    /// The engine's conventional standard for this cut.
59    pub default: f64,
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    /// The wire shape editors consume: camelCase, kind tagged by `type` —
67    /// the same conventions as every other descriptor in this crate.
68    #[test]
69    fn descriptors_serialize_in_wire_shape() {
70        let d = CriterionDescriptor {
71            key: "freeboard",
72            label: "Freeboard",
73            help: "Clearance kept below the rim.",
74            quantity: Some("depth"),
75            kind: CriterionKind::Value { default: 0.3 },
76        };
77        let json = serde_json::to_value(d).unwrap();
78        assert_eq!(json["quantity"], "depth");
79        assert_eq!(json["kind"]["type"], "value");
80        assert_eq!(json["kind"]["default"], 0.3);
81
82        let band = CriterionDescriptor {
83            key: "velocity",
84            label: "Velocity",
85            help: "Self-cleansing to erosive.",
86            quantity: Some("velocity"),
87            kind: CriterionKind::Band {
88                cuts: &[
89                    BandCut {
90                        key: "selfCleansing",
91                        label: "Self-cleansing",
92                        default: 0.6,
93                    },
94                    BandCut {
95                        key: "erosive",
96                        label: "Erosive",
97                        default: 3.0,
98                    },
99                ],
100            },
101        };
102        let json = serde_json::to_value(band).unwrap();
103        assert_eq!(json["kind"]["type"], "band");
104        assert_eq!(json["kind"]["cuts"][1]["key"], "erosive");
105    }
106}