1use axioval_ir::contract::{
10 self as schema, CategoryLevel, Selector, SeverityBand, SeverityOverride,
11};
12use axioval_ir::{NotEvaluatedReason, Object, Severity};
13
14use crate::{CapabilityEvaluation, CompiledRule, RuleContext, SelectorVerdict};
15
16#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct Deviation {
23 lower: f64,
24 upper: f64,
25}
26
27impl Deviation {
28 #[must_use]
31 pub fn try_new(lower: f64, upper: f64) -> Option<Self> {
32 (lower >= 0.0 && lower <= upper).then_some(Self { lower, upper })
33 }
34
35 #[must_use]
40 pub fn below(minimum: f64, lower: f64, upper: f64) -> Self {
41 Self::relative(minimum - upper, minimum - lower, minimum)
42 }
43
44 #[must_use]
47 pub fn above(maximum: f64, lower: f64, upper: f64) -> Self {
48 Self::relative(lower - maximum, upper - maximum, maximum)
49 }
50
51 fn relative(least: f64, most: f64, bound: f64) -> Self {
52 let scale = bound.abs();
53 let (lower, upper) = if scale == 0.0 {
54 let infinite = |miss: f64| if miss > 0.0 { f64::INFINITY } else { 0.0 };
55 (infinite(least), infinite(most))
56 } else {
57 (
60 (least / scale).next_down().next_down(),
61 (most / scale).next_up().next_up(),
62 )
63 };
64 let lower = if lower.is_nan() { 0.0 } else { lower.max(0.0) };
65 let upper = if upper.is_nan() {
66 f64::INFINITY
67 } else {
68 upper.max(lower)
69 };
70 Self { lower, upper }
71 }
72
73 #[must_use]
77 pub fn worst(self, other: Self) -> Self {
78 Self {
79 lower: self.lower.max(other.lower),
80 upper: self.upper.max(other.upper),
81 }
82 }
83
84 #[must_use]
87 pub fn least(self, other: Self) -> Self {
88 Self {
89 lower: self.lower.min(other.lower),
90 upper: self.upper.min(other.upper),
91 }
92 }
93
94 #[must_use]
96 pub fn lower(&self) -> f64 {
97 self.lower
98 }
99
100 #[must_use]
102 pub fn upper(&self) -> f64 {
103 self.upper
104 }
105}
106
107#[derive(Clone, Debug, Default, PartialEq)]
110pub struct RuleRefinement {
111 pub severity_bands: Vec<SeverityBand>,
113 pub severity_overrides: Vec<SeverityOverride>,
116 pub categories: Vec<CategoryLevel>,
118}
119
120impl RuleRefinement {
121 #[must_use]
123 pub fn is_empty(&self) -> bool {
124 self.severity_bands.is_empty()
125 && self.severity_overrides.is_empty()
126 && self.categories.is_empty()
127 }
128
129 #[must_use]
132 pub fn needs_refiner(&self) -> bool {
133 !self.severity_overrides.is_empty() || !self.categories.is_empty()
134 }
135}
136
137pub trait OutcomeRefiner: Send + Sync {
147 fn refine(
150 &self,
151 context: &RuleContext<'_>,
152 rule: &CompiledRule,
153 refining: &Refining<'_>,
154 evaluation: &mut CapabilityEvaluation,
155 );
156
157 fn selected(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> usize;
160
161 fn evaluate_selector(
169 &self,
170 context: &RuleContext<'_>,
171 selector: &Selector,
172 object: &Object,
173 ) -> SelectorVerdict {
174 let _ = (context, selector, object);
175 SelectorVerdict::Undecided(
176 NotEvaluatedReason::MissingService,
177 "the host's outcome refiner evaluates no selectors".into(),
178 )
179 }
180}
181
182#[derive(Clone, Copy, Debug)]
185pub struct Refining<'a> {
186 pub refinement: &'a RuleRefinement,
188 pub locations: Option<&'a LocationPolicy>,
190}
191
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
195pub enum LocationMethod {
196 Storeys,
199 Containers,
202 Geometry,
206}
207
208#[derive(Clone, Debug, Eq, PartialEq)]
218pub struct LocationPolicy {
219 pub method: LocationMethod,
220 pub storey_kinds: Vec<String>,
221 pub space_kinds: Vec<String>,
222 pub containment: Vec<String>,
223 pub name: Option<(String, String)>,
224}
225
226pub(crate) fn validate_bands(bands: &[SeverityBand]) -> Result<(), String> {
229 let mut previous = 0.0;
230 for (index, band) in bands.iter().enumerate() {
231 if !band.below.is_finite() || band.below <= previous {
232 return Err(format!(
233 "severity band {index} must lie below a finite threshold above {previous}"
234 ));
235 }
236 previous = band.below;
237 }
238 Ok(())
239}
240
241pub(crate) fn grade(
248 bands: &[SeverityBand],
249 beyond: &Severity,
250 deviation: Deviation,
251) -> (Severity, bool) {
252 let mut reached: Vec<Severity> = Vec::new();
253 let mut from = 0.0;
254 for band in bands {
255 if deviation.lower < band.below && deviation.upper >= from {
256 reached.push(report_severity(&band.severity));
257 }
258 from = band.below;
259 }
260 if deviation.upper >= from {
261 reached.push(beyond.clone());
262 }
263 let worst = reached
265 .iter()
266 .min()
267 .cloned()
268 .unwrap_or_else(|| beyond.clone());
269 let mixed = reached.iter().any(|severity| *severity != worst);
270 (worst, mixed)
271}
272
273#[must_use]
275pub fn report_severity(severity: &schema::Severity) -> Severity {
276 match severity {
277 schema::Severity::Error => Severity::Error,
278 schema::Severity::Warning => Severity::Warning,
279 schema::Severity::Info => Severity::Info,
280 }
281}
282
283pub(crate) fn label(severity: &Severity) -> &'static str {
285 match severity {
286 Severity::Error => "error",
287 Severity::Warning => "warning",
288 Severity::Info => "info",
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 fn bands() -> Vec<SeverityBand> {
297 vec![
298 SeverityBand {
299 below: 0.05,
300 severity: schema::Severity::Info,
301 },
302 SeverityBand {
303 below: 0.2,
304 severity: schema::Severity::Warning,
305 },
306 ]
307 }
308
309 #[test]
310 fn a_deviation_takes_its_band_and_the_rule_severity_beyond() {
311 let grade = |lower, upper| grade(&bands(), &Severity::Error, Deviation { lower, upper }).0;
312 assert_eq!(grade(0.03, 0.03), Severity::Info);
313 assert_eq!(grade(0.1, 0.1), Severity::Warning);
314 assert_eq!(grade(0.3, 0.3), Severity::Error);
315 assert_eq!(grade(0.05, 0.05), Severity::Warning);
316 assert_eq!(grade(0.2, f64::INFINITY), Severity::Error);
317 }
318
319 #[test]
320 fn a_straddling_deviation_takes_its_most_severe_band() {
321 let (severity, mixed) = grade(
322 &bands(),
323 &Severity::Error,
324 Deviation {
325 lower: 0.03,
326 upper: 0.1,
327 },
328 );
329 assert_eq!(severity, Severity::Warning);
330 assert!(mixed);
331 let (severity, mixed) = grade(
332 &bands(),
333 &Severity::Error,
334 Deviation {
335 lower: 0.1,
336 upper: 0.25,
337 },
338 );
339 assert_eq!(severity, Severity::Error);
340 assert!(mixed);
341 }
342
343 #[test]
344 fn a_relative_deviation_holds_the_exact_quotient() {
345 let shortfall = Deviation::below(10.0, 7.0, 7.0);
346 assert!(shortfall.lower() <= 0.3 && 0.3 <= shortfall.upper());
347 let excess = Deviation::above(2.0, 2.5, 3.0);
348 assert!(excess.lower() <= 0.25 && 0.5 <= excess.upper());
349 assert!(Deviation::below(0.0, -1.0, -1.0).upper().is_infinite());
350 assert!(
351 Deviation::above(1.0, 2.0, f64::INFINITY)
352 .upper()
353 .is_infinite()
354 );
355 }
356
357 #[test]
358 fn bands_must_ascend_from_zero() {
359 assert!(validate_bands(&bands()).is_ok());
360 let mut descending = bands();
361 descending.reverse();
362 assert!(validate_bands(&descending).is_err());
363 assert!(
364 validate_bands(&[SeverityBand {
365 below: 0.0,
366 severity: schema::Severity::Info
367 }])
368 .is_err()
369 );
370 }
371}