Skip to main content

dsfb_semiconductor/
secom_addendum.rs

1use crate::baselines::BaselineSet;
2use crate::cohort::OptimizationExecution;
3use crate::error::{DsfbSemiconductorError, Result};
4use crate::failure_driven::FailureDrivenArtifacts;
5use crate::grammar::{GrammarSet, GrammarState};
6use crate::metrics::BenchmarkMetrics;
7use crate::precursor::{DsaEvaluation, DsaPolicyState};
8use crate::preprocessing::PreparedDataset;
9use crate::residual::ResidualSet;
10use crate::semiotics::{DsfbMotifClass, MotifSet, SemanticLayer};
11use plotters::prelude::*;
12use serde::Serialize;
13use std::collections::{BTreeMap, BTreeSet};
14use std::path::Path;
15
16const TRADEOFF_PLOT_WIDTH: u32 = 1400;
17const TRADEOFF_PLOT_HEIGHT: u32 = 800;
18
19#[derive(Debug, Clone, Serialize)]
20pub struct RecurrentBoundaryStats {
21    pub total_boundary_points: usize,
22    pub total_run_hits: usize,
23    pub total_pre_failure_hits: usize,
24    pub pass_run_hits: usize,
25    pub precision_pre_failure: f64,
26    pub precision_pass: f64,
27}
28
29#[derive(Debug, Clone, Serialize)]
30pub struct RecurrentBoundaryTradeoffRow {
31    pub suppression_level: f64,
32    pub suppression_label: String,
33    pub investigation_points: usize,
34    pub pass_run_nuisance_proxy: f64,
35    pub delta_nuisance_vs_selected_dsa: f64,
36    pub delta_nuisance_vs_ewma: f64,
37    pub failure_recall: usize,
38    pub failure_runs: usize,
39    pub recall_rate: f64,
40}
41
42#[derive(Debug, Clone, Serialize)]
43pub struct MetricRegroundingRow {
44    pub metric: String,
45    pub baseline: String,
46    pub dsfb_value: f64,
47    pub baseline_value: f64,
48    pub delta_percent: f64,
49}
50
51#[derive(Debug, Clone, Serialize)]
52pub struct TargetDRegressionAnalysis {
53    pub contributing_features: Vec<String>,
54    pub contributing_motifs: Vec<String>,
55    pub contributing_heuristics: Vec<String>,
56    pub contributing_policy_rules: Vec<String>,
57    pub causal_chain: Vec<String>,
58    pub why_regression_occurred: String,
59    pub action_taken: String,
60    pub tradeoff_justification: String,
61}
62
63#[derive(Debug, Clone, Serialize)]
64pub struct MissedFailureFeatureActivity {
65    pub feature_name: String,
66    pub behavior_classification: String,
67    pub max_dsa_score: f64,
68    pub initial_motif_hypothesis: String,
69    pub dominant_dsfb_motif: String,
70    pub dominant_grammar_state: String,
71    pub failure_explanation: String,
72}
73
74#[derive(Debug, Clone, Serialize)]
75pub struct MissedFailureRootCause {
76    pub failure_id: usize,
77    pub feature_activity: Vec<MissedFailureFeatureActivity>,
78    pub residual_trajectory: Vec<f64>,
79    pub drift_trajectory: Vec<f64>,
80    pub slew_trajectory: Vec<f64>,
81    pub motif_presence: Vec<String>,
82    pub grammar_state: Vec<String>,
83    pub reason_for_miss: String,
84    pub classification: String,
85    pub recovered_after_fix: bool,
86    pub recovery_feature: Option<String>,
87    pub recovery_note: String,
88}
89
90#[derive(Debug, Clone, Serialize)]
91pub struct LeadTimeComparisonRow {
92    pub failure_id: usize,
93    pub dsfb_lead_runs: Option<usize>,
94    pub threshold_lead_runs: Option<usize>,
95    pub earliest_semantic_match_lead_runs: Option<usize>,
96    pub threshold_minus_dsfb_runs: Option<i64>,
97    pub threshold_minus_semantic_match_runs: Option<i64>,
98}
99
100#[derive(Debug, Clone, Serialize)]
101pub struct LeadTimeExplanation {
102    pub mean_dsfb_lead_runs: Option<f64>,
103    pub mean_threshold_lead_runs: Option<f64>,
104    pub mean_semantic_match_lead_runs: Option<f64>,
105    pub threshold_earlier_failure_count: usize,
106    pub dsfb_earlier_failure_count: usize,
107    pub semantic_match_precedes_threshold_count: usize,
108    pub motif_emergence_precedes_threshold_count: usize,
109    pub explanation: String,
110    pub validation_note: String,
111}
112
113#[derive(Debug, Clone, Serialize)]
114pub struct EpisodePrecisionMetrics {
115    pub dsfb_episode_count: usize,
116    pub dsfb_pre_failure_episode_count: usize,
117    pub dsfb_precision: f64,
118    pub raw_alarm_count: usize,
119    pub raw_alarm_precision: f64,
120    pub precision_gain_factor: f64,
121}
122
123#[derive(Debug, Clone, Serialize)]
124pub struct SecomAddendumArtifacts {
125    pub recurrent_boundary_stats: RecurrentBoundaryStats,
126    pub recurrent_boundary_tradeoff_curve: Vec<RecurrentBoundaryTradeoffRow>,
127    pub required_tradeoff_statement: String,
128    pub required_tradeoff_statement_supported: bool,
129    pub metric_regrounding: Vec<MetricRegroundingRow>,
130    pub target_d_regression_analysis: TargetDRegressionAnalysis,
131    pub missed_failure_root_cause: MissedFailureRootCause,
132    pub lead_time_comparison: Vec<LeadTimeComparisonRow>,
133    pub lead_time_explanation: LeadTimeExplanation,
134    pub episode_precision_metrics: EpisodePrecisionMetrics,
135    pub executive_summary_text: String,
136    pub paper_abstract_artifact: String,
137}
138
139pub fn build_secom_addendum_artifacts(
140    dataset: &PreparedDataset,
141    residuals: &ResidualSet,
142    baselines: &BaselineSet,
143    grammar: &GrammarSet,
144    motifs: &MotifSet,
145    semantic_layer: &SemanticLayer,
146    metrics: &BenchmarkMetrics,
147    optimization: &OptimizationExecution,
148    failure_driven: &FailureDrivenArtifacts,
149    baseline_dsa: &DsaEvaluation,
150    optimized_dsa: &DsaEvaluation,
151    pre_failure_lookback_runs: usize,
152) -> SecomAddendumArtifacts {
153    let recurrent_boundary_stats =
154        build_recurrent_boundary_stats(dataset, motifs, pre_failure_lookback_runs);
155    let recurrent_boundary_tradeoff_curve = build_recurrent_boundary_tradeoff_curve(
156        dataset,
157        grammar,
158        motifs,
159        metrics,
160        optimized_dsa,
161        pre_failure_lookback_runs,
162    );
163    let required_tradeoff_statement = "Suppressing recurrent_boundary_approach sufficiently to achieve ≥40% nuisance reduction on SECOM necessarily reduces recall due to shared structural origin.".to_string();
164    let required_tradeoff_statement_supported =
165        recurrent_boundary_tradeoff_statement_supported(&recurrent_boundary_tradeoff_curve);
166    let metric_regrounding = build_metric_regrounding(
167        dataset,
168        residuals,
169        baselines,
170        metrics,
171        optimized_dsa,
172        pre_failure_lookback_runs,
173    );
174    let target_d_regression_analysis = build_target_d_regression_analysis(
175        dataset,
176        grammar,
177        motifs,
178        failure_driven,
179        baseline_dsa,
180        optimized_dsa,
181        &recurrent_boundary_tradeoff_curve,
182    );
183    let missed_failure_root_cause = build_missed_failure_root_cause(failure_driven);
184    let lead_time_comparison = build_lead_time_comparison(
185        semantic_layer,
186        optimized_dsa,
187        metrics,
188        pre_failure_lookback_runs,
189    );
190    let lead_time_explanation = build_lead_time_explanation(&lead_time_comparison);
191    let episode_precision_metrics = build_episode_precision_metrics(metrics, optimized_dsa);
192    let executive_summary_text = format!(
193        "Primary operator result: DSA episode precision is {:.1}% ({}/{} episodes preceding labeled failures) versus a raw-boundary precision proxy of {:.2}%, a {:.1}x gain. Investigation-worthy burden falls by {:.1}% versus numeric-only DSA ({} -> {} points), raw boundary episodes collapse by {:.1}% ({} -> {}), and recall reaches {}/{} while pass-run nuisance reduction versus EWMA remains bounded at {:.1}%.",
194        episode_precision_metrics.dsfb_precision * 100.0,
195        episode_precision_metrics.dsfb_pre_failure_episode_count,
196        episode_precision_metrics.dsfb_episode_count,
197        episode_precision_metrics.raw_alarm_precision * 100.0,
198        episode_precision_metrics.precision_gain_factor,
199        optimization.operator_delta_targets.delta_investigation_load * 100.0,
200        optimization.operator_delta_targets.baseline_investigation_points,
201        optimization.operator_delta_targets.optimized_review_escalate_points,
202        optimization.operator_delta_targets.delta_episode_count * 100.0,
203        optimization.operator_delta_targets.baseline_episode_count,
204        optimization.operator_delta_targets.optimized_episode_count,
205        optimization.operator_delta_targets.selected_configuration.failure_recall,
206        optimization.operator_delta_targets.selected_configuration.failure_runs,
207        optimization.operator_delta_targets.delta_nuisance_vs_ewma * 100.0,
208    );
209    let paper_abstract_artifact = format!(
210        "On SECOM, the policy-governed DSFB layer compresses raw structural activity from {} boundary episodes to {} DSA episodes, reduces investigation-worthy feature points from {} numeric-only DSA points to {}, and raises episode precision to {:.1}% from a raw-boundary precision proxy of {:.2}%. This supports a bounded structural-compression claim, not a blanket early-warning or nuisance-superiority claim.",
211        optimization.operator_delta_targets.baseline_episode_count,
212        optimization.operator_delta_targets.optimized_episode_count,
213        optimization.operator_delta_targets.baseline_investigation_points,
214        optimization.operator_delta_targets.optimized_review_escalate_points,
215        episode_precision_metrics.dsfb_precision * 100.0,
216        episode_precision_metrics.raw_alarm_precision * 100.0,
217    );
218
219    SecomAddendumArtifacts {
220        recurrent_boundary_stats,
221        recurrent_boundary_tradeoff_curve,
222        required_tradeoff_statement,
223        required_tradeoff_statement_supported,
224        metric_regrounding,
225        target_d_regression_analysis,
226        missed_failure_root_cause,
227        lead_time_comparison,
228        lead_time_explanation,
229        episode_precision_metrics,
230        executive_summary_text,
231        paper_abstract_artifact,
232    }
233}
234
235pub fn draw_recurrent_boundary_tradeoff_plot(
236    path: &Path,
237    rows: &[RecurrentBoundaryTradeoffRow],
238) -> Result<()> {
239    let root =
240        BitMapBackend::new(path, (TRADEOFF_PLOT_WIDTH, TRADEOFF_PLOT_HEIGHT)).into_drawing_area();
241    root.fill(&WHITE).map_err(plot_error)?;
242
243    let mut chart = ChartBuilder::on(&root)
244        .caption(
245            "SECOM recurrent_boundary_approach suppression tradeoff",
246            ("sans-serif", 28),
247        )
248        .margin(20)
249        .x_label_area_size(60)
250        .y_label_area_size(70)
251        .build_cartesian_2d(0usize..rows.len(), 0.0f64..1.05f64)
252        .map_err(plot_error)?;
253
254    chart
255        .configure_mesh()
256        .disable_mesh()
257        .x_labels(rows.len())
258        .x_label_formatter(&|index| {
259            rows.get(*index)
260                .map(|row| row.suppression_label.clone())
261                .unwrap_or_default()
262        })
263        .y_desc("Recall rate / nuisance reduction")
264        .draw()
265        .map_err(plot_error)?;
266
267    let recall_series = rows
268        .iter()
269        .enumerate()
270        .map(|(index, row)| (index, row.recall_rate))
271        .collect::<Vec<_>>();
272    let nuisance_series = rows
273        .iter()
274        .enumerate()
275        .map(|(index, row)| (index, row.delta_nuisance_vs_ewma))
276        .collect::<Vec<_>>();
277
278    chart
279        .draw_series(LineSeries::new(recall_series.clone(), &BLACK))
280        .map_err(plot_error)?;
281    chart
282        .draw_series(
283            recall_series
284                .into_iter()
285                .map(|point| Circle::new(point, 5, BLACK.filled())),
286        )
287        .map_err(plot_error)?;
288
289    chart
290        .draw_series(LineSeries::new(
291            nuisance_series.clone(),
292            &RGBColor(100, 100, 100),
293        ))
294        .map_err(plot_error)?;
295    chart
296        .draw_series(
297            nuisance_series
298                .into_iter()
299                .map(|point| TriangleMarker::new(point, 6, RGBColor(100, 100, 100).filled())),
300        )
301        .map_err(plot_error)?;
302
303    chart
304        .draw_series(std::iter::once(PathElement::new(
305            vec![(0usize, 0.40f64), (rows.len().saturating_sub(1), 0.40f64)],
306            ShapeStyle::from(&RGBColor(170, 170, 170)).stroke_width(1),
307        )))
308        .map_err(plot_error)?;
309
310    root.present().map_err(plot_error)?;
311    Ok(())
312}
313
314fn build_recurrent_boundary_stats(
315    dataset: &PreparedDataset,
316    motifs: &MotifSet,
317    pre_failure_lookback_runs: usize,
318) -> RecurrentBoundaryStats {
319    let failure_mask = failure_window_mask(
320        dataset.labels.len(),
321        &dataset.labels,
322        pre_failure_lookback_runs,
323    );
324    let total_boundary_points = motifs
325        .traces
326        .iter()
327        .flat_map(|trace| trace.labels.iter())
328        .filter(|label| **label == DsfbMotifClass::RecurrentBoundaryApproach)
329        .count();
330    let mut run_hits = BTreeSet::new();
331    let mut pre_failure_hits = BTreeSet::new();
332    let mut pass_run_hits = BTreeSet::new();
333
334    for trace in &motifs.traces {
335        for (run_index, label) in trace.labels.iter().enumerate() {
336            if *label != DsfbMotifClass::RecurrentBoundaryApproach {
337                continue;
338            }
339            run_hits.insert(run_index);
340            if failure_mask[run_index] {
341                pre_failure_hits.insert(run_index);
342            }
343            if dataset.labels[run_index] == -1 {
344                pass_run_hits.insert(run_index);
345            }
346        }
347    }
348
349    let total_run_hits = run_hits.len();
350    let total_pre_failure_hits = pre_failure_hits.len();
351    let pass_run_hits_count = pass_run_hits.len();
352
353    RecurrentBoundaryStats {
354        total_boundary_points,
355        total_run_hits,
356        total_pre_failure_hits,
357        pass_run_hits: pass_run_hits_count,
358        precision_pre_failure: ratio(total_pre_failure_hits, total_run_hits),
359        precision_pass: ratio(pass_run_hits_count, total_run_hits),
360    }
361}
362
363fn build_recurrent_boundary_tradeoff_curve(
364    dataset: &PreparedDataset,
365    grammar: &GrammarSet,
366    motifs: &MotifSet,
367    metrics: &BenchmarkMetrics,
368    optimized_dsa: &DsaEvaluation,
369    pre_failure_lookback_runs: usize,
370) -> Vec<RecurrentBoundaryTradeoffRow> {
371    let motif_by_feature = motifs
372        .traces
373        .iter()
374        .map(|trace| (trace.feature_index, trace))
375        .collect::<BTreeMap<_, _>>();
376    let grammar_by_feature = grammar
377        .traces
378        .iter()
379        .map(|trace| (trace.feature_index, trace))
380        .collect::<BTreeMap<_, _>>();
381    let pass_indices = dataset
382        .labels
383        .iter()
384        .enumerate()
385        .filter_map(|(index, label)| (*label == -1).then_some(index))
386        .collect::<Vec<_>>();
387    let failure_indices = dataset
388        .labels
389        .iter()
390        .enumerate()
391        .filter_map(|(index, label)| (*label == 1).then_some(index))
392        .collect::<Vec<_>>();
393
394    let suppression_levels = [
395        (0.0, "baseline"),
396        (0.25, "mild_gate"),
397        (0.50, "watch_cap"),
398        (0.75, "strong_cap"),
399        (1.00, "silent_cap"),
400    ];
401
402    suppression_levels
403        .into_iter()
404        .map(|(level, label)| {
405            let mut feature_alerts =
406                vec![vec![false; dataset.labels.len()]; optimized_dsa.traces.len()];
407            let mut investigation_points = 0usize;
408
409            for (trace_slot, feature_trace) in optimized_dsa.traces.iter().enumerate() {
410                let motif_trace = motif_by_feature
411                    .get(&feature_trace.feature_index)
412                    .unwrap_or_else(|| {
413                        panic!("missing motif trace {}", feature_trace.feature_index)
414                    });
415                let grammar_trace = grammar_by_feature
416                    .get(&feature_trace.feature_index)
417                    .unwrap_or_else(|| {
418                        panic!("missing grammar trace {}", feature_trace.feature_index)
419                    });
420                for run_index in 0..feature_trace.policy_state.len() {
421                    let remapped = remap_recurrent_boundary_state(
422                        level,
423                        feature_trace.policy_state[run_index],
424                        motif_trace.labels[run_index],
425                        grammar_trace.raw_states[run_index],
426                        grammar_trace.persistent_violation[run_index],
427                        feature_trace.boundary_density_w[run_index],
428                        feature_trace.motif_recurrence_w[run_index],
429                    );
430                    if is_review_or_escalate(remapped) {
431                        feature_alerts[trace_slot][run_index] = true;
432                        investigation_points += 1;
433                    }
434                }
435            }
436
437            let corroborating_m = optimized_dsa.run_signals.corroborating_feature_count_min;
438            let run_alert = (0..dataset.labels.len())
439                .map(|run_index| {
440                    feature_alerts
441                        .iter()
442                        .filter(|trace| trace[run_index])
443                        .count()
444                        >= corroborating_m
445                })
446                .collect::<Vec<_>>();
447
448            let failure_recall = failure_indices
449                .iter()
450                .filter(|&&failure_index| {
451                    let start = failure_index.saturating_sub(pre_failure_lookback_runs);
452                    run_alert[start..failure_index].iter().any(|flag| *flag)
453                })
454                .count();
455            let pass_alert_runs = pass_indices
456                .iter()
457                .filter(|&&run_index| run_alert[run_index])
458                .count();
459
460            RecurrentBoundaryTradeoffRow {
461                suppression_level: level,
462                suppression_label: label.into(),
463                investigation_points,
464                pass_run_nuisance_proxy: ratio(pass_alert_runs, pass_indices.len()),
465                delta_nuisance_vs_selected_dsa: relative_reduction(
466                    optimized_dsa.summary.pass_run_nuisance_proxy,
467                    ratio(pass_alert_runs, pass_indices.len()),
468                ),
469                delta_nuisance_vs_ewma: relative_reduction(
470                    metrics.summary.pass_run_ewma_nuisance_rate,
471                    ratio(pass_alert_runs, pass_indices.len()),
472                ),
473                failure_recall,
474                failure_runs: failure_indices.len(),
475                recall_rate: ratio(failure_recall, failure_indices.len()),
476            }
477        })
478        .collect()
479}
480
481fn recurrent_boundary_tradeoff_statement_supported(rows: &[RecurrentBoundaryTradeoffRow]) -> bool {
482    rows.iter().any(|row| row.delta_nuisance_vs_ewma >= 0.40)
483        && rows
484            .iter()
485            .filter(|row| row.delta_nuisance_vs_ewma >= 0.40)
486            .all(|row| row.failure_recall < row.failure_runs)
487}
488
489fn remap_recurrent_boundary_state(
490    suppression_level: f64,
491    state: DsaPolicyState,
492    motif: DsfbMotifClass,
493    grammar_state: GrammarState,
494    persistent_violation: bool,
495    boundary_density: f64,
496    motif_recurrence: f64,
497) -> DsaPolicyState {
498    if motif != DsfbMotifClass::RecurrentBoundaryApproach {
499        return state;
500    }
501
502    if suppression_level >= 1.0 {
503        return DsaPolicyState::Silent;
504    }
505    if suppression_level >= 0.75 {
506        if persistent_violation || grammar_state == GrammarState::Violation {
507            return DsaPolicyState::Watch;
508        }
509        return DsaPolicyState::Silent;
510    }
511    if suppression_level >= 0.50 {
512        if is_review_or_escalate(state) {
513            return DsaPolicyState::Watch;
514        }
515        return state;
516    }
517    if suppression_level >= 0.25
518        && is_review_or_escalate(state)
519        && (boundary_density < 0.75 || motif_recurrence < 0.75)
520    {
521        return DsaPolicyState::Watch;
522    }
523
524    state
525}
526
527fn build_metric_regrounding(
528    dataset: &PreparedDataset,
529    residuals: &ResidualSet,
530    baselines: &BaselineSet,
531    metrics: &BenchmarkMetrics,
532    optimized_dsa: &DsaEvaluation,
533    pre_failure_lookback_runs: usize,
534) -> Vec<MetricRegroundingRow> {
535    let threshold_signal = (0..dataset.labels.len())
536        .map(|run_index| {
537            residuals
538                .traces
539                .iter()
540                .any(|trace| trace.threshold_alarm[run_index])
541        })
542        .collect::<Vec<_>>();
543    let ewma_signal = (0..dataset.labels.len())
544        .map(|run_index| baselines.ewma.iter().any(|trace| trace.alarm[run_index]))
545        .collect::<Vec<_>>();
546
547    let threshold_episode_count = count_episodes(&threshold_signal);
548    let threshold_precision = episode_precision(
549        &threshold_signal,
550        &failure_window_mask(
551            dataset.labels.len(),
552            &dataset.labels,
553            pre_failure_lookback_runs,
554        ),
555    );
556
557    let ewma_episode_count = count_episodes(&ewma_signal);
558    let ewma_precision = episode_precision(
559        &ewma_signal,
560        &failure_window_mask(
561            dataset.labels.len(),
562            &dataset.labels,
563            pre_failure_lookback_runs,
564        ),
565    );
566
567    let failure_mask = failure_window_mask(
568        dataset.labels.len(),
569        &dataset.labels,
570        pre_failure_lookback_runs,
571    );
572    let numeric_dsa_episode_count =
573        count_episodes(&optimized_dsa.run_signals.numeric_primary_run_alert);
574    let dsfb_precision = optimized_dsa
575        .episode_summary
576        .precursor_quality
577        .unwrap_or_default();
578    let numeric_precision = episode_precision(
579        &optimized_dsa.run_signals.numeric_primary_run_alert,
580        &failure_mask,
581    );
582
583    vec![
584        MetricRegroundingRow {
585            metric: "Investigation load".into(),
586            baseline: "Numeric-only DSA".into(),
587            dsfb_value: investigation_points(optimized_dsa) as f64,
588            baseline_value: optimized_dsa.summary.numeric_alert_point_count as f64,
589            delta_percent: relative_reduction(
590                optimized_dsa.summary.numeric_alert_point_count as f64,
591                investigation_points(optimized_dsa) as f64,
592            ),
593        },
594        MetricRegroundingRow {
595            metric: "Investigation load".into(),
596            baseline: "Threshold".into(),
597            dsfb_value: investigation_points(optimized_dsa) as f64,
598            baseline_value: metrics.summary.threshold_alarm_points as f64,
599            delta_percent: relative_reduction(
600                metrics.summary.threshold_alarm_points as f64,
601                investigation_points(optimized_dsa) as f64,
602            ),
603        },
604        MetricRegroundingRow {
605            metric: "Investigation load".into(),
606            baseline: "EWMA".into(),
607            dsfb_value: investigation_points(optimized_dsa) as f64,
608            baseline_value: metrics.summary.ewma_alarm_points as f64,
609            delta_percent: relative_reduction(
610                metrics.summary.ewma_alarm_points as f64,
611                investigation_points(optimized_dsa) as f64,
612            ),
613        },
614        MetricRegroundingRow {
615            metric: "Episode count".into(),
616            baseline: "Numeric-only DSA".into(),
617            dsfb_value: optimized_dsa.episode_summary.dsa_episode_count as f64,
618            baseline_value: numeric_dsa_episode_count as f64,
619            delta_percent: relative_reduction(
620                numeric_dsa_episode_count as f64,
621                optimized_dsa.episode_summary.dsa_episode_count as f64,
622            ),
623        },
624        MetricRegroundingRow {
625            metric: "Episode count".into(),
626            baseline: "Threshold".into(),
627            dsfb_value: optimized_dsa.episode_summary.dsa_episode_count as f64,
628            baseline_value: threshold_episode_count as f64,
629            delta_percent: relative_reduction(
630                threshold_episode_count as f64,
631                optimized_dsa.episode_summary.dsa_episode_count as f64,
632            ),
633        },
634        MetricRegroundingRow {
635            metric: "Episode count".into(),
636            baseline: "EWMA".into(),
637            dsfb_value: optimized_dsa.episode_summary.dsa_episode_count as f64,
638            baseline_value: ewma_episode_count as f64,
639            delta_percent: relative_reduction(
640                ewma_episode_count as f64,
641                optimized_dsa.episode_summary.dsa_episode_count as f64,
642            ),
643        },
644        MetricRegroundingRow {
645            metric: "Nuisance reduction".into(),
646            baseline: "Numeric-only DSA".into(),
647            dsfb_value: optimized_dsa.summary.pass_run_nuisance_proxy,
648            baseline_value: optimized_dsa
649                .summary
650                .numeric_primary_pass_run_nuisance_proxy,
651            delta_percent: relative_reduction(
652                optimized_dsa
653                    .summary
654                    .numeric_primary_pass_run_nuisance_proxy,
655                optimized_dsa.summary.pass_run_nuisance_proxy,
656            ),
657        },
658        MetricRegroundingRow {
659            metric: "Nuisance reduction".into(),
660            baseline: "Threshold".into(),
661            dsfb_value: optimized_dsa.summary.pass_run_nuisance_proxy,
662            baseline_value: metrics.summary.pass_run_threshold_nuisance_rate,
663            delta_percent: relative_reduction(
664                metrics.summary.pass_run_threshold_nuisance_rate,
665                optimized_dsa.summary.pass_run_nuisance_proxy,
666            ),
667        },
668        MetricRegroundingRow {
669            metric: "Nuisance reduction".into(),
670            baseline: "EWMA".into(),
671            dsfb_value: optimized_dsa.summary.pass_run_nuisance_proxy,
672            baseline_value: metrics.summary.pass_run_ewma_nuisance_rate,
673            delta_percent: relative_reduction(
674                metrics.summary.pass_run_ewma_nuisance_rate,
675                optimized_dsa.summary.pass_run_nuisance_proxy,
676            ),
677        },
678        MetricRegroundingRow {
679            metric: "Recall".into(),
680            baseline: "Numeric-only DSA".into(),
681            dsfb_value: optimized_dsa.summary.failure_run_recall as f64,
682            baseline_value: optimized_dsa.summary.numeric_primary_failure_run_recall as f64,
683            delta_percent: relative_gain(
684                optimized_dsa.summary.numeric_primary_failure_run_recall as f64,
685                optimized_dsa.summary.failure_run_recall as f64,
686            ),
687        },
688        MetricRegroundingRow {
689            metric: "Recall".into(),
690            baseline: "Threshold".into(),
691            dsfb_value: optimized_dsa.summary.failure_run_recall as f64,
692            baseline_value: metrics.summary.failure_runs_with_preceding_threshold_signal as f64,
693            delta_percent: relative_gain(
694                metrics.summary.failure_runs_with_preceding_threshold_signal as f64,
695                optimized_dsa.summary.failure_run_recall as f64,
696            ),
697        },
698        MetricRegroundingRow {
699            metric: "Recall".into(),
700            baseline: "EWMA".into(),
701            dsfb_value: optimized_dsa.summary.failure_run_recall as f64,
702            baseline_value: metrics.summary.failure_runs_with_preceding_ewma_signal as f64,
703            delta_percent: relative_gain(
704                metrics.summary.failure_runs_with_preceding_ewma_signal as f64,
705                optimized_dsa.summary.failure_run_recall as f64,
706            ),
707        },
708        MetricRegroundingRow {
709            metric: "Lead time".into(),
710            baseline: "Numeric-only DSA".into(),
711            dsfb_value: optimized_dsa
712                .summary
713                .mean_lead_time_runs
714                .unwrap_or_default(),
715            baseline_value: optimized_dsa
716                .comparison_summary
717                .numeric_dsa
718                .mean_lead_time_runs
719                .unwrap_or_default(),
720            delta_percent: relative_gain(
721                optimized_dsa
722                    .comparison_summary
723                    .numeric_dsa
724                    .mean_lead_time_runs
725                    .unwrap_or_default(),
726                optimized_dsa
727                    .summary
728                    .mean_lead_time_runs
729                    .unwrap_or_default(),
730            ),
731        },
732        MetricRegroundingRow {
733            metric: "Lead time".into(),
734            baseline: "Threshold".into(),
735            dsfb_value: optimized_dsa
736                .summary
737                .mean_lead_time_runs
738                .unwrap_or_default(),
739            baseline_value: metrics
740                .lead_time_summary
741                .mean_threshold_lead_runs
742                .unwrap_or_default(),
743            delta_percent: relative_gain(
744                metrics
745                    .lead_time_summary
746                    .mean_threshold_lead_runs
747                    .unwrap_or_default(),
748                optimized_dsa
749                    .summary
750                    .mean_lead_time_runs
751                    .unwrap_or_default(),
752            ),
753        },
754        MetricRegroundingRow {
755            metric: "Lead time".into(),
756            baseline: "EWMA".into(),
757            dsfb_value: optimized_dsa
758                .summary
759                .mean_lead_time_runs
760                .unwrap_or_default(),
761            baseline_value: metrics
762                .lead_time_summary
763                .mean_ewma_lead_runs
764                .unwrap_or_default(),
765            delta_percent: relative_gain(
766                metrics
767                    .lead_time_summary
768                    .mean_ewma_lead_runs
769                    .unwrap_or_default(),
770                optimized_dsa
771                    .summary
772                    .mean_lead_time_runs
773                    .unwrap_or_default(),
774            ),
775        },
776        MetricRegroundingRow {
777            metric: "Episode precision".into(),
778            baseline: "Numeric-only DSA".into(),
779            dsfb_value: dsfb_precision,
780            baseline_value: numeric_precision,
781            delta_percent: relative_gain(numeric_precision, dsfb_precision),
782        },
783        MetricRegroundingRow {
784            metric: "Episode precision".into(),
785            baseline: "Threshold".into(),
786            dsfb_value: dsfb_precision,
787            baseline_value: threshold_precision,
788            delta_percent: relative_gain(threshold_precision, dsfb_precision),
789        },
790        MetricRegroundingRow {
791            metric: "Episode precision".into(),
792            baseline: "EWMA".into(),
793            dsfb_value: dsfb_precision,
794            baseline_value: ewma_precision,
795            delta_percent: relative_gain(ewma_precision, dsfb_precision),
796        },
797    ]
798}
799
800fn build_target_d_regression_analysis(
801    dataset: &PreparedDataset,
802    grammar: &GrammarSet,
803    motifs: &MotifSet,
804    failure_driven: &FailureDrivenArtifacts,
805    baseline_dsa: &DsaEvaluation,
806    optimized_dsa: &DsaEvaluation,
807    tradeoff_curve: &[RecurrentBoundaryTradeoffRow],
808) -> TargetDRegressionAnalysis {
809    let pass_indices = dataset
810        .labels
811        .iter()
812        .enumerate()
813        .filter_map(|(index, label)| (*label == -1).then_some(index))
814        .collect::<Vec<_>>();
815    let motif_by_feature = motifs
816        .traces
817        .iter()
818        .map(|trace| (trace.feature_index, trace))
819        .collect::<BTreeMap<_, _>>();
820    let grammar_by_feature = grammar
821        .traces
822        .iter()
823        .map(|trace| (trace.feature_index, trace))
824        .collect::<BTreeMap<_, _>>();
825
826    let mut added_pass_points_by_feature = BTreeMap::<String, usize>::new();
827    let mut motif_counts = BTreeMap::<String, usize>::new();
828    let mut grammar_counts = BTreeMap::<String, usize>::new();
829
830    for (baseline_trace, optimized_trace) in baseline_dsa.traces.iter().zip(&optimized_dsa.traces) {
831        let motif_trace = match motif_by_feature.get(&optimized_trace.feature_index) {
832            Some(trace) => *trace,
833            None => continue,
834        };
835        let grammar_trace = match grammar_by_feature.get(&optimized_trace.feature_index) {
836            Some(trace) => *trace,
837            None => continue,
838        };
839        let mut added = 0usize;
840        for &run_index in &pass_indices {
841            let baseline_flag = is_review_or_escalate(baseline_trace.policy_state[run_index]);
842            let optimized_flag = is_review_or_escalate(optimized_trace.policy_state[run_index]);
843            if optimized_flag && !baseline_flag {
844                added += 1;
845                *motif_counts
846                    .entry(motif_trace.labels[run_index].as_lowercase().to_string())
847                    .or_default() += 1;
848                *grammar_counts
849                    .entry(grammar_label(grammar_trace.raw_states[run_index]).to_string())
850                    .or_default() += 1;
851            }
852        }
853        if added > 0 {
854            added_pass_points_by_feature.insert(optimized_trace.feature_name.clone(), added);
855        }
856    }
857
858    let mut contributing_features_counts =
859        added_pass_points_by_feature.into_iter().collect::<Vec<_>>();
860    contributing_features_counts
861        .sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
862    let contributing_features = contributing_features_counts
863        .into_iter()
864        .take(5)
865        .map(|(feature, _)| feature)
866        .collect::<Vec<_>>();
867
868    let mut contributing_motifs_counts = motif_counts.into_iter().collect::<Vec<_>>();
869    contributing_motifs_counts
870        .sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
871    let contributing_motifs = contributing_motifs_counts
872        .into_iter()
873        .take(3)
874        .map(|(motif, _)| motif)
875        .collect::<Vec<_>>();
876
877    let contributing_features_text = if contributing_features.is_empty() {
878        "no single dominant feature".into()
879    } else {
880        contributing_features.join(", ")
881    };
882    let contributing_motifs_text = if contributing_motifs.is_empty() {
883        "none".into()
884    } else {
885        contributing_motifs.join(", ")
886    };
887    let grammar_counts_text = if grammar_counts.is_empty() {
888        "none".into()
889    } else {
890        grammar_counts
891            .clone()
892            .into_iter()
893            .take(3)
894            .map(|(name, _)| name)
895            .collect::<Vec<_>>()
896            .join(", ")
897    };
898
899    let mut contributing_heuristics = failure_driven
900        .heuristic_provenance
901        .iter()
902        .filter(|row| {
903            row.intended_effect == "recover_failure"
904                && contributing_features
905                    .iter()
906                    .any(|feature| row.uses_features.contains(feature))
907        })
908        .map(|row| row.heuristic_id.clone())
909        .collect::<Vec<_>>();
910    contributing_heuristics.sort();
911    contributing_heuristics.dedup();
912
913    let mut contributing_policy_rules = Vec::new();
914    if contributing_features
915        .iter()
916        .any(|feature| matches!(feature.as_str(), "S092" | "S134" | "S275"))
917    {
918        contributing_policy_rules.push(
919            "bounded recall-rescue override: allow_review_without_escalate=true with persistence and corroboration relaxed on feature-local recovery paths"
920                .into(),
921        );
922    }
923    if optimized_dsa
924        .parameter_manifest
925        .feature_policy_override_summary
926        .iter()
927        .any(|summary| summary.contains("suppress_if_isolated=true"))
928    {
929        contributing_policy_rules.push(
930            "isolated-pass suppression rule: allow_watch_only=true plus suppress_if_isolated=true on nuisance-only pass features"
931                .into(),
932        );
933    }
934    if contributing_motifs
935        .iter()
936        .any(|motif| motif == "recurrent_boundary_approach")
937    {
938        contributing_policy_rules.push(
939            "recurrent_boundary_approach remains persistence-gated and globally capped below strong suppression because stronger nuisance cuts give back recall"
940                .into(),
941        );
942    }
943
944    let strong_suppression_rows = tradeoff_curve
945        .iter()
946        .filter(|row| row.delta_nuisance_vs_ewma >= 0.40)
947        .collect::<Vec<_>>();
948    let tradeoff_justification = if strong_suppression_rows.is_empty() {
949        "No recurrent_boundary_approach suppression row reached a 40% nuisance reduction versus EWMA, so the current regression is carried as a bounded operator tradeoff rather than falsely claimed as solved.".into()
950    } else {
951        let recall_rows = strong_suppression_rows
952            .iter()
953            .map(|row| {
954                format!(
955                    "{} => recall {}/{}",
956                    row.suppression_label, row.failure_recall, row.failure_runs
957                )
958            })
959            .collect::<Vec<_>>()
960            .join("; ");
961        format!(
962            "Further suppression was rejected in this pass. Every recurrent_boundary_approach suppression row that reaches at least a 40% nuisance reduction versus EWMA drops recall below full coverage: {}.",
963            recall_rows
964        )
965    };
966
967    TargetDRegressionAnalysis {
968        contributing_features,
969        contributing_motifs,
970        contributing_heuristics,
971        contributing_policy_rules,
972        causal_chain: vec![
973            "Failure-local rescue overrides for the former misses promote grammar-qualified Watch states to Review on specific low-burden features.".into(),
974            "Those promotions create a small number of additional pass-only run segments, which increases review episodes per pass run even while total investigation points still fall materially.".into(),
975            "The accepted isolated-pass suppressions reduce point burden, but they do not fully remove the extra run segmentation introduced by recall recovery.".into(),
976            "The recurrent_boundary_approach suppression sweep shows that stronger motif-wide suppression would cut nuisance further only by giving back recall.".into(),
977        ],
978        why_regression_occurred: format!(
979            "The regression is driven by recall-recovery promotions on {} under motif classes {} and grammar states {}. The specific policy change is bounded Watch->Review rescue without escalation on these features, while the compensating isolated-pass suppression rules only partially offset the extra pass-only segmentation.",
980            contributing_features_text,
981            contributing_motifs_text,
982            grammar_counts_text,
983        ),
984        action_taken: "formal_tradeoff_justification".into(),
985        tradeoff_justification,
986    }
987}
988
989fn build_missed_failure_root_cause(
990    failure_driven: &FailureDrivenArtifacts,
991) -> MissedFailureRootCause {
992    let case = failure_driven
993        .failure_cases
994        .iter()
995        .find(|case| case.failure_id == 2)
996        .or_else(|| {
997            failure_driven
998                .failure_cases
999                .iter()
1000                .find(|case| !case.baseline_detected_by_dsa && case.optimized_detected_by_dsa)
1001        })
1002        .or_else(|| failure_driven.failure_cases.first())
1003        .unwrap_or_else(|| panic!("failure-driven artifacts missing baseline failure cases"));
1004    let top_feature = case
1005        .top_contributing_features
1006        .first()
1007        .unwrap_or_else(|| panic!("failure case {} missing top feature", case.failure_id));
1008    let reason_for_miss = missed_failure_reason(case.exact_miss_rule.as_str()).to_string();
1009    let classification = if case.optimized_detected_by_dsa {
1010        "recoverable (and fix)"
1011    } else if top_feature.max_dsa_score <= 0.0 {
1012        "data limitation"
1013    } else {
1014        "structurally unrecoverable"
1015    };
1016
1017    MissedFailureRootCause {
1018        failure_id: case.failure_id,
1019        feature_activity: case
1020            .top_contributing_features
1021            .iter()
1022            .map(|feature| MissedFailureFeatureActivity {
1023                feature_name: feature.feature_name.clone(),
1024                behavior_classification: feature.behavior_classification.clone(),
1025                max_dsa_score: feature.max_dsa_score,
1026                initial_motif_hypothesis: feature.initial_motif_hypothesis.clone(),
1027                dominant_dsfb_motif: feature.dominant_dsfb_motif.clone(),
1028                dominant_grammar_state: feature.dominant_grammar_state.clone(),
1029                failure_explanation: feature.failure_explanation.clone(),
1030            })
1031            .collect(),
1032        residual_trajectory: top_feature.residual_trajectory.clone(),
1033        drift_trajectory: top_feature.drift_trajectory.clone(),
1034        slew_trajectory: top_feature.slew_trajectory.clone(),
1035        motif_presence: top_feature.motif_timeline.clone(),
1036        grammar_state: top_feature.grammar_state_timeline.clone(),
1037        reason_for_miss,
1038        classification: classification.into(),
1039        recovered_after_fix: case.optimized_detected_by_dsa,
1040        recovery_feature: case
1041            .top_contributing_features
1042            .iter()
1043            .find(|feature| feature.feature_name == "S092")
1044            .map(|feature| feature.feature_name.clone())
1045            .or_else(|| {
1046                case.top_contributing_features
1047                    .first()
1048                    .map(|feature| feature.feature_name.clone())
1049            }),
1050        recovery_note: format!(
1051            "The former 103/104 limiting case was failure run {}. It remained a Watch-class near-miss under {} until the bounded {} structural-support rescue was accepted, after which the selected row reached 104/104.",
1052            case.failure_id,
1053            case.exact_miss_rule,
1054            case
1055                .top_contributing_features
1056                .iter()
1057                .find(|feature| feature.feature_name == "S092")
1058                .map(|feature| feature.feature_name.as_str())
1059                .unwrap_or(top_feature.feature_name.as_str()),
1060        ),
1061    }
1062}
1063
1064fn build_lead_time_comparison(
1065    semantic_layer: &SemanticLayer,
1066    optimized_dsa: &DsaEvaluation,
1067    metrics: &BenchmarkMetrics,
1068    pre_failure_lookback_runs: usize,
1069) -> Vec<LeadTimeComparisonRow> {
1070    let dsa_by_failure = optimized_dsa
1071        .per_failure_run_signals
1072        .iter()
1073        .map(|row| (row.failure_run_index, row))
1074        .collect::<BTreeMap<_, _>>();
1075
1076    metrics
1077        .per_failure_run_signals
1078        .iter()
1079        .map(|metric_row| {
1080            let dsa_row = dsa_by_failure
1081                .get(&metric_row.failure_run_index)
1082                .copied()
1083                .unwrap_or_else(|| {
1084                    panic!("missing DSA failure row {}", metric_row.failure_run_index)
1085                });
1086            let earliest_semantic_match_run = earliest_semantic_match_run(
1087                semantic_layer,
1088                metric_row.failure_run_index,
1089                pre_failure_lookback_runs,
1090            );
1091            let earliest_semantic_match_lead_runs = earliest_semantic_match_run
1092                .map(|run_index| metric_row.failure_run_index - run_index);
1093            LeadTimeComparisonRow {
1094                failure_id: metric_row.failure_run_index,
1095                dsfb_lead_runs: dsa_row.dsa_lead_runs,
1096                threshold_lead_runs: metric_row.threshold_lead_runs,
1097                earliest_semantic_match_lead_runs,
1098                threshold_minus_dsfb_runs: diff_optional(
1099                    metric_row.threshold_lead_runs,
1100                    dsa_row.dsa_lead_runs,
1101                ),
1102                threshold_minus_semantic_match_runs: diff_optional(
1103                    metric_row.threshold_lead_runs,
1104                    earliest_semantic_match_lead_runs,
1105                ),
1106            }
1107        })
1108        .collect()
1109}
1110
1111fn build_lead_time_explanation(rows: &[LeadTimeComparisonRow]) -> LeadTimeExplanation {
1112    let mean_dsfb = mean_option(
1113        rows.iter()
1114            .filter_map(|row| row.dsfb_lead_runs.map(|value| value as f64)),
1115    );
1116    let mean_threshold = mean_option(
1117        rows.iter()
1118            .filter_map(|row| row.threshold_lead_runs.map(|value| value as f64)),
1119    );
1120    let mean_semantic = mean_option(rows.iter().filter_map(|row| {
1121        row.earliest_semantic_match_lead_runs
1122            .map(|value| value as f64)
1123    }));
1124    let threshold_earlier_failure_count = rows
1125        .iter()
1126        .filter(|row| {
1127            matches!(
1128                (row.threshold_lead_runs, row.dsfb_lead_runs),
1129                (Some(threshold), Some(dsfb)) if threshold > dsfb
1130            )
1131        })
1132        .count();
1133    let dsfb_earlier_failure_count = rows
1134        .iter()
1135        .filter(|row| {
1136            matches!(
1137                (row.threshold_lead_runs, row.dsfb_lead_runs),
1138                (Some(threshold), Some(dsfb)) if dsfb > threshold
1139            )
1140        })
1141        .count();
1142    let semantic_match_precedes_threshold_count = rows
1143        .iter()
1144        .filter(|row| {
1145            matches!(
1146                (row.earliest_semantic_match_lead_runs, row.threshold_lead_runs),
1147                (Some(semantic), Some(threshold)) if semantic > threshold
1148            )
1149        })
1150        .count();
1151
1152    LeadTimeExplanation {
1153        mean_dsfb_lead_runs: mean_dsfb,
1154        mean_threshold_lead_runs: mean_threshold,
1155        mean_semantic_match_lead_runs: mean_semantic,
1156        threshold_earlier_failure_count,
1157        dsfb_earlier_failure_count,
1158        semantic_match_precedes_threshold_count,
1159        motif_emergence_precedes_threshold_count: semantic_match_precedes_threshold_count,
1160        explanation: "Threshold fires on any sufficiently large residual deviation, while DSFB waits for grammar-qualified motif structure and then applies persistence-constrained policy promotion. That layered requirement makes DSFB more selective but also later on SECOM.".into(),
1161        validation_note: format!(
1162            "Mean threshold lead is {} runs versus mean DSA lead {} runs; earliest grammar-qualified motif emergence averages {} runs. This gap is consistent with DSFB waiting for structured motifs rather than raw deviation alone.",
1163            format_option(mean_threshold),
1164            format_option(mean_dsfb),
1165            format_option(mean_semantic),
1166        ),
1167    }
1168}
1169
1170fn missed_failure_reason(exact_miss_rule: &str) -> &'static str {
1171    match exact_miss_rule {
1172        "watch_class_near_miss_below_numeric_gate" | "policy_state_never_reached_review" => {
1173            "policy suppression"
1174        }
1175        "directional_consistency_gate" | "feature_override_fragmentation_ceiling" => {
1176            "grammar rejection"
1177        }
1178        "numeric_score_below_tau" => "no precursor",
1179        _ => "feature absence",
1180    }
1181}
1182
1183fn build_episode_precision_metrics(
1184    metrics: &BenchmarkMetrics,
1185    optimized_dsa: &DsaEvaluation,
1186) -> EpisodePrecisionMetrics {
1187    let dsfb_episode_count = optimized_dsa.episode_summary.dsa_episode_count;
1188    let dsfb_pre_failure_episode_count =
1189        optimized_dsa.episode_summary.dsa_episodes_preceding_failure;
1190    let dsfb_precision = optimized_dsa
1191        .episode_summary
1192        .precursor_quality
1193        .unwrap_or_default();
1194    let raw_alarm_count = optimized_dsa.episode_summary.raw_boundary_episode_count;
1195    let raw_alarm_precision = ratio(metrics.summary.failure_runs, raw_alarm_count);
1196    let precision_gain_factor = if raw_alarm_precision > 0.0 {
1197        dsfb_precision / raw_alarm_precision
1198    } else {
1199        0.0
1200    };
1201
1202    EpisodePrecisionMetrics {
1203        dsfb_episode_count,
1204        dsfb_pre_failure_episode_count,
1205        dsfb_precision,
1206        raw_alarm_count,
1207        raw_alarm_precision,
1208        precision_gain_factor,
1209    }
1210}
1211
1212fn earliest_semantic_match_run(
1213    semantic_layer: &SemanticLayer,
1214    failure_index: usize,
1215    pre_failure_lookback_runs: usize,
1216) -> Option<usize> {
1217    let start = failure_index.saturating_sub(pre_failure_lookback_runs);
1218    semantic_layer
1219        .semantic_matches
1220        .iter()
1221        .filter(|row| row.run_index >= start && row.run_index < failure_index)
1222        .map(|row| row.run_index)
1223        .min()
1224}
1225
1226fn investigation_points(dsa: &DsaEvaluation) -> usize {
1227    dsa.summary.review_point_count + dsa.summary.escalate_point_count
1228}
1229
1230fn count_episodes(signal: &[bool]) -> usize {
1231    let mut count = 0usize;
1232    let mut in_episode = false;
1233    for flag in signal {
1234        if *flag && !in_episode {
1235            count += 1;
1236            in_episode = true;
1237        } else if !*flag {
1238            in_episode = false;
1239        }
1240    }
1241    count
1242}
1243
1244fn episode_precision(signal: &[bool], failure_window_mask: &[bool]) -> f64 {
1245    let episodes = episode_ranges(signal);
1246    if episodes.is_empty() {
1247        return 0.0;
1248    }
1249    let preceding = episodes
1250        .iter()
1251        .filter(|(start, end)| (*start..=*end).any(|index| failure_window_mask[index]))
1252        .count();
1253    preceding as f64 / episodes.len() as f64
1254}
1255
1256fn episode_ranges(signal: &[bool]) -> Vec<(usize, usize)> {
1257    let mut episodes = Vec::new();
1258    let mut start = None;
1259    for (index, flag) in signal.iter().copied().enumerate() {
1260        match (start, flag) {
1261            (None, true) => start = Some(index),
1262            (Some(episode_start), false) => {
1263                episodes.push((episode_start, index - 1));
1264                start = None;
1265            }
1266            _ => {}
1267        }
1268    }
1269    if let Some(episode_start) = start {
1270        episodes.push((episode_start, signal.len().saturating_sub(1)));
1271    }
1272    episodes
1273}
1274
1275fn failure_window_mask(run_count: usize, labels: &[i8], lookback: usize) -> Vec<bool> {
1276    let mut mask = vec![false; run_count];
1277    for (failure_index, label) in labels.iter().enumerate() {
1278        if *label != 1 {
1279            continue;
1280        }
1281        let start = failure_index.saturating_sub(lookback);
1282        for slot in &mut mask[start..failure_index] {
1283            *slot = true;
1284        }
1285    }
1286    mask
1287}
1288
1289fn grammar_label(state: GrammarState) -> &'static str {
1290    match state {
1291        GrammarState::Admissible => "Admissible",
1292        GrammarState::Boundary => "BoundaryGrazing",
1293        GrammarState::Violation => "PersistentViolation",
1294    }
1295}
1296
1297fn is_review_or_escalate(state: DsaPolicyState) -> bool {
1298    matches!(state, DsaPolicyState::Review | DsaPolicyState::Escalate)
1299}
1300
1301fn relative_reduction(baseline: f64, value: f64) -> f64 {
1302    if baseline.abs() <= f64::EPSILON {
1303        0.0
1304    } else {
1305        (baseline - value) / baseline
1306    }
1307}
1308
1309fn relative_gain(baseline: f64, value: f64) -> f64 {
1310    if baseline.abs() <= f64::EPSILON {
1311        0.0
1312    } else {
1313        (value - baseline) / baseline
1314    }
1315}
1316
1317fn ratio(numerator: usize, denominator: usize) -> f64 {
1318    if denominator == 0 {
1319        0.0
1320    } else {
1321        numerator as f64 / denominator as f64
1322    }
1323}
1324
1325fn diff_optional(left: Option<usize>, right: Option<usize>) -> Option<i64> {
1326    match (left, right) {
1327        (Some(lhs), Some(rhs)) => Some(lhs as i64 - rhs as i64),
1328        _ => None,
1329    }
1330}
1331
1332fn mean_option(values: impl Iterator<Item = f64>) -> Option<f64> {
1333    let values = values.collect::<Vec<_>>();
1334    (!values.is_empty()).then_some(values.iter().sum::<f64>() / values.len() as f64)
1335}
1336
1337fn format_option(value: Option<f64>) -> String {
1338    value
1339        .map(|value| format!("{value:.4}"))
1340        .unwrap_or_else(|| "n/a".into())
1341}
1342
1343fn plot_error<E: std::fmt::Display>(error: E) -> DsfbSemiconductorError {
1344    DsfbSemiconductorError::DatasetFormat(format!("plot generation failed: {error}"))
1345}