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}