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}