1use crate::metrics::{BenchmarkMetrics, MotifMetric};
2use serde::{Deserialize, Serialize};
3
4pub const PRE_FAILURE_SLOW_DRIFT: &str = "pre_failure_slow_drift";
5pub const TRANSIENT_EXCURSION: &str = "transient_excursion";
6pub const TRANSITION_EXCURSION: &str = "transition_excursion";
7pub const RECURRENT_BOUNDARY_APPROACH: &str = "recurrent_boundary_approach";
8pub const PERSISTENT_INSTABILITY_CLUSTER: &str = "persistent_instability_cluster";
9pub const TRANSITION_CLUSTER_SUPPORT: &str = "transition_cluster_support";
10pub const WATCH_ONLY_BOUNDARY_GRAZING: &str = "watch_only_boundary_grazing";
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub enum HeuristicAlertClass {
14 Silent,
15 Watch,
16 Review,
17 Escalate,
18}
19
20impl HeuristicAlertClass {
21 pub fn as_lowercase(self) -> &'static str {
22 match self {
23 Self::Silent => "silent",
24 Self::Watch => "watch",
25 Self::Review => "review",
26 Self::Escalate => "escalate",
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy)]
32pub struct HeuristicPolicyDefinition {
33 pub motif_name: &'static str,
34 pub signature_definition: &'static str,
35 pub grammar_constraints: &'static str,
36 pub regime_conditions: &'static str,
37 pub applicability_rules: &'static str,
38 pub interpretation: &'static str,
39 pub alert_class_default: HeuristicAlertClass,
40 pub requires_persistence: bool,
41 pub requires_corroboration: bool,
42 pub minimum_window: usize,
43 pub minimum_hits: usize,
44 pub recommended_action: &'static str,
45 pub escalation_policy: &'static str,
46 pub non_unique_warning: &'static str,
47 pub known_limitations: &'static str,
48 pub contributes_to_dsa: bool,
49 pub suppresses_alert: bool,
50 pub promotes_alert: bool,
51}
52
53impl HeuristicPolicyDefinition {
54 pub fn maximum_allowed_fragmentation(self) -> f64 {
55 1.0 / self.minimum_hits.max(1) as f64
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
60pub struct FeaturePolicyOverride {
61 pub feature_index: usize,
62 pub feature_name: String,
63 pub alert_class_override: Option<HeuristicAlertClass>,
64 pub requires_persistence_override: Option<bool>,
65 pub requires_corroboration_override: Option<bool>,
66 pub minimum_window_override: Option<usize>,
67 pub minimum_hits_override: Option<usize>,
68 pub maximum_allowed_fragmentation_override: Option<f64>,
69 pub rescue_eligible: bool,
70 pub rescue_priority: usize,
71 pub allow_watch_only: Option<bool>,
72 pub allow_review_without_escalate: Option<bool>,
73 pub suppress_if_isolated: Option<bool>,
74 pub override_reason: String,
75}
76
77const POLICY_DEFINITIONS: &[HeuristicPolicyDefinition] = &[
78 HeuristicPolicyDefinition {
79 motif_name: PRE_FAILURE_SLOW_DRIFT,
80 signature_definition:
81 "Residual norm exceeds 0.5*rho with drift above the healthy-window drift threshold.",
82 grammar_constraints:
83 "grammar_state=Boundary and grammar_reason=SustainedOutwardDrift",
84 regime_conditions:
85 "Outward drift remains thresholded and curvature stays below the abrupt-slew regime.",
86 applicability_rules:
87 "Apply only after grammar filtering confirms boundary proximity without direct envelope exit.",
88 interpretation:
89 "Candidate DSA-compatible drift motif that supports closer monitoring or maintenance review.",
90 alert_class_default: HeuristicAlertClass::Review,
91 requires_persistence: true,
92 requires_corroboration: false,
93 minimum_window: 5,
94 minimum_hits: 2,
95 recommended_action:
96 "Increase review cadence, inspect neighboring channels, and corroborate with process context before intervention.",
97 escalation_policy:
98 "Escalate when the motif persists across repeated runs or is corroborated by scalar alarms and engineering context.",
99 non_unique_warning:
100 "This motif is not mechanism-specific and may reflect multiple latent causes.",
101 known_limitations:
102 "SECOM is anonymized and instance-level, so this motif does not support chamber-level attribution on its own.",
103 contributes_to_dsa: true,
104 suppresses_alert: false,
105 promotes_alert: true,
106 },
107 HeuristicPolicyDefinition {
108 motif_name: TRANSIENT_EXCURSION,
109 signature_definition:
110 "Residual norm enters the boundary zone with slew above the healthy-window slew threshold.",
111 grammar_constraints:
112 "grammar_state in {Boundary, Violation} and grammar_reason=AbruptSlewViolation",
113 regime_conditions:
114 "Curvature dominates the local sign tuple during a non-admissible excursion.",
115 applicability_rules:
116 "Apply only after grammar filtering confirms abrupt boundary interaction.",
117 interpretation:
118 "Compatible with transient upset or abrupt regime change, but not uniquely diagnostic.",
119 alert_class_default: HeuristicAlertClass::Silent,
120 requires_persistence: true,
121 requires_corroboration: true,
122 minimum_window: 5,
123 minimum_hits: 2,
124 recommended_action:
125 "Check for corroborating context, inspect neighboring channels, and prefer confirmation over immediate intervention.",
126 escalation_policy:
127 "Escalate only when repeated, clustered with other motifs, or accompanied by direct envelope violations.",
128 non_unique_warning:
129 "A transient excursion can reflect measurement noise, regime switch, or genuine degradation.",
130 known_limitations:
131 "A single abrupt excursion does not identify physical cause and may not persist long enough for confident attribution.",
132 contributes_to_dsa: true,
133 suppresses_alert: true,
134 promotes_alert: true,
135 },
136 HeuristicPolicyDefinition {
137 motif_name: RECURRENT_BOUNDARY_APPROACH,
138 signature_definition:
139 "Residual norm revisits the boundary zone repeatedly without a confirmed envelope exit.",
140 grammar_constraints:
141 "grammar_state=Boundary and grammar_reason=RecurrentBoundaryGrazing",
142 regime_conditions:
143 "Boundary revisitation persists without direct envelope exit and without stable violation.",
144 applicability_rules:
145 "Apply only after grammar filtering confirms repeated boundary approach under the local envelope.",
146 interpretation:
147 "Ambiguous DSA motif that warrants continued observation rather than decisive attribution.",
148 alert_class_default: HeuristicAlertClass::Watch,
149 requires_persistence: true,
150 requires_corroboration: true,
151 minimum_window: 10,
152 minimum_hits: 3,
153 recommended_action:
154 "Track persistence, compare against the scalar baselines, and prioritize manual review over automatic maintenance action.",
155 escalation_policy:
156 "Escalate when recurrent grazing concentrates in pre-failure windows or transitions into direct violations.",
157 non_unique_warning:
158 "Repeated boundary grazing can arise from nuisance variation as well as meaningful DSA structure.",
159 known_limitations:
160 "This motif is especially sensitive to envelope and drift thresholds, so calibration materially affects its prevalence.",
161 contributes_to_dsa: true,
162 suppresses_alert: true,
163 promotes_alert: true,
164 },
165];
166
167const EXPANDED_POLICY_DEFINITIONS: &[HeuristicPolicyDefinition] = &[
168 HeuristicPolicyDefinition {
169 motif_name: TRANSITION_EXCURSION,
170 signature_definition:
171 "Grammar-qualified transition motif with elevated slew, non-admissible envelope interaction, and abrupt state change.",
172 grammar_constraints:
173 "grammar_state in {TransientViolation, PersistentViolation} and grammar_reason=AbruptSlewViolation",
174 regime_conditions:
175 "Curvature dominates the sign tuple while the trajectory departs admissibility.",
176 applicability_rules:
177 "Apply only after grammar filtering confirms abrupt transition pressure at the envelope.",
178 interpretation:
179 "Candidate transition-instability event with elevated structural salience but ambiguous physical cause.",
180 alert_class_default: HeuristicAlertClass::Review,
181 requires_persistence: false,
182 requires_corroboration: false,
183 minimum_window: 3,
184 minimum_hits: 1,
185 recommended_action:
186 "Inspect adjacent channels and grouped corroborators before promoting beyond Review.",
187 escalation_policy:
188 "Escalate only when the transition persists, repeats, or aligns with grouped corroboration.",
189 non_unique_warning:
190 "A transition excursion is not a unique fault signature and may reflect multiple process changes.",
191 known_limitations:
192 "SECOM does not expose mechanism labels, so this motif remains interpretive rather than causal.",
193 contributes_to_dsa: false,
194 suppresses_alert: false,
195 promotes_alert: true,
196 },
197 HeuristicPolicyDefinition {
198 motif_name: PERSISTENT_INSTABILITY_CLUSTER,
199 signature_definition:
200 "Repeated or sustained outward grammar pressure that is not reducible to isolated spikes.",
201 grammar_constraints:
202 "grammar_state in {SustainedOutwardDrift, PersistentViolation}",
203 regime_conditions:
204 "Non-admissible pressure recurs across neighboring runs with bounded fragmentation.",
205 applicability_rules:
206 "Apply only after grammar filtering confirms sustained pressure rather than single-point excursions.",
207 interpretation:
208 "Candidate persistent instability regime with potential operator significance if corroborated.",
209 alert_class_default: HeuristicAlertClass::Review,
210 requires_persistence: true,
211 requires_corroboration: true,
212 minimum_window: 5,
213 minimum_hits: 2,
214 recommended_action:
215 "Review grouped corroborators, inspect adjacent precursor channels, and preserve ambiguity explicitly.",
216 escalation_policy:
217 "Escalate only when grouped corroboration or persistent violation confirms sustained structure.",
218 non_unique_warning:
219 "Persistent instability remains semantically ambiguous and does not identify a unique root cause.",
220 known_limitations:
221 "This motif is sensitive to persistence choices and grouped corroboration windows.",
222 contributes_to_dsa: false,
223 suppresses_alert: false,
224 promotes_alert: true,
225 },
226 HeuristicPolicyDefinition {
227 motif_name: TRANSITION_CLUSTER_SUPPORT,
228 signature_definition:
229 "Corroborating burst or boundary-pressure feature that aligns temporally with a primary precursor feature.",
230 grammar_constraints:
231 "grammar_state in {BoundaryGrazing, SustainedOutwardDrift, TransientViolation}",
232 regime_conditions:
233 "Supportive structure is temporally aligned with a grouped primary feature rather than isolated.",
234 applicability_rules:
235 "Apply only after grammar filtering and grouped temporal alignment confirm corroboration.",
236 interpretation:
237 "Supportive corroborator motif that increases confidence in another feature but is not decisive alone.",
238 alert_class_default: HeuristicAlertClass::Watch,
239 requires_persistence: false,
240 requires_corroboration: true,
241 minimum_window: 3,
242 minimum_hits: 1,
243 recommended_action:
244 "Use as corroboration support; do not escalate on this motif alone.",
245 escalation_policy:
246 "Never escalate solely from support motifs without a primary precursor feature.",
247 non_unique_warning:
248 "Corroboration support indicates temporal alignment, not causal identity.",
249 known_limitations:
250 "Grouped alignment is deterministic but remains a limited surrogate for true mechanism coupling.",
251 contributes_to_dsa: false,
252 suppresses_alert: true,
253 promotes_alert: true,
254 },
255 HeuristicPolicyDefinition {
256 motif_name: WATCH_ONLY_BOUNDARY_GRAZING,
257 signature_definition:
258 "Boundary proximity without sufficient persistence or corroboration for Review promotion.",
259 grammar_constraints: "grammar_state=BoundaryGrazing",
260 regime_conditions:
261 "Admissibility pressure is visible but remains weak, isolated, or sentinel-like.",
262 applicability_rules:
263 "Apply only after grammar filtering confirms envelope grazing without sustained outward drift.",
264 interpretation:
265 "Low-amplitude sentinel signal appropriate for Watch-only handling.",
266 alert_class_default: HeuristicAlertClass::Watch,
267 requires_persistence: false,
268 requires_corroboration: true,
269 minimum_window: 3,
270 minimum_hits: 1,
271 recommended_action:
272 "Retain as Watch-only and wait for corroboration before manual investigation.",
273 escalation_policy:
274 "Do not escalate directly from boundary grazing without stronger semantic support.",
275 non_unique_warning:
276 "Boundary grazing alone is structurally ambiguous and often nuisance-dominated.",
277 known_limitations:
278 "This motif deliberately favors burden suppression over coverage recovery when isolated.",
279 contributes_to_dsa: false,
280 suppresses_alert: true,
281 promotes_alert: false,
282 },
283];
284
285#[derive(Debug, Clone, Serialize)]
286pub struct HeuristicEntry {
287 pub motif_name: String,
288 pub signature_definition: String,
289 pub grammar_constraints: String,
290 pub regime_conditions: String,
291 pub applicability_rules: String,
292 pub applicable_dataset: String,
293 pub provenance_status: String,
294 pub interpretation: String,
295 pub severity: String,
296 pub confidence: String,
297 pub alert_class_default: HeuristicAlertClass,
298 pub requires_persistence: bool,
299 pub requires_corroboration: bool,
300 pub minimum_window: usize,
301 pub minimum_hits: usize,
302 pub maximum_allowed_fragmentation: f64,
303 pub recommended_action: String,
304 pub escalation_policy: String,
305 pub non_unique_warning: String,
306 pub known_limitations: String,
307 pub contributes_to_dsa_scoring: bool,
308 pub contributes_to_dsa: bool,
309 pub suppresses_alert: bool,
310 pub promotes_alert: bool,
311 pub observed_point_hits: usize,
312 pub observed_run_hits: usize,
313 pub pre_failure_window_run_hits: usize,
314 pub pre_failure_window_precision_proxy: Option<f64>,
315 pub status_note: String,
316}
317
318pub fn dsa_contributing_motif_names() -> &'static [&'static str] {
319 &[
320 PRE_FAILURE_SLOW_DRIFT,
321 TRANSIENT_EXCURSION,
322 RECURRENT_BOUNDARY_APPROACH,
323 ]
324}
325
326pub fn heuristic_policy_definitions() -> &'static [HeuristicPolicyDefinition] {
327 POLICY_DEFINITIONS
328}
329
330pub fn heuristic_policy_definition(motif_name: &str) -> Option<HeuristicPolicyDefinition> {
331 POLICY_DEFINITIONS
332 .iter()
333 .copied()
334 .chain(EXPANDED_POLICY_DEFINITIONS.iter().copied())
335 .find(|definition| definition.motif_name == motif_name)
336}
337
338pub fn expanded_semantic_policy_definitions() -> Vec<HeuristicPolicyDefinition> {
339 POLICY_DEFINITIONS
340 .iter()
341 .copied()
342 .chain(EXPANDED_POLICY_DEFINITIONS.iter().copied())
343 .collect()
344}
345
346pub fn build_heuristics_bank(
347 metrics: &BenchmarkMetrics,
348 dataset_name: &str,
349) -> Vec<HeuristicEntry> {
350 POLICY_DEFINITIONS
351 .iter()
352 .map(|definition| {
353 let metric = motif(metrics, definition.motif_name);
354 HeuristicEntry {
355 motif_name: definition.motif_name.into(),
356 signature_definition: definition.signature_definition.into(),
357 grammar_constraints: definition.grammar_constraints.into(),
358 regime_conditions: definition.regime_conditions.into(),
359 applicability_rules: definition.applicability_rules.into(),
360 applicable_dataset: dataset_name.into(),
361 provenance_status: observed_status(metric),
362 interpretation: definition.interpretation.into(),
363 severity: definition.alert_class_default.as_lowercase().into(),
364 confidence: confidence_note(metric),
365 alert_class_default: definition.alert_class_default,
366 requires_persistence: definition.requires_persistence,
367 requires_corroboration: definition.requires_corroboration,
368 minimum_window: definition.minimum_window,
369 minimum_hits: definition.minimum_hits,
370 maximum_allowed_fragmentation: definition.maximum_allowed_fragmentation(),
371 recommended_action: definition.recommended_action.into(),
372 escalation_policy: definition.escalation_policy.into(),
373 non_unique_warning: definition.non_unique_warning.into(),
374 known_limitations: definition.known_limitations.into(),
375 contributes_to_dsa_scoring: definition.contributes_to_dsa,
376 contributes_to_dsa: definition.contributes_to_dsa,
377 suppresses_alert: definition.suppresses_alert,
378 promotes_alert: definition.promotes_alert,
379 observed_point_hits: metric.point_hits,
380 observed_run_hits: metric.run_hits,
381 pre_failure_window_run_hits: metric.pre_failure_window_run_hits,
382 pre_failure_window_precision_proxy: metric.pre_failure_window_precision_proxy,
383 status_note: format!(
384 "Observed {} points and {} run hits; {} of those run hits fall inside the configured pre-failure windows. Default alert class is {} with minimum_window={}, minimum_hits={}, and maximum_allowed_fragmentation={:.4}.",
385 metric.point_hits,
386 metric.run_hits,
387 metric.pre_failure_window_run_hits,
388 definition.alert_class_default.as_lowercase(),
389 definition.minimum_window,
390 definition.minimum_hits,
391 definition.maximum_allowed_fragmentation(),
392 ),
393 }
394 })
395 .collect()
396}
397
398fn motif<'a>(metrics: &'a BenchmarkMetrics, motif_name: &str) -> &'a MotifMetric {
399 metrics
400 .motif_metrics
401 .iter()
402 .find(|metric| metric.motif_name == motif_name)
403 .unwrap_or_else(|| panic!("missing motif metric for {motif_name}"))
404}
405
406fn observed_status(metric: &MotifMetric) -> String {
407 if metric.point_hits > 0 {
408 "SECOM-observed".into()
409 } else {
410 "framework-defined".into()
411 }
412}
413
414fn confidence_note(metric: &MotifMetric) -> String {
415 if metric.point_hits > 0 {
416 "Stage-II observed on SECOM; interpretive and non-mechanistic.".into()
417 } else {
418 "Framework-defined only; not yet observed in the current run.".into()
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::metrics::{
426 BenchmarkMetrics, BoundaryEpisodeSummary, DensitySummary, LeadTimeSummary,
427 };
428 use crate::preprocessing::DatasetSummary;
429
430 fn sample_metrics() -> BenchmarkMetrics {
431 BenchmarkMetrics {
432 summary: crate::metrics::BenchmarkSummary {
433 dataset_summary: DatasetSummary {
434 run_count: 10,
435 feature_count: 3,
436 pass_count: 8,
437 fail_count: 2,
438 dataset_missing_fraction: 0.0,
439 healthy_pass_runs_requested: 3,
440 healthy_pass_runs_found: 3,
441 },
442 analyzable_feature_count: 3,
443 grammar_imputation_suppression_points: 0,
444 threshold_alarm_points: 0,
445 ewma_alarm_points: 0,
446 cusum_alarm_points: 0,
447 run_energy_alarm_points: 0,
448 pca_fdc_alarm_points: 0,
449 dsfb_raw_boundary_points: 0,
450 dsfb_persistent_boundary_points: 0,
451 dsfb_raw_violation_points: 0,
452 dsfb_persistent_violation_points: 0,
453 failure_runs: 2,
454 failure_runs_with_preceding_dsfb_raw_signal: 0,
455 failure_runs_with_preceding_dsfb_persistent_signal: 0,
456 failure_runs_with_preceding_dsfb_raw_boundary_signal: 0,
457 failure_runs_with_preceding_dsfb_persistent_boundary_signal: 0,
458 failure_runs_with_preceding_dsfb_raw_violation_signal: 0,
459 failure_runs_with_preceding_dsfb_persistent_violation_signal: 0,
460 failure_runs_with_preceding_ewma_signal: 0,
461 failure_runs_with_preceding_cusum_signal: 0,
462 failure_runs_with_preceding_run_energy_signal: 0,
463 failure_runs_with_preceding_pca_fdc_signal: 0,
464 failure_runs_with_preceding_threshold_signal: 0,
465 pass_runs: 8,
466 pass_runs_with_dsfb_raw_boundary_signal: 0,
467 pass_runs_with_dsfb_persistent_boundary_signal: 0,
468 pass_runs_with_dsfb_raw_violation_signal: 0,
469 pass_runs_with_dsfb_persistent_violation_signal: 0,
470 pass_runs_with_ewma_signal: 0,
471 pass_runs_with_cusum_signal: 0,
472 pass_runs_with_run_energy_signal: 0,
473 pass_runs_with_pca_fdc_signal: 0,
474 pass_runs_with_threshold_signal: 0,
475 pass_run_dsfb_raw_boundary_nuisance_rate: 0.0,
476 pass_run_dsfb_persistent_boundary_nuisance_rate: 0.0,
477 pass_run_dsfb_raw_violation_nuisance_rate: 0.0,
478 pass_run_dsfb_persistent_violation_nuisance_rate: 0.0,
479 pass_run_ewma_nuisance_rate: 0.0,
480 pass_run_cusum_nuisance_rate: 0.0,
481 pass_run_run_energy_nuisance_rate: 0.0,
482 pass_run_pca_fdc_nuisance_rate: 0.0,
483 pass_run_threshold_nuisance_rate: 0.0,
484 },
485 lead_time_summary: LeadTimeSummary {
486 failure_runs_with_raw_boundary_lead: 0,
487 failure_runs_with_persistent_boundary_lead: 0,
488 failure_runs_with_raw_violation_lead: 0,
489 failure_runs_with_persistent_violation_lead: 0,
490 failure_runs_with_threshold_lead: 0,
491 failure_runs_with_ewma_lead: 0,
492 failure_runs_with_cusum_lead: 0,
493 failure_runs_with_run_energy_lead: 0,
494 failure_runs_with_pca_fdc_lead: 0,
495 mean_raw_boundary_lead_runs: None,
496 mean_persistent_boundary_lead_runs: None,
497 mean_raw_violation_lead_runs: None,
498 mean_persistent_violation_lead_runs: None,
499 mean_threshold_lead_runs: None,
500 mean_ewma_lead_runs: None,
501 mean_cusum_lead_runs: None,
502 mean_run_energy_lead_runs: None,
503 mean_pca_fdc_lead_runs: None,
504 mean_raw_boundary_minus_cusum_delta_runs: None,
505 mean_raw_boundary_minus_run_energy_delta_runs: None,
506 mean_raw_boundary_minus_pca_fdc_delta_runs: None,
507 mean_raw_boundary_minus_threshold_delta_runs: None,
508 mean_raw_boundary_minus_ewma_delta_runs: None,
509 mean_persistent_boundary_minus_cusum_delta_runs: None,
510 mean_persistent_boundary_minus_run_energy_delta_runs: None,
511 mean_persistent_boundary_minus_pca_fdc_delta_runs: None,
512 mean_persistent_boundary_minus_threshold_delta_runs: None,
513 mean_persistent_boundary_minus_ewma_delta_runs: None,
514 mean_raw_violation_minus_cusum_delta_runs: None,
515 mean_raw_violation_minus_run_energy_delta_runs: None,
516 mean_raw_violation_minus_pca_fdc_delta_runs: None,
517 mean_raw_violation_minus_threshold_delta_runs: None,
518 mean_raw_violation_minus_ewma_delta_runs: None,
519 mean_persistent_violation_minus_cusum_delta_runs: None,
520 mean_persistent_violation_minus_run_energy_delta_runs: None,
521 mean_persistent_violation_minus_pca_fdc_delta_runs: None,
522 mean_persistent_violation_minus_threshold_delta_runs: None,
523 mean_persistent_violation_minus_ewma_delta_runs: None,
524 },
525 density_summary: DensitySummary {
526 density_window: 3,
527 mean_raw_boundary_density_failure: 0.0,
528 mean_raw_boundary_density_pass: 0.0,
529 mean_persistent_boundary_density_failure: 0.0,
530 mean_persistent_boundary_density_pass: 0.0,
531 mean_raw_violation_density_failure: 0.0,
532 mean_raw_violation_density_pass: 0.0,
533 mean_persistent_violation_density_failure: 0.0,
534 mean_persistent_violation_density_pass: 0.0,
535 mean_threshold_density_failure: 0.0,
536 mean_threshold_density_pass: 0.0,
537 mean_ewma_density_failure: 0.0,
538 mean_ewma_density_pass: 0.0,
539 mean_cusum_density_failure: 0.0,
540 mean_cusum_density_pass: 0.0,
541 },
542 boundary_episode_summary: BoundaryEpisodeSummary {
543 raw_episode_count: 0,
544 persistent_episode_count: 0,
545 mean_raw_episode_length: None,
546 mean_persistent_episode_length: None,
547 max_raw_episode_length: 0,
548 max_persistent_episode_length: 0,
549 raw_non_escalating_episode_fraction: None,
550 persistent_non_escalating_episode_fraction: None,
551 },
552 dsa_summary: None,
553 motif_metrics: vec![
554 MotifMetric {
555 motif_name: PRE_FAILURE_SLOW_DRIFT.into(),
556 point_hits: 5,
557 run_hits: 4,
558 pre_failure_window_run_hits: 3,
559 pre_failure_window_precision_proxy: Some(0.75),
560 },
561 MotifMetric {
562 motif_name: TRANSIENT_EXCURSION.into(),
563 point_hits: 2,
564 run_hits: 2,
565 pre_failure_window_run_hits: 1,
566 pre_failure_window_precision_proxy: Some(0.5),
567 },
568 MotifMetric {
569 motif_name: RECURRENT_BOUNDARY_APPROACH.into(),
570 point_hits: 7,
571 run_hits: 5,
572 pre_failure_window_run_hits: 3,
573 pre_failure_window_precision_proxy: Some(0.6),
574 },
575 ],
576 per_failure_run_signals: Vec::new(),
577 density_metrics: Vec::new(),
578 feature_metrics: Vec::new(),
579 top_feature_indices: Vec::new(),
580 }
581 }
582
583 #[test]
584 fn heuristic_policy_mapping_is_deterministic() {
585 let bank = build_heuristics_bank(&sample_metrics(), "SECOM");
586 let transient = bank
587 .iter()
588 .find(|entry| entry.motif_name == TRANSIENT_EXCURSION)
589 .unwrap();
590 let recurrent = bank
591 .iter()
592 .find(|entry| entry.motif_name == RECURRENT_BOUNDARY_APPROACH)
593 .unwrap();
594 let drift = bank
595 .iter()
596 .find(|entry| entry.motif_name == PRE_FAILURE_SLOW_DRIFT)
597 .unwrap();
598
599 assert_eq!(transient.alert_class_default, HeuristicAlertClass::Silent);
600 assert_eq!(recurrent.alert_class_default, HeuristicAlertClass::Watch);
601 assert_eq!(drift.alert_class_default, HeuristicAlertClass::Review);
602 assert!(transient.requires_corroboration);
603 assert!(recurrent.requires_corroboration);
604 assert!(!drift.requires_corroboration);
605 }
606
607 #[test]
608 fn maximum_fragmentation_defaults_follow_minimum_hits() {
609 let transient = heuristic_policy_definition(TRANSIENT_EXCURSION).unwrap();
610 let recurrent = heuristic_policy_definition(RECURRENT_BOUNDARY_APPROACH).unwrap();
611
612 assert!((transient.maximum_allowed_fragmentation() - 0.5).abs() < 1.0e-9);
613 assert!((recurrent.maximum_allowed_fragmentation() - (1.0 / 3.0)).abs() < 1.0e-9);
614 }
615}