Skip to main content

dsfb_semiconductor/
semiotics.rs

1use crate::grammar::{FeatureGrammarTrace, GrammarReason, GrammarSet, GrammarState};
2use crate::heuristics::{
3    expanded_semantic_policy_definitions, heuristic_policy_definition, HeuristicAlertClass,
4    PERSISTENT_INSTABILITY_CLUSTER, PRE_FAILURE_SLOW_DRIFT, RECURRENT_BOUNDARY_APPROACH,
5    TRANSIENT_EXCURSION, TRANSITION_CLUSTER_SUPPORT, TRANSITION_EXCURSION,
6    WATCH_ONLY_BOUNDARY_GRAZING,
7};
8use crate::nominal::NominalModel;
9use crate::preprocessing::PreparedDataset;
10use crate::residual::{ResidualFeatureTrace, ResidualSet};
11use crate::signs::{FeatureSigns, SignSet};
12use serde::Serialize;
13use std::collections::{BTreeMap, BTreeSet};
14
15#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum DsfbMotifClass {
17    StableAdmissible,
18    RecurrentBoundaryApproach,
19    PreFailureSlowDrift,
20    TransitionExcursion,
21    PersistentInstabilityCluster,
22    TransitionClusterSupport,
23    WatchOnlyBoundaryGrazing,
24}
25
26impl DsfbMotifClass {
27    pub fn as_lowercase(self) -> &'static str {
28        match self {
29            Self::StableAdmissible => "stable_admissible",
30            Self::RecurrentBoundaryApproach => "recurrent_boundary_approach",
31            Self::PreFailureSlowDrift => "pre_failure_slow_drift",
32            Self::TransitionExcursion => "transition_excursion",
33            Self::PersistentInstabilityCluster => "persistent_instability_cluster",
34            Self::TransitionClusterSupport => "transition_cluster_support",
35            Self::WatchOnlyBoundaryGrazing => "watch_only_boundary_grazing",
36        }
37    }
38
39    pub fn definition(self) -> &'static str {
40        match self {
41            Self::StableAdmissible => "Low residual magnitude, low drift, and low slew inside the admissibility envelope.",
42            Self::RecurrentBoundaryApproach => "Repeated boundary proximity with outward structural tendency and bounded fragmentation.",
43            Self::PreFailureSlowDrift => "Persistent outward drift with moderate residual growth and limited abrupt slew.",
44            Self::TransitionExcursion => "Elevated slew burst aligned with a grammar transition or violation onset.",
45            Self::PersistentInstabilityCluster => "Repeated or sustained outward grammar pressure that is not reducible to isolated spikes.",
46            Self::TransitionClusterSupport => "Corroborating burst or pressure feature aligned with a primary structural transition.",
47            Self::WatchOnlyBoundaryGrazing => "Boundary proximity without sufficient persistence or corroboration for Review promotion.",
48        }
49    }
50}
51
52#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
53pub enum ScaffoldGrammarState {
54    Admissible,
55    BoundaryGrazing,
56    SustainedOutwardDrift,
57    TransientViolation,
58    PersistentViolation,
59    Recovery,
60}
61
62impl ScaffoldGrammarState {
63    pub fn as_lowercase(self) -> &'static str {
64        match self {
65            Self::Admissible => "admissible",
66            Self::BoundaryGrazing => "boundary_grazing",
67            Self::SustainedOutwardDrift => "sustained_outward_drift",
68            Self::TransientViolation => "transient_violation",
69            Self::PersistentViolation => "persistent_violation",
70            Self::Recovery => "recovery",
71        }
72    }
73}
74
75#[derive(Debug, Clone, Serialize)]
76pub struct FeatureMotifTrace {
77    pub feature_index: usize,
78    pub feature_name: String,
79    pub labels: Vec<DsfbMotifClass>,
80}
81
82#[derive(Debug, Clone, Serialize)]
83pub struct MotifSummaryRow {
84    pub motif_label: String,
85    pub definition: String,
86    pub point_hits: usize,
87    pub pre_failure_point_hits: usize,
88}
89
90#[derive(Debug, Clone, Serialize)]
91pub struct SemanticMatchRecord {
92    pub feature_index: usize,
93    pub feature_name: String,
94    pub feature_role: String,
95    pub run_index: usize,
96    pub timestamp: String,
97    pub label: i8,
98    pub grammar_state: String,
99    pub grammar_reason: String,
100    pub motif_label: String,
101    pub heuristic_name: String,
102    pub alert_class_default: String,
103    pub grammar_constraints: String,
104    pub regime_conditions: String,
105    pub applicability_rules: String,
106    pub feature_scope: String,
107    pub ambiguity_note: String,
108    pub rescue_eligibility_guidance: String,
109    pub burden_contribution_class: String,
110    pub structural_score_proxy: f64,
111    pub rank: usize,
112}
113
114#[derive(Debug, Clone, Serialize)]
115pub struct StructuralDeltaMetrics {
116    pub grammar_violation_precision: Option<f64>,
117    pub motif_precision_pre_failure: Option<f64>,
118    pub structural_separation: Option<f64>,
119    pub precursor_stability: Option<f64>,
120    pub episode_precision: Option<f64>,
121    pub compression_ratio: Option<f64>,
122}
123
124#[derive(Debug, Clone, Serialize)]
125pub struct MotifSet {
126    pub traces: Vec<FeatureMotifTrace>,
127    pub summary_rows: Vec<MotifSummaryRow>,
128}
129
130#[derive(Debug, Clone, Serialize)]
131pub struct SemanticLayer {
132    pub semantic_matches: Vec<SemanticMatchRecord>,
133    pub ranked_candidates: Vec<SemanticMatchRecord>,
134    pub structural_delta_metrics: StructuralDeltaMetrics,
135}
136
137#[derive(Debug, Clone)]
138pub struct FeatureSemanticFlags {
139    pub semantic_flags: BTreeMap<&'static str, Vec<bool>>,
140    pub any_semantic_match: Vec<bool>,
141}
142
143#[derive(Debug, Clone, Serialize)]
144pub struct FeatureSignRecord {
145    pub feature_index: usize,
146    pub feature_name: String,
147    pub feature_role: String,
148    pub group_name: String,
149    pub run_index: usize,
150    pub timestamp: String,
151    pub label: i8,
152    pub normalized_residual: f64,
153    pub drift: f64,
154    pub slew: f64,
155    pub normalized_residual_norm: f64,
156    pub sigma_norm: f64,
157    pub is_imputed: bool,
158}
159
160#[derive(Debug, Clone, Serialize)]
161pub struct FeatureMotifTimelineRecord {
162    pub feature_index: usize,
163    pub feature_name: String,
164    pub feature_role: String,
165    pub group_name: String,
166    pub run_index: usize,
167    pub timestamp: String,
168    pub label: i8,
169    pub motif_label: String,
170}
171
172#[derive(Debug, Clone, Serialize)]
173pub struct FeatureGrammarStateRecord {
174    pub feature_index: usize,
175    pub feature_name: String,
176    pub feature_role: String,
177    pub group_name: String,
178    pub run_index: usize,
179    pub timestamp: String,
180    pub label: i8,
181    pub grammar_state: String,
182    pub raw_state: String,
183    pub confirmed_state: String,
184    pub raw_reason: String,
185    pub confirmed_reason: String,
186    pub normalized_envelope_ratio: f64,
187    pub persistent_boundary: bool,
188    pub persistent_violation: bool,
189    pub suppressed_by_imputation: bool,
190}
191
192#[derive(Debug, Clone, Serialize)]
193pub struct EnvelopeInteractionSummaryRow {
194    pub feature_index: usize,
195    pub feature_name: String,
196    pub feature_role: String,
197    pub group_name: String,
198    pub boundary_grazing_points: usize,
199    pub sustained_outward_drift_points: usize,
200    pub transient_violation_points: usize,
201    pub persistent_violation_points: usize,
202    pub recovery_points: usize,
203    pub max_normalized_envelope_ratio: f64,
204    pub mean_normalized_envelope_ratio: f64,
205}
206
207#[derive(Debug, Clone, Serialize)]
208pub struct ExpandedHeuristicEntry {
209    pub heuristic_name: String,
210    pub motif_signature: String,
211    pub allowed_grammar_states: String,
212    pub role_class: String,
213    pub feature_scope: String,
214    pub interpretation_text: String,
215    pub ambiguity_note: String,
216    pub rescue_eligibility_guidance: String,
217    pub burden_contribution_class: String,
218    pub alert_class_default: String,
219    pub requires_persistence: bool,
220    pub requires_corroboration: bool,
221    pub minimum_window: usize,
222    pub minimum_hits: usize,
223    pub maximum_allowed_fragmentation: f64,
224}
225
226#[derive(Debug, Clone, Serialize)]
227pub struct FeaturePolicyDecisionRecord {
228    pub feature_index: usize,
229    pub feature_name: String,
230    pub feature_role: String,
231    pub group_name: String,
232    pub run_index: usize,
233    pub timestamp: String,
234    pub label: i8,
235    pub grammar_state: String,
236    pub motif_label: String,
237    pub semantic_label: Option<String>,
238    pub policy_ceiling: String,
239    pub policy_state: String,
240    pub investigation_worthy: bool,
241    pub corroborated: bool,
242    pub corroborated_by: String,
243    pub rationale: String,
244}
245
246#[derive(Debug, Clone, Serialize)]
247pub struct GroupSignRecord {
248    pub group_name: String,
249    pub run_index: usize,
250    pub timestamp: String,
251    pub label: i8,
252    pub active_feature_count: usize,
253    pub normalized_residual_mean: f64,
254    pub drift_mean: f64,
255    pub slew_mean: f64,
256    pub envelope_separation_mean: f64,
257}
258
259#[derive(Debug, Clone, Serialize)]
260pub struct GroupGrammarStateRecord {
261    pub group_name: String,
262    pub run_index: usize,
263    pub timestamp: String,
264    pub label: i8,
265    pub active_feature_count: usize,
266    pub grammar_state: String,
267    pub boundary_member_count: usize,
268    pub pressure_member_count: usize,
269    pub violation_member_count: usize,
270    pub envelope_separation_mean: f64,
271}
272
273#[derive(Debug, Clone, Serialize)]
274pub struct GroupSemanticMatchRecord {
275    pub group_name: String,
276    pub run_index: usize,
277    pub timestamp: String,
278    pub label: i8,
279    pub grammar_state: String,
280    pub heuristic_name: String,
281    pub participating_features: String,
282    pub structural_score_proxy: f64,
283    pub rank: usize,
284}
285
286#[derive(Debug, Clone, Serialize)]
287pub struct GroupDefinitionRecord {
288    pub group_name: String,
289    pub member_features: String,
290    pub member_roles: String,
291    pub preferred_motifs: String,
292    pub empirical_basis: String,
293    pub group_size: usize,
294    pub rescue_eligible_member_count: usize,
295    pub highest_rescue_priority: String,
296    pub semantic_match_count: usize,
297    pub dominant_group_heuristic: Option<String>,
298    pub pressure_run_count: usize,
299    pub violation_run_count: usize,
300    pub mean_active_feature_count: f64,
301    pub mean_envelope_separation: f64,
302    pub coactivation_member_threshold: usize,
303    pub minimum_failure_coactivation_runs: usize,
304    pub failure_coactivation_run_count: usize,
305    pub pass_coactivation_run_count: usize,
306    pub validated: bool,
307    pub rejection_reason: Option<String>,
308}
309
310#[derive(Debug, Clone, Serialize)]
311pub struct ScaffoldSemioticsArtifacts {
312    pub feature_signs: Vec<FeatureSignRecord>,
313    pub feature_motif_timeline: Vec<FeatureMotifTimelineRecord>,
314    pub feature_grammar_states: Vec<FeatureGrammarStateRecord>,
315    pub envelope_interaction_summary: Vec<EnvelopeInteractionSummaryRow>,
316    pub heuristics_bank_expanded: Vec<ExpandedHeuristicEntry>,
317    pub feature_policy_decisions: Vec<FeaturePolicyDecisionRecord>,
318    pub group_definitions: Vec<GroupDefinitionRecord>,
319    pub group_signs: Vec<GroupSignRecord>,
320    pub group_grammar_states: Vec<GroupGrammarStateRecord>,
321    pub group_semantic_matches: Vec<GroupSemanticMatchRecord>,
322}
323
324#[derive(Debug, Clone, Copy)]
325struct FeatureScaffoldSpec {
326    feature_name: &'static str,
327    role: &'static str,
328    preferred_motifs: &'static [&'static str],
329    default_policy_ceiling: HeuristicAlertClass,
330    rescue_eligible: bool,
331    rescue_priority: &'static str,
332    group_name: &'static str,
333}
334
335#[derive(Debug, Clone, Copy)]
336struct GroupScaffoldSpec {
337    group_name: &'static str,
338    members: &'static [&'static str],
339}
340
341const FEATURE_SCAFFOLD: &[FeatureScaffoldSpec] = &[
342    FeatureScaffoldSpec {
343        feature_name: "S059",
344        role: "primary recurrent-boundary precursor",
345        preferred_motifs: &[RECURRENT_BOUNDARY_APPROACH, PRE_FAILURE_SLOW_DRIFT],
346        default_policy_ceiling: HeuristicAlertClass::Review,
347        rescue_eligible: true,
348        rescue_priority: "high",
349        group_name: "group_a",
350    },
351    FeatureScaffoldSpec {
352        feature_name: "S133",
353        role: "candidate slow-drift precursor",
354        preferred_motifs: &["slow_drift_precursor"],
355        default_policy_ceiling: HeuristicAlertClass::Review,
356        rescue_eligible: true,
357        rescue_priority: "high",
358        group_name: "group_a",
359    },
360    FeatureScaffoldSpec {
361        feature_name: "S123",
362        role: "transition / instability feature",
363        preferred_motifs: &[
364            TRANSITION_EXCURSION,
365            "persistent_instability",
366            "burst_instability",
367        ],
368        default_policy_ceiling: HeuristicAlertClass::Escalate,
369        rescue_eligible: true,
370        rescue_priority: "medium",
371        group_name: "group_b",
372    },
373    FeatureScaffoldSpec {
374        feature_name: "S540",
375        role: "burst-support corroborator",
376        preferred_motifs: &[TRANSITION_CLUSTER_SUPPORT, "burst_instability"],
377        default_policy_ceiling: HeuristicAlertClass::Review,
378        rescue_eligible: false,
379        rescue_priority: "low",
380        group_name: "group_b",
381    },
382    FeatureScaffoldSpec {
383        feature_name: "S128",
384        role: "co-burst corroborator",
385        preferred_motifs: &[TRANSITION_CLUSTER_SUPPORT],
386        default_policy_ceiling: HeuristicAlertClass::Review,
387        rescue_eligible: false,
388        rescue_priority: "low",
389        group_name: "group_b",
390    },
391    FeatureScaffoldSpec {
392        feature_name: "S104",
393        role: "watch-only sentinel",
394        preferred_motifs: &[WATCH_ONLY_BOUNDARY_GRAZING, "noise_like"],
395        default_policy_ceiling: HeuristicAlertClass::Watch,
396        rescue_eligible: false,
397        rescue_priority: "none",
398        group_name: "group_c",
399    },
400    FeatureScaffoldSpec {
401        feature_name: "S134",
402        role: "recall-rescue feature",
403        preferred_motifs: &["recovery_pattern"],
404        default_policy_ceiling: HeuristicAlertClass::Review,
405        rescue_eligible: true,
406        rescue_priority: "high",
407        group_name: "ungrouped",
408    },
409    FeatureScaffoldSpec {
410        feature_name: "S275",
411        role: "recall-rescue feature",
412        preferred_motifs: &["recovery_pattern"],
413        default_policy_ceiling: HeuristicAlertClass::Review,
414        rescue_eligible: true,
415        rescue_priority: "high",
416        group_name: "ungrouped",
417    },
418];
419
420const GROUP_SCAFFOLD: &[GroupScaffoldSpec] = &[
421    GroupScaffoldSpec {
422        group_name: "group_a",
423        members: &["S059", "S133"],
424    },
425    GroupScaffoldSpec {
426        group_name: "group_b",
427        members: &["S123", "S540", "S128"],
428    },
429    GroupScaffoldSpec {
430        group_name: "group_c",
431        members: &["S104"],
432    },
433];
434
435const GROUP_FAILURE_COACTIVATION_MIN: usize = 2;
436const GROUP_MEMBER_COACTIVATION_MIN: usize = 2;
437
438pub fn classify_motifs(
439    dataset: &PreparedDataset,
440    nominal: &NominalModel,
441    residuals: &ResidualSet,
442    signs: &SignSet,
443    grammar: &GrammarSet,
444    pre_failure_lookback_runs: usize,
445) -> MotifSet {
446    let failure_window_mask = failure_window_mask(
447        dataset.labels.len(),
448        &dataset.labels,
449        pre_failure_lookback_runs,
450    );
451    let mut traces = Vec::with_capacity(residuals.traces.len());
452    let mut counts = BTreeMap::<DsfbMotifClass, (usize, usize)>::new();
453
454    for (((residual_trace, sign_trace), grammar_trace), feature) in residuals
455        .traces
456        .iter()
457        .zip(&signs.traces)
458        .zip(&grammar.traces)
459        .zip(&nominal.features)
460    {
461        let labels =
462            classify_feature_motif_labels(residual_trace, sign_trace, grammar_trace, feature.rho);
463        for (run_index, label) in labels.iter().copied().enumerate() {
464            let entry = counts.entry(label).or_insert((0, 0));
465            entry.0 += 1;
466            if failure_window_mask[run_index] {
467                entry.1 += 1;
468            }
469        }
470        traces.push(FeatureMotifTrace {
471            feature_index: residual_trace.feature_index,
472            feature_name: residual_trace.feature_name.clone(),
473            labels,
474        });
475    }
476
477    let summary_rows = [
478        DsfbMotifClass::StableAdmissible,
479        DsfbMotifClass::RecurrentBoundaryApproach,
480        DsfbMotifClass::PreFailureSlowDrift,
481        DsfbMotifClass::TransitionExcursion,
482        DsfbMotifClass::PersistentInstabilityCluster,
483        DsfbMotifClass::TransitionClusterSupport,
484        DsfbMotifClass::WatchOnlyBoundaryGrazing,
485    ]
486    .into_iter()
487    .map(|label| {
488        let (point_hits, pre_failure_point_hits) = counts.get(&label).copied().unwrap_or((0, 0));
489        MotifSummaryRow {
490            motif_label: label.as_lowercase().into(),
491            definition: label.definition().into(),
492            point_hits,
493            pre_failure_point_hits,
494        }
495    })
496    .collect();
497
498    MotifSet {
499        traces,
500        summary_rows,
501    }
502}
503
504pub fn build_semantic_layer(
505    dataset: &PreparedDataset,
506    residuals: &ResidualSet,
507    signs: &SignSet,
508    grammar: &GrammarSet,
509    motifs: &MotifSet,
510    nominal: &NominalModel,
511    pre_failure_lookback_runs: usize,
512) -> SemanticLayer {
513    let failure_window_mask = failure_window_mask(
514        dataset.labels.len(),
515        &dataset.labels,
516        pre_failure_lookback_runs,
517    );
518    let mut semantic_matches = Vec::new();
519    let mut ranked_candidates = Vec::new();
520
521    for ((((residual_trace, sign_trace), grammar_trace), motif_trace), feature) in residuals
522        .traces
523        .iter()
524        .zip(&signs.traces)
525        .zip(&grammar.traces)
526        .zip(&motifs.traces)
527        .zip(&nominal.features)
528    {
529        let matches = build_feature_semantic_matches(
530            dataset,
531            residual_trace,
532            sign_trace,
533            grammar_trace,
534            motif_trace,
535            feature.rho,
536        );
537        ranked_candidates.extend(matches.iter().cloned());
538        semantic_matches.extend(matches);
539    }
540
541    let structural_delta_metrics = compute_structural_delta_metrics(
542        residuals,
543        grammar,
544        &semantic_matches,
545        &failure_window_mask,
546    );
547
548    SemanticLayer {
549        semantic_matches,
550        ranked_candidates,
551        structural_delta_metrics,
552    }
553}
554
555pub fn feature_semantic_flags(
556    residual_trace: &ResidualFeatureTrace,
557    _sign_trace: &FeatureSigns,
558    grammar_trace: &FeatureGrammarTrace,
559    _feature_rho: f64,
560) -> FeatureSemanticFlags {
561    let mut semantic_flags = BTreeMap::<&'static str, Vec<bool>>::new();
562    for heuristic_name in [
563        PRE_FAILURE_SLOW_DRIFT,
564        TRANSIENT_EXCURSION,
565        RECURRENT_BOUNDARY_APPROACH,
566    ] {
567        semantic_flags.insert(heuristic_name, vec![false; residual_trace.norms.len()]);
568    }
569
570    let mut any_semantic_match = vec![false; residual_trace.norms.len()];
571    for run_index in 0..residual_trace.norms.len() {
572        let state = grammar_trace.raw_states[run_index];
573        let reason = grammar_trace.raw_reasons[run_index];
574
575        if state == GrammarState::Boundary && reason == GrammarReason::SustainedOutwardDrift {
576            semantic_flags
577                .get_mut(PRE_FAILURE_SLOW_DRIFT)
578                .expect("pre_failure_slow_drift bucket")[run_index] = true;
579            any_semantic_match[run_index] = true;
580        }
581        if matches!(state, GrammarState::Boundary | GrammarState::Violation)
582            && reason == GrammarReason::AbruptSlewViolation
583        {
584            semantic_flags
585                .get_mut(TRANSIENT_EXCURSION)
586                .expect("transient_excursion bucket")[run_index] = true;
587            any_semantic_match[run_index] = true;
588        }
589        if state == GrammarState::Boundary && reason == GrammarReason::RecurrentBoundaryGrazing {
590            semantic_flags
591                .get_mut(RECURRENT_BOUNDARY_APPROACH)
592                .expect("recurrent_boundary_approach bucket")[run_index] = true;
593            any_semantic_match[run_index] = true;
594        }
595    }
596
597    FeatureSemanticFlags {
598        semantic_flags,
599        any_semantic_match,
600    }
601}
602
603pub fn build_scaffold_semiotics(
604    dataset: &PreparedDataset,
605    nominal: &NominalModel,
606    residuals: &ResidualSet,
607    grammar: &GrammarSet,
608    motifs: &MotifSet,
609    semantic_layer: &SemanticLayer,
610) -> ScaffoldSemioticsArtifacts {
611    let selected = FEATURE_SCAFFOLD
612        .iter()
613        .filter_map(|spec| {
614            let feature = nominal
615                .features
616                .iter()
617                .find(|feature| feature.feature_name == spec.feature_name)?;
618            let residual_trace = residuals
619                .traces
620                .iter()
621                .find(|trace| trace.feature_index == feature.feature_index)?;
622            let grammar_trace = grammar
623                .traces
624                .iter()
625                .find(|trace| trace.feature_index == feature.feature_index)?;
626            let motif_trace = motifs
627                .traces
628                .iter()
629                .find(|trace| trace.feature_index == feature.feature_index)?;
630            Some((
631                spec,
632                feature.rho,
633                residual_trace,
634                grammar_trace,
635                motif_trace,
636            ))
637        })
638        .collect::<Vec<_>>();
639
640    let feature_signs = build_feature_sign_records(dataset, &selected);
641    let feature_motif_timeline = build_feature_motif_timeline(dataset, &selected);
642    let feature_grammar_states = build_feature_grammar_states(dataset, &selected);
643    let envelope_interaction_summary = build_envelope_interaction_summary(&selected);
644    let heuristics_bank_expanded = build_expanded_heuristics_bank();
645    let candidate_group_signs = build_group_signs(dataset, &feature_signs);
646    let candidate_group_grammar_states =
647        build_group_grammar_states(dataset, &candidate_group_signs, &feature_grammar_states);
648    let candidate_group_semantic_matches =
649        build_group_semantic_matches(dataset, &candidate_group_grammar_states, semantic_layer);
650    let group_definitions = build_group_definitions(
651        &candidate_group_signs,
652        &candidate_group_grammar_states,
653        &candidate_group_semantic_matches,
654    );
655    let valid_group_names = group_definitions
656        .iter()
657        .filter(|row| row.validated)
658        .map(|row| row.group_name.as_str())
659        .collect::<BTreeSet<_>>();
660    let group_signs = candidate_group_signs
661        .into_iter()
662        .filter(|row| valid_group_names.contains(row.group_name.as_str()))
663        .collect::<Vec<_>>();
664    let group_grammar_states = candidate_group_grammar_states
665        .into_iter()
666        .filter(|row| valid_group_names.contains(row.group_name.as_str()))
667        .collect::<Vec<_>>();
668    let group_semantic_matches = candidate_group_semantic_matches
669        .into_iter()
670        .filter(|row| valid_group_names.contains(row.group_name.as_str()))
671        .collect::<Vec<_>>();
672    let feature_policy_decisions =
673        build_feature_policy_decisions(dataset, &selected, semantic_layer, &group_semantic_matches);
674
675    ScaffoldSemioticsArtifacts {
676        feature_signs,
677        feature_motif_timeline,
678        feature_grammar_states,
679        envelope_interaction_summary,
680        heuristics_bank_expanded,
681        feature_policy_decisions,
682        group_definitions,
683        group_signs,
684        group_grammar_states,
685        group_semantic_matches,
686    }
687}
688
689fn classify_feature_motif_labels(
690    residual_trace: &ResidualFeatureTrace,
691    sign_trace: &FeatureSigns,
692    grammar_trace: &FeatureGrammarTrace,
693    feature_rho: f64,
694) -> Vec<DsfbMotifClass> {
695    let mut labels = Vec::with_capacity(residual_trace.norms.len());
696    for run_index in 0..residual_trace.norms.len() {
697        let grammar_label = classify_grammar_label(grammar_trace, run_index);
698        let norm_ratio = residual_trace.norms[run_index] / feature_rho.max(1.0e-12);
699        let drift_threshold = sign_trace.drift_threshold.max(1.0e-12);
700        let slew_threshold = sign_trace.slew_threshold.max(1.0e-12);
701        let drift_ratio = sign_trace.drift[run_index].abs() / drift_threshold;
702        let slew_ratio = sign_trace.slew[run_index].abs() / slew_threshold;
703        let recent_start = run_index.saturating_sub(4);
704        let recent_non_admissible = (recent_start..=run_index)
705            .filter(|&index| {
706                classify_grammar_label(grammar_trace, index) != ScaffoldGrammarState::Admissible
707            })
708            .count();
709        let recent_pressure = (recent_start..=run_index)
710            .filter(|&index| {
711                matches!(
712                    classify_grammar_label(grammar_trace, index),
713                    ScaffoldGrammarState::BoundaryGrazing
714                        | ScaffoldGrammarState::SustainedOutwardDrift
715                        | ScaffoldGrammarState::TransientViolation
716                        | ScaffoldGrammarState::PersistentViolation
717                )
718            })
719            .count();
720        let is_corroborator = matches!(residual_trace.feature_name.as_str(), "S540" | "S128");
721        let is_sentinel = residual_trace.feature_name == "S104";
722
723        let label = if grammar_label == ScaffoldGrammarState::PersistentViolation
724            || (recent_pressure >= 3
725                && matches!(grammar_label, ScaffoldGrammarState::SustainedOutwardDrift))
726        {
727            if is_corroborator {
728                DsfbMotifClass::TransitionClusterSupport
729            } else {
730                DsfbMotifClass::PersistentInstabilityCluster
731            }
732        } else if matches!(grammar_label, ScaffoldGrammarState::TransientViolation)
733            && slew_ratio >= 1.0
734        {
735            if is_corroborator {
736                DsfbMotifClass::TransitionClusterSupport
737            } else {
738                DsfbMotifClass::TransitionExcursion
739            }
740        } else if grammar_label == ScaffoldGrammarState::SustainedOutwardDrift
741            && sign_trace.drift[run_index] >= sign_trace.drift_threshold
742            && slew_ratio < 1.0
743            && norm_ratio >= 0.40
744        {
745            DsfbMotifClass::PreFailureSlowDrift
746        } else if grammar_label == ScaffoldGrammarState::BoundaryGrazing {
747            if is_sentinel || recent_non_admissible < 2 {
748                DsfbMotifClass::WatchOnlyBoundaryGrazing
749            } else {
750                DsfbMotifClass::RecurrentBoundaryApproach
751            }
752        } else if is_corroborator && recent_pressure >= 2 {
753            DsfbMotifClass::TransitionClusterSupport
754        } else if norm_ratio <= 0.25
755            && drift_ratio < 1.0
756            && slew_ratio < 1.0
757            && matches!(
758                grammar_label,
759                ScaffoldGrammarState::Admissible | ScaffoldGrammarState::Recovery
760            )
761        {
762            DsfbMotifClass::StableAdmissible
763        } else if recent_non_admissible >= 2 {
764            DsfbMotifClass::RecurrentBoundaryApproach
765        } else {
766            DsfbMotifClass::StableAdmissible
767        };
768        labels.push(label);
769    }
770    labels
771}
772
773fn build_feature_semantic_matches(
774    dataset: &PreparedDataset,
775    residual_trace: &ResidualFeatureTrace,
776    sign_trace: &FeatureSigns,
777    grammar_trace: &FeatureGrammarTrace,
778    motif_trace: &FeatureMotifTrace,
779    feature_rho: f64,
780) -> Vec<SemanticMatchRecord> {
781    let mut rows = Vec::new();
782    let feature_role = feature_scaffold_spec(&residual_trace.feature_name)
783        .map(|spec| spec.role)
784        .unwrap_or("unscaffolded_feature");
785    for run_index in 0..motif_trace.labels.len() {
786        let grammar_label = classify_grammar_label(grammar_trace, run_index);
787        let candidates = semantic_candidates_for_run(
788            &residual_trace.feature_name,
789            grammar_label,
790            grammar_trace.raw_reasons[run_index],
791            motif_trace.labels[run_index],
792        );
793        for (rank, heuristic_name) in candidates.into_iter().enumerate() {
794            let Some(policy) = heuristic_policy_definition(heuristic_name) else {
795                continue;
796            };
797            let metadata = expanded_heuristic_entry_metadata(heuristic_name);
798            rows.push(SemanticMatchRecord {
799                feature_index: residual_trace.feature_index,
800                feature_name: residual_trace.feature_name.clone(),
801                feature_role: feature_role.into(),
802                run_index,
803                timestamp: dataset.timestamps[run_index]
804                    .format("%Y-%m-%d %H:%M:%S")
805                    .to_string(),
806                label: dataset.labels[run_index],
807                grammar_state: grammar_label.as_lowercase().into(),
808                grammar_reason: format!("{:?}", grammar_trace.raw_reasons[run_index]),
809                motif_label: motif_trace.labels[run_index].as_lowercase().into(),
810                heuristic_name: heuristic_name.into(),
811                alert_class_default: policy.alert_class_default.as_lowercase().into(),
812                grammar_constraints: metadata.allowed_grammar_states.into(),
813                regime_conditions: semantic_regime_conditions(heuristic_name).into(),
814                applicability_rules: semantic_applicability_rules(heuristic_name).into(),
815                feature_scope: metadata.feature_scope.into(),
816                ambiguity_note: metadata.ambiguity_note.into(),
817                rescue_eligibility_guidance: metadata.rescue_eligibility_guidance.into(),
818                burden_contribution_class: metadata.burden_contribution_class.into(),
819                structural_score_proxy: sign_trace.drift[run_index].abs()
820                    + sign_trace.slew[run_index].abs()
821                    + (residual_trace.norms[run_index] / feature_rho.max(1.0e-12)),
822                rank: rank + 1,
823            });
824        }
825    }
826    rows
827}
828
829fn semantic_candidates_for_run(
830    feature_name: &str,
831    grammar_state: ScaffoldGrammarState,
832    grammar_reason: GrammarReason,
833    motif_label: DsfbMotifClass,
834) -> Vec<&'static str> {
835    let mut candidates = Vec::new();
836    let is_corroborator = matches!(feature_name, "S540" | "S128");
837
838    if grammar_state == ScaffoldGrammarState::SustainedOutwardDrift
839        && motif_label == DsfbMotifClass::PreFailureSlowDrift
840    {
841        candidates.push(PRE_FAILURE_SLOW_DRIFT);
842    }
843    if matches!(
844        grammar_state,
845        ScaffoldGrammarState::TransientViolation | ScaffoldGrammarState::PersistentViolation
846    ) && motif_label == DsfbMotifClass::TransitionExcursion
847    {
848        candidates.push(TRANSITION_EXCURSION);
849    }
850    if grammar_state == ScaffoldGrammarState::BoundaryGrazing
851        && motif_label == DsfbMotifClass::RecurrentBoundaryApproach
852    {
853        candidates.push(RECURRENT_BOUNDARY_APPROACH);
854    }
855    if matches!(
856        grammar_state,
857        ScaffoldGrammarState::SustainedOutwardDrift | ScaffoldGrammarState::PersistentViolation
858    ) && motif_label == DsfbMotifClass::PersistentInstabilityCluster
859    {
860        candidates.push(PERSISTENT_INSTABILITY_CLUSTER);
861    }
862    if is_corroborator
863        && matches!(
864            grammar_state,
865            ScaffoldGrammarState::BoundaryGrazing
866                | ScaffoldGrammarState::SustainedOutwardDrift
867                | ScaffoldGrammarState::TransientViolation
868        )
869        && motif_label == DsfbMotifClass::TransitionClusterSupport
870    {
871        candidates.push(TRANSITION_CLUSTER_SUPPORT);
872    }
873    if grammar_state == ScaffoldGrammarState::BoundaryGrazing
874        && motif_label == DsfbMotifClass::WatchOnlyBoundaryGrazing
875    {
876        candidates.push(WATCH_ONLY_BOUNDARY_GRAZING);
877    }
878
879    if grammar_reason == GrammarReason::AbruptSlewViolation
880        && candidates.is_empty()
881        && !is_corroborator
882    {
883        candidates.push(TRANSITION_EXCURSION);
884    }
885
886    candidates
887}
888
889fn build_feature_sign_records(
890    dataset: &PreparedDataset,
891    selected: &[(
892        &FeatureScaffoldSpec,
893        f64,
894        &ResidualFeatureTrace,
895        &FeatureGrammarTrace,
896        &FeatureMotifTrace,
897    )],
898) -> Vec<FeatureSignRecord> {
899    let mut rows = Vec::new();
900    for (spec, rho, residual_trace, _, _) in selected {
901        let mut normalized_residual = Vec::with_capacity(residual_trace.residuals.len());
902        let mut drift = vec![0.0; residual_trace.residuals.len()];
903        let mut slew = vec![0.0; residual_trace.residuals.len()];
904        for run_index in 0..residual_trace.residuals.len() {
905            normalized_residual.push(residual_trace.residuals[run_index] / rho.max(1.0e-12));
906        }
907        for run_index in 1..normalized_residual.len() {
908            if residual_trace.is_imputed[run_index] || residual_trace.is_imputed[run_index - 1] {
909                drift[run_index] = 0.0;
910            } else {
911                drift[run_index] =
912                    normalized_residual[run_index] - normalized_residual[run_index - 1];
913            }
914        }
915        for run_index in 1..drift.len() {
916            if residual_trace.is_imputed[run_index] || residual_trace.is_imputed[run_index - 1] {
917                slew[run_index] = 0.0;
918            } else {
919                slew[run_index] = drift[run_index] - drift[run_index - 1];
920            }
921        }
922        for run_index in 0..normalized_residual.len() {
923            rows.push(FeatureSignRecord {
924                feature_index: residual_trace.feature_index,
925                feature_name: residual_trace.feature_name.clone(),
926                feature_role: spec.role.into(),
927                group_name: spec.group_name.into(),
928                run_index,
929                timestamp: dataset.timestamps[run_index]
930                    .format("%Y-%m-%d %H:%M:%S")
931                    .to_string(),
932                label: dataset.labels[run_index],
933                normalized_residual: normalized_residual[run_index],
934                drift: drift[run_index],
935                slew: slew[run_index],
936                normalized_residual_norm: residual_trace.norms[run_index] / rho.max(1.0e-12),
937                sigma_norm: (normalized_residual[run_index] * normalized_residual[run_index]
938                    + drift[run_index] * drift[run_index]
939                    + slew[run_index] * slew[run_index])
940                    .sqrt(),
941                is_imputed: residual_trace.is_imputed[run_index],
942            });
943        }
944    }
945    rows
946}
947
948fn build_feature_motif_timeline(
949    dataset: &PreparedDataset,
950    selected: &[(
951        &FeatureScaffoldSpec,
952        f64,
953        &ResidualFeatureTrace,
954        &FeatureGrammarTrace,
955        &FeatureMotifTrace,
956    )],
957) -> Vec<FeatureMotifTimelineRecord> {
958    let mut rows = Vec::new();
959    for (spec, _, residual_trace, _, motif_trace) in selected {
960        for (run_index, motif_label) in motif_trace.labels.iter().enumerate() {
961            rows.push(FeatureMotifTimelineRecord {
962                feature_index: residual_trace.feature_index,
963                feature_name: residual_trace.feature_name.clone(),
964                feature_role: spec.role.into(),
965                group_name: spec.group_name.into(),
966                run_index,
967                timestamp: dataset.timestamps[run_index]
968                    .format("%Y-%m-%d %H:%M:%S")
969                    .to_string(),
970                label: dataset.labels[run_index],
971                motif_label: motif_label.as_lowercase().into(),
972            });
973        }
974    }
975    rows
976}
977
978fn build_feature_grammar_states(
979    dataset: &PreparedDataset,
980    selected: &[(
981        &FeatureScaffoldSpec,
982        f64,
983        &ResidualFeatureTrace,
984        &FeatureGrammarTrace,
985        &FeatureMotifTrace,
986    )],
987) -> Vec<FeatureGrammarStateRecord> {
988    let mut rows = Vec::new();
989    for (spec, rho, residual_trace, grammar_trace, _) in selected {
990        for run_index in 0..grammar_trace.raw_states.len() {
991            rows.push(FeatureGrammarStateRecord {
992                feature_index: residual_trace.feature_index,
993                feature_name: residual_trace.feature_name.clone(),
994                feature_role: spec.role.into(),
995                group_name: spec.group_name.into(),
996                run_index,
997                timestamp: dataset.timestamps[run_index]
998                    .format("%Y-%m-%d %H:%M:%S")
999                    .to_string(),
1000                label: dataset.labels[run_index],
1001                grammar_state: classify_grammar_label(grammar_trace, run_index)
1002                    .as_lowercase()
1003                    .into(),
1004                raw_state: format!("{:?}", grammar_trace.raw_states[run_index]),
1005                confirmed_state: format!("{:?}", grammar_trace.states[run_index]),
1006                raw_reason: format!("{:?}", grammar_trace.raw_reasons[run_index]),
1007                confirmed_reason: format!("{:?}", grammar_trace.reasons[run_index]),
1008                normalized_envelope_ratio: residual_trace.norms[run_index] / rho.max(1.0e-12),
1009                persistent_boundary: grammar_trace.persistent_boundary[run_index],
1010                persistent_violation: grammar_trace.persistent_violation[run_index],
1011                suppressed_by_imputation: grammar_trace.suppressed_by_imputation[run_index],
1012            });
1013        }
1014    }
1015    rows
1016}
1017
1018fn build_envelope_interaction_summary(
1019    selected: &[(
1020        &FeatureScaffoldSpec,
1021        f64,
1022        &ResidualFeatureTrace,
1023        &FeatureGrammarTrace,
1024        &FeatureMotifTrace,
1025    )],
1026) -> Vec<EnvelopeInteractionSummaryRow> {
1027    selected
1028        .iter()
1029        .map(|(spec, rho, residual_trace, grammar_trace, _)| {
1030            let mut boundary_grazing_points = 0usize;
1031            let mut sustained_outward_drift_points = 0usize;
1032            let mut transient_violation_points = 0usize;
1033            let mut persistent_violation_points = 0usize;
1034            let mut recovery_points = 0usize;
1035            let mut max_ratio = 0.0_f64;
1036            let mut ratios = Vec::with_capacity(residual_trace.norms.len());
1037            for run_index in 0..residual_trace.norms.len() {
1038                let grammar_label = classify_grammar_label(grammar_trace, run_index);
1039                match grammar_label {
1040                    ScaffoldGrammarState::BoundaryGrazing => boundary_grazing_points += 1,
1041                    ScaffoldGrammarState::SustainedOutwardDrift => {
1042                        sustained_outward_drift_points += 1
1043                    }
1044                    ScaffoldGrammarState::TransientViolation => transient_violation_points += 1,
1045                    ScaffoldGrammarState::PersistentViolation => persistent_violation_points += 1,
1046                    ScaffoldGrammarState::Recovery => recovery_points += 1,
1047                    ScaffoldGrammarState::Admissible => {}
1048                }
1049                let ratio = residual_trace.norms[run_index] / rho.max(1.0e-12);
1050                max_ratio = max_ratio.max(ratio);
1051                ratios.push(ratio);
1052            }
1053            EnvelopeInteractionSummaryRow {
1054                feature_index: residual_trace.feature_index,
1055                feature_name: residual_trace.feature_name.clone(),
1056                feature_role: spec.role.into(),
1057                group_name: spec.group_name.into(),
1058                boundary_grazing_points,
1059                sustained_outward_drift_points,
1060                transient_violation_points,
1061                persistent_violation_points,
1062                recovery_points,
1063                max_normalized_envelope_ratio: max_ratio,
1064                mean_normalized_envelope_ratio: mean(&ratios).unwrap_or(0.0),
1065            }
1066        })
1067        .collect()
1068}
1069
1070fn build_expanded_heuristics_bank() -> Vec<ExpandedHeuristicEntry> {
1071    expanded_semantic_policy_definitions()
1072        .into_iter()
1073        .map(|definition| {
1074            let metadata = expanded_heuristic_entry_metadata(definition.motif_name);
1075            ExpandedHeuristicEntry {
1076                heuristic_name: definition.motif_name.into(),
1077                motif_signature: definition.signature_definition.into(),
1078                allowed_grammar_states: metadata.allowed_grammar_states.into(),
1079                role_class: metadata.role_class.into(),
1080                feature_scope: metadata.feature_scope.into(),
1081                interpretation_text: definition.interpretation.into(),
1082                ambiguity_note: metadata.ambiguity_note.into(),
1083                rescue_eligibility_guidance: metadata.rescue_eligibility_guidance.into(),
1084                burden_contribution_class: metadata.burden_contribution_class.into(),
1085                alert_class_default: definition.alert_class_default.as_lowercase().into(),
1086                requires_persistence: definition.requires_persistence,
1087                requires_corroboration: definition.requires_corroboration,
1088                minimum_window: definition.minimum_window,
1089                minimum_hits: definition.minimum_hits,
1090                maximum_allowed_fragmentation: definition.maximum_allowed_fragmentation(),
1091            }
1092        })
1093        .collect()
1094}
1095
1096fn build_group_signs(
1097    dataset: &PreparedDataset,
1098    feature_signs: &[FeatureSignRecord],
1099) -> Vec<GroupSignRecord> {
1100    let by_key = feature_signs.iter().fold(
1101        BTreeMap::<(&str, usize), Vec<&FeatureSignRecord>>::new(),
1102        |mut acc, row| {
1103            acc.entry((row.group_name.as_str(), row.run_index))
1104                .or_default()
1105                .push(row);
1106            acc
1107        },
1108    );
1109    let mut rows: Vec<GroupSignRecord> = Vec::new();
1110    for group in GROUP_SCAFFOLD {
1111        for run_index in 0..dataset.labels.len() {
1112            let members = by_key
1113                .get(&(group.group_name, run_index))
1114                .cloned()
1115                .unwrap_or_default();
1116            let active_feature_count = members.len();
1117            let normalized_residual_mean = mean_of_records(&members, |row| row.normalized_residual);
1118            let drift_mean = mean_of_records(&members, |row| row.drift);
1119            let slew_mean = mean_of_records(&members, |row| row.slew);
1120            let envelope_separation_mean =
1121                mean_of_records(&members, |row| row.normalized_residual_norm);
1122            rows.push(GroupSignRecord {
1123                group_name: group.group_name.into(),
1124                run_index,
1125                timestamp: dataset.timestamps[run_index]
1126                    .format("%Y-%m-%d %H:%M:%S")
1127                    .to_string(),
1128                label: dataset.labels[run_index],
1129                active_feature_count,
1130                normalized_residual_mean,
1131                drift_mean,
1132                slew_mean,
1133                envelope_separation_mean,
1134            });
1135        }
1136    }
1137    rows
1138}
1139
1140fn build_group_grammar_states(
1141    dataset: &PreparedDataset,
1142    group_signs: &[GroupSignRecord],
1143    feature_grammar_states: &[FeatureGrammarStateRecord],
1144) -> Vec<GroupGrammarStateRecord> {
1145    let feature_by_key = feature_grammar_states.iter().fold(
1146        BTreeMap::<(&str, usize), Vec<&FeatureGrammarStateRecord>>::new(),
1147        |mut acc, row| {
1148            acc.entry((row.group_name.as_str(), row.run_index))
1149                .or_default()
1150                .push(row);
1151            acc
1152        },
1153    );
1154    let mut rows: Vec<GroupGrammarStateRecord> = Vec::new();
1155    for sign_row in group_signs {
1156        let members = feature_by_key
1157            .get(&(sign_row.group_name.as_str(), sign_row.run_index))
1158            .cloned()
1159            .unwrap_or_default();
1160        let boundary_member_count = members
1161            .iter()
1162            .filter(|row| row.grammar_state == ScaffoldGrammarState::BoundaryGrazing.as_lowercase())
1163            .count();
1164        let pressure_member_count = members
1165            .iter()
1166            .filter(|row| {
1167                matches!(
1168                    row.grammar_state.as_str(),
1169                    "boundary_grazing"
1170                        | "sustained_outward_drift"
1171                        | "transient_violation"
1172                        | "persistent_violation"
1173                )
1174            })
1175            .count();
1176        let violation_member_count = members
1177            .iter()
1178            .filter(|row| {
1179                matches!(
1180                    row.grammar_state.as_str(),
1181                    "transient_violation" | "persistent_violation"
1182                )
1183            })
1184            .count();
1185        let previous_state = sign_row.run_index.checked_sub(1).and_then(|previous| {
1186            rows.iter()
1187                .rev()
1188                .find(|row| row.group_name == sign_row.group_name && row.run_index == previous)
1189                .map(|row| row.grammar_state.as_str())
1190        });
1191        let grammar_state = if sign_row.active_feature_count == 0 {
1192            ScaffoldGrammarState::Admissible
1193        } else if sign_row.envelope_separation_mean >= 1.0 && violation_member_count >= 2 {
1194            ScaffoldGrammarState::PersistentViolation
1195        } else if sign_row.envelope_separation_mean >= 1.0 && violation_member_count >= 1 {
1196            ScaffoldGrammarState::TransientViolation
1197        } else if sign_row.envelope_separation_mean >= 0.6 && sign_row.drift_mean > 0.0 {
1198            ScaffoldGrammarState::SustainedOutwardDrift
1199        } else if sign_row.envelope_separation_mean >= 0.6 && boundary_member_count >= 1 {
1200            ScaffoldGrammarState::BoundaryGrazing
1201        } else if previous_state.is_some_and(|state| {
1202            matches!(
1203                state,
1204                "boundary_grazing"
1205                    | "sustained_outward_drift"
1206                    | "transient_violation"
1207                    | "persistent_violation"
1208            )
1209        }) && sign_row.envelope_separation_mean < 0.4
1210        {
1211            ScaffoldGrammarState::Recovery
1212        } else {
1213            ScaffoldGrammarState::Admissible
1214        };
1215        rows.push(GroupGrammarStateRecord {
1216            group_name: sign_row.group_name.clone(),
1217            run_index: sign_row.run_index,
1218            timestamp: dataset.timestamps[sign_row.run_index]
1219                .format("%Y-%m-%d %H:%M:%S")
1220                .to_string(),
1221            label: dataset.labels[sign_row.run_index],
1222            active_feature_count: sign_row.active_feature_count,
1223            grammar_state: grammar_state.as_lowercase().into(),
1224            boundary_member_count,
1225            pressure_member_count,
1226            violation_member_count,
1227            envelope_separation_mean: sign_row.envelope_separation_mean,
1228        });
1229    }
1230    rows
1231}
1232
1233fn build_group_semantic_matches(
1234    dataset: &PreparedDataset,
1235    group_grammar_states: &[GroupGrammarStateRecord],
1236    semantic_layer: &SemanticLayer,
1237) -> Vec<GroupSemanticMatchRecord> {
1238    let feature_matches = semantic_layer.semantic_matches.iter().fold(
1239        BTreeMap::<(&str, usize), Vec<&SemanticMatchRecord>>::new(),
1240        |mut acc, row| {
1241            if let Some(spec) = feature_scaffold_spec(&row.feature_name) {
1242                acc.entry((spec.group_name, row.run_index))
1243                    .or_default()
1244                    .push(row);
1245            }
1246            acc
1247        },
1248    );
1249
1250    let mut rows = Vec::new();
1251    for group_row in group_grammar_states {
1252        let Some(group_spec) = group_scaffold_spec(&group_row.group_name) else {
1253            continue;
1254        };
1255        let matches = feature_matches
1256            .get(&(group_row.group_name.as_str(), group_row.run_index))
1257            .cloned()
1258            .unwrap_or_default();
1259        let mut candidates = Vec::<(&'static str, Vec<String>, f64)>::new();
1260        let participating_features = matches
1261            .iter()
1262            .map(|row| row.feature_name.clone())
1263            .collect::<Vec<_>>();
1264        let score = matches
1265            .iter()
1266            .map(|row| row.structural_score_proxy)
1267            .fold(0.0, f64::max);
1268        if group_row.group_name == "group_a" {
1269            if matches
1270                .iter()
1271                .any(|row| row.heuristic_name == PRE_FAILURE_SLOW_DRIFT)
1272                && matches!(
1273                    group_row.grammar_state.as_str(),
1274                    "sustained_outward_drift" | "persistent_violation"
1275                )
1276            {
1277                candidates.push((
1278                    PRE_FAILURE_SLOW_DRIFT,
1279                    participating_features.clone(),
1280                    score,
1281                ));
1282            }
1283            if matches
1284                .iter()
1285                .any(|row| row.heuristic_name == RECURRENT_BOUNDARY_APPROACH)
1286                && matches!(
1287                    group_row.grammar_state.as_str(),
1288                    "boundary_grazing" | "sustained_outward_drift"
1289                )
1290            {
1291                candidates.push((
1292                    RECURRENT_BOUNDARY_APPROACH,
1293                    participating_features.clone(),
1294                    score,
1295                ));
1296            }
1297        } else if group_row.group_name == "group_b" {
1298            if matches
1299                .iter()
1300                .any(|row| row.heuristic_name == TRANSITION_EXCURSION)
1301                && matches!(
1302                    group_row.grammar_state.as_str(),
1303                    "transient_violation" | "persistent_violation"
1304                )
1305            {
1306                candidates.push((
1307                    PERSISTENT_INSTABILITY_CLUSTER,
1308                    participating_features.clone(),
1309                    score,
1310                ));
1311            }
1312            if matches
1313                .iter()
1314                .any(|row| row.heuristic_name == TRANSITION_CLUSTER_SUPPORT)
1315            {
1316                candidates.push((
1317                    TRANSITION_CLUSTER_SUPPORT,
1318                    participating_features.clone(),
1319                    score,
1320                ));
1321            }
1322        } else if group_row.group_name == "group_c"
1323            && group_row.grammar_state == ScaffoldGrammarState::BoundaryGrazing.as_lowercase()
1324            && matches
1325                .iter()
1326                .any(|row| row.heuristic_name == WATCH_ONLY_BOUNDARY_GRAZING)
1327        {
1328            candidates.push((
1329                WATCH_ONLY_BOUNDARY_GRAZING,
1330                participating_features.clone(),
1331                score,
1332            ));
1333        }
1334        for (rank, (heuristic_name, participating, structural_score_proxy)) in
1335            candidates.into_iter().enumerate()
1336        {
1337            rows.push(GroupSemanticMatchRecord {
1338                group_name: group_spec.group_name.into(),
1339                run_index: group_row.run_index,
1340                timestamp: dataset.timestamps[group_row.run_index]
1341                    .format("%Y-%m-%d %H:%M:%S")
1342                    .to_string(),
1343                label: dataset.labels[group_row.run_index],
1344                grammar_state: group_row.grammar_state.clone(),
1345                heuristic_name: heuristic_name.into(),
1346                participating_features: participating.join(","),
1347                structural_score_proxy,
1348                rank: rank + 1,
1349            });
1350        }
1351    }
1352    rows
1353}
1354
1355fn build_group_definitions(
1356    group_signs: &[GroupSignRecord],
1357    group_grammar_states: &[GroupGrammarStateRecord],
1358    group_semantic_matches: &[GroupSemanticMatchRecord],
1359) -> Vec<GroupDefinitionRecord> {
1360    let group_signs_by_name = group_signs.iter().fold(
1361        BTreeMap::<&str, Vec<&GroupSignRecord>>::new(),
1362        |mut acc, row| {
1363            acc.entry(row.group_name.as_str()).or_default().push(row);
1364            acc
1365        },
1366    );
1367    let group_grammar_by_name = group_grammar_states.iter().fold(
1368        BTreeMap::<&str, Vec<&GroupGrammarStateRecord>>::new(),
1369        |mut acc, row| {
1370            acc.entry(row.group_name.as_str()).or_default().push(row);
1371            acc
1372        },
1373    );
1374    let group_semantics_by_name = group_semantic_matches.iter().fold(
1375        BTreeMap::<&str, Vec<&GroupSemanticMatchRecord>>::new(),
1376        |mut acc, row| {
1377            acc.entry(row.group_name.as_str()).or_default().push(row);
1378            acc
1379        },
1380    );
1381
1382    GROUP_SCAFFOLD
1383        .iter()
1384        .map(|group| {
1385            let signs = group_signs_by_name
1386                .get(group.group_name)
1387                .cloned()
1388                .unwrap_or_default();
1389            let grammar_rows = group_grammar_by_name
1390                .get(group.group_name)
1391                .cloned()
1392                .unwrap_or_default();
1393            let semantic_rows = group_semantics_by_name
1394                .get(group.group_name)
1395                .cloned()
1396                .unwrap_or_default();
1397            let members = group
1398                .members
1399                .iter()
1400                .filter_map(|feature_name| feature_scaffold_spec(feature_name))
1401                .collect::<Vec<_>>();
1402            let preferred_motifs = members
1403                .iter()
1404                .flat_map(|spec| spec.preferred_motifs.iter().copied())
1405                .collect::<Vec<_>>();
1406            let mut preferred_motifs = preferred_motifs;
1407            preferred_motifs.sort_unstable();
1408            preferred_motifs.dedup();
1409            let mut heuristic_counts = BTreeMap::<String, usize>::new();
1410            for row in &semantic_rows {
1411                *heuristic_counts
1412                    .entry(row.heuristic_name.clone())
1413                    .or_default() += 1;
1414            }
1415            let dominant_group_heuristic = heuristic_counts
1416                .into_iter()
1417                .max_by(|left, right| left.1.cmp(&right.1).then_with(|| right.0.cmp(&left.0)))
1418                .map(|(name, _)| name);
1419            let highest_rescue_priority = members
1420                .iter()
1421                .map(|spec| spec.rescue_priority)
1422                .max_by_key(|priority| rescue_priority_rank(priority))
1423                .unwrap_or("none");
1424            let coactivation_member_threshold = group.members.len().min(GROUP_MEMBER_COACTIVATION_MIN).max(1);
1425            let failure_coactivation_run_count = grammar_rows
1426                .iter()
1427                .filter(|row| {
1428                    row.label == 1
1429                        && row.pressure_member_count >= coactivation_member_threshold
1430                })
1431                .count();
1432            let pass_coactivation_run_count = grammar_rows
1433                .iter()
1434                .filter(|row| {
1435                    row.label == -1
1436                        && row.pressure_member_count >= coactivation_member_threshold
1437                })
1438                .count();
1439            let validated = failure_coactivation_run_count >= GROUP_FAILURE_COACTIVATION_MIN
1440                && pass_coactivation_run_count == 0;
1441            let rejection_reason = if validated {
1442                None
1443            } else if failure_coactivation_run_count < GROUP_FAILURE_COACTIVATION_MIN {
1444                Some("failure co-activation is below the required minimum".into())
1445            } else if pass_coactivation_run_count > 0 {
1446                Some("group co-activates in pass runs and is rejected".into())
1447            } else {
1448                Some("group failed strict grouped-semiotics validation".into())
1449            };
1450
1451            GroupDefinitionRecord {
1452                group_name: group.group_name.into(),
1453                member_features: group.members.join(","),
1454                member_roles: members
1455                    .iter()
1456                    .map(|spec| spec.role)
1457                    .collect::<Vec<_>>()
1458                    .join(","),
1459                preferred_motifs: preferred_motifs.join(","),
1460                empirical_basis: format!(
1461                    "Scaffolded from saved top-feature co-activity; {} grouped semantic matches, {} pressure runs, {} violation runs.",
1462                    semantic_rows.len(),
1463                    grammar_rows
1464                        .iter()
1465                        .filter(|row| {
1466                            matches!(
1467                                row.grammar_state.as_str(),
1468                                "boundary_grazing"
1469                                    | "sustained_outward_drift"
1470                                    | "transient_violation"
1471                                    | "persistent_violation"
1472                            )
1473                        })
1474                        .count(),
1475                    grammar_rows
1476                        .iter()
1477                        .filter(|row| {
1478                            matches!(
1479                                row.grammar_state.as_str(),
1480                                "transient_violation" | "persistent_violation"
1481                            )
1482                        })
1483                        .count()
1484                ),
1485                group_size: group.members.len(),
1486                rescue_eligible_member_count: members
1487                    .iter()
1488                    .filter(|spec| spec.rescue_eligible)
1489                    .count(),
1490                highest_rescue_priority: highest_rescue_priority.into(),
1491                semantic_match_count: semantic_rows.len(),
1492                dominant_group_heuristic,
1493                pressure_run_count: grammar_rows
1494                    .iter()
1495                    .filter(|row| {
1496                        matches!(
1497                            row.grammar_state.as_str(),
1498                            "boundary_grazing"
1499                                | "sustained_outward_drift"
1500                                | "transient_violation"
1501                                | "persistent_violation"
1502                        )
1503                    })
1504                    .count(),
1505                violation_run_count: grammar_rows
1506                    .iter()
1507                    .filter(|row| {
1508                        matches!(
1509                            row.grammar_state.as_str(),
1510                            "transient_violation" | "persistent_violation"
1511                        )
1512                    })
1513                    .count(),
1514                mean_active_feature_count: mean_of_records(&signs, |row| row.active_feature_count as f64),
1515                mean_envelope_separation: mean_of_records(&signs, |row| row.envelope_separation_mean),
1516                coactivation_member_threshold,
1517                minimum_failure_coactivation_runs: GROUP_FAILURE_COACTIVATION_MIN,
1518                failure_coactivation_run_count,
1519                pass_coactivation_run_count,
1520                validated,
1521                rejection_reason,
1522            }
1523        })
1524        .collect()
1525}
1526
1527fn build_feature_policy_decisions(
1528    dataset: &PreparedDataset,
1529    selected: &[(
1530        &FeatureScaffoldSpec,
1531        f64,
1532        &ResidualFeatureTrace,
1533        &FeatureGrammarTrace,
1534        &FeatureMotifTrace,
1535    )],
1536    semantic_layer: &SemanticLayer,
1537    group_semantic_matches: &[GroupSemanticMatchRecord],
1538) -> Vec<FeaturePolicyDecisionRecord> {
1539    let semantic_by_feature_run = semantic_layer
1540        .ranked_candidates
1541        .iter()
1542        .filter_map(|row| {
1543            feature_scaffold_spec(&row.feature_name)?;
1544            Some(((row.feature_name.clone(), row.run_index), row.clone()))
1545        })
1546        .fold(
1547            BTreeMap::<(String, usize), SemanticMatchRecord>::new(),
1548            |mut acc, (key, row)| {
1549                acc.entry(key)
1550                    .and_modify(|existing| {
1551                        if row.rank < existing.rank {
1552                            *existing = row.clone();
1553                        }
1554                    })
1555                    .or_insert(row);
1556                acc
1557            },
1558        );
1559    let group_support = group_semantic_matches.iter().fold(
1560        BTreeMap::<(&str, usize), Vec<&GroupSemanticMatchRecord>>::new(),
1561        |mut acc, row| {
1562            acc.entry((row.group_name.as_str(), row.run_index))
1563                .or_default()
1564                .push(row);
1565            acc
1566        },
1567    );
1568
1569    let mut rows = Vec::new();
1570    let mut base_states = BTreeMap::<(String, usize), String>::new();
1571
1572    for (spec, _, residual_trace, grammar_trace, motif_trace) in selected {
1573        for run_index in 0..residual_trace.norms.len() {
1574            let semantic = semantic_by_feature_run
1575                .get(&(residual_trace.feature_name.clone(), run_index))
1576                .cloned();
1577            let grammar_state = classify_grammar_label(grammar_trace, run_index);
1578            let motif_label = motif_trace.labels[run_index];
1579            let group_matches = group_support
1580                .get(&(spec.group_name, run_index))
1581                .cloned()
1582                .unwrap_or_default();
1583            let mut corroborators = group_matches
1584                .iter()
1585                .flat_map(|row| row.participating_features.split(','))
1586                .map(str::trim)
1587                .filter(|name| !name.is_empty() && *name != residual_trace.feature_name)
1588                .map(str::to_string)
1589                .collect::<Vec<_>>();
1590            corroborators.sort();
1591            corroborators.dedup();
1592
1593            let (policy_state, rationale) = base_policy_state(
1594                spec,
1595                grammar_state,
1596                motif_label,
1597                semantic.as_ref(),
1598                !corroborators.is_empty(),
1599            );
1600            base_states.insert(
1601                (residual_trace.feature_name.clone(), run_index),
1602                policy_state.to_string(),
1603            );
1604            rows.push(FeaturePolicyDecisionRecord {
1605                feature_index: residual_trace.feature_index,
1606                feature_name: residual_trace.feature_name.clone(),
1607                feature_role: spec.role.into(),
1608                group_name: spec.group_name.into(),
1609                run_index,
1610                timestamp: dataset.timestamps[run_index]
1611                    .format("%Y-%m-%d %H:%M:%S")
1612                    .to_string(),
1613                label: dataset.labels[run_index],
1614                grammar_state: grammar_state.as_lowercase().into(),
1615                motif_label: motif_label.as_lowercase().into(),
1616                semantic_label: semantic.as_ref().map(|row| row.heuristic_name.clone()),
1617                policy_ceiling: spec.default_policy_ceiling.as_lowercase().into(),
1618                policy_state: policy_state.into(),
1619                investigation_worthy: matches!(policy_state, "review" | "escalate"),
1620                corroborated: !corroborators.is_empty(),
1621                corroborated_by: corroborators.join(","),
1622                rationale,
1623            });
1624        }
1625    }
1626
1627    for row in &mut rows {
1628        let related_support = related_feature_support(
1629            &base_states,
1630            &row.feature_name,
1631            row.run_index,
1632            row.corroborated,
1633        );
1634        if row.feature_name == "S059"
1635            && row.policy_state == "review"
1636            && (related_support
1637                || row.grammar_state == ScaffoldGrammarState::PersistentViolation.as_lowercase())
1638        {
1639            row.policy_state = "escalate".into();
1640            row.investigation_worthy = true;
1641            row.rationale =
1642                "primary precursor escalated only after corroboration or persistent violation"
1643                    .into();
1644        } else if row.feature_name == "S123"
1645            && row.policy_state == "review"
1646            && (related_support
1647                || row.grammar_state == ScaffoldGrammarState::PersistentViolation.as_lowercase())
1648        {
1649            row.policy_state = "escalate".into();
1650            row.investigation_worthy = true;
1651            row.rationale = "transition instability escalated under grammar transition support or persistent violation".into();
1652        } else if matches!(row.feature_name.as_str(), "S540" | "S128")
1653            && row.policy_state == "escalate"
1654        {
1655            row.policy_state = "review".into();
1656            row.investigation_worthy = true;
1657            row.rationale = "corroborator never escalates alone in the scaffold policy".into();
1658        } else if row.feature_name == "S104" && row.policy_state != "watch" {
1659            row.policy_state = "watch".into();
1660            row.investigation_worthy = false;
1661            row.rationale = "sentinel remains watch-only by scaffold design".into();
1662        }
1663    }
1664
1665    rows
1666}
1667
1668fn base_policy_state(
1669    spec: &FeatureScaffoldSpec,
1670    grammar_state: ScaffoldGrammarState,
1671    motif_label: DsfbMotifClass,
1672    semantic: Option<&SemanticMatchRecord>,
1673    corroborated: bool,
1674) -> (&'static str, String) {
1675    match spec.feature_name {
1676        "S059" => {
1677            if semantic
1678                .as_ref()
1679                .is_some_and(|row| row.heuristic_name == RECURRENT_BOUNDARY_APPROACH)
1680                && grammar_state == ScaffoldGrammarState::BoundaryGrazing
1681            {
1682                ("review", "S059 recurrent boundary approach promoted to Review under boundary grazing".into())
1683            } else if semantic
1684                .as_ref()
1685                .is_some_and(|row| row.heuristic_name == PRE_FAILURE_SLOW_DRIFT)
1686                && matches!(
1687                    grammar_state,
1688                    ScaffoldGrammarState::SustainedOutwardDrift
1689                        | ScaffoldGrammarState::PersistentViolation
1690                )
1691            {
1692                (
1693                    if corroborated { "review" } else { "watch" },
1694                    "S059 slow drift retained unless grammar and corroboration support promotion".into(),
1695                )
1696            } else {
1697                ("silent", "S059 remained below scaffold promotion conditions".into())
1698            }
1699        }
1700        "S133" => {
1701            if semantic
1702                .as_ref()
1703                .is_some_and(|row| row.heuristic_name == PRE_FAILURE_SLOW_DRIFT)
1704                && grammar_state == ScaffoldGrammarState::SustainedOutwardDrift
1705            {
1706                ("review", "S133 remains Review-only when failure-localized slow drift is present".into())
1707            } else {
1708                ("silent", "S133 remained below scaffold promotion conditions".into())
1709            }
1710        }
1711        "S123" => {
1712            if semantic
1713                .as_ref()
1714                .is_some_and(|row| row.heuristic_name == TRANSITION_EXCURSION)
1715                && matches!(
1716                    grammar_state,
1717                    ScaffoldGrammarState::TransientViolation
1718                        | ScaffoldGrammarState::PersistentViolation
1719                )
1720            {
1721                (
1722                    if grammar_state == ScaffoldGrammarState::PersistentViolation {
1723                        "escalate"
1724                    } else {
1725                        "review"
1726                    },
1727                    "S123 transition instability promoted rapidly after grammar-qualified transition excursion".into(),
1728                )
1729            } else if motif_label == DsfbMotifClass::PersistentInstabilityCluster {
1730                ("review", "S123 persistent instability cluster retained at Review until corroborated".into())
1731            } else {
1732                ("silent", "S123 remained below scaffold promotion conditions".into())
1733            }
1734        }
1735        "S540" | "S128" => {
1736            if semantic
1737                .as_ref()
1738                .is_some_and(|row| row.heuristic_name == TRANSITION_CLUSTER_SUPPORT)
1739            {
1740                (
1741                    if corroborated { "review" } else { "watch" },
1742                    "secondary corroborator contributes support but cannot escalate alone".into(),
1743                )
1744            } else {
1745                ("silent", "secondary corroborator remained structurally isolated".into())
1746            }
1747        }
1748        "S104" => {
1749            if matches!(motif_label, DsfbMotifClass::WatchOnlyBoundaryGrazing)
1750                && grammar_state == ScaffoldGrammarState::BoundaryGrazing
1751            {
1752                ("watch", "sentinel boundary grazing is watch-only by scaffold design".into())
1753            } else {
1754                ("silent", "sentinel remained admissible or unsupported".into())
1755            }
1756        }
1757        "S134" | "S275" => (
1758            "silent",
1759            "recall-rescue feature remains outside primary scaffold promotion and is reserved for bounded rescue logic".into(),
1760        ),
1761        _ => ("silent", format!("{} is outside the scaffolded policy set", spec.feature_name)),
1762    }
1763}
1764
1765fn related_feature_support(
1766    base_states: &BTreeMap<(String, usize), String>,
1767    feature_name: &str,
1768    run_index: usize,
1769    corroborated: bool,
1770) -> bool {
1771    if corroborated {
1772        return true;
1773    }
1774    match feature_name {
1775        "S059" => {
1776            has_active_state(base_states, "S133", run_index)
1777                || has_active_state(base_states, "S123", run_index)
1778        }
1779        "S133" => {
1780            has_active_state(base_states, "S059", run_index)
1781                || has_active_state(base_states, "S123", run_index)
1782        }
1783        "S123" => {
1784            has_active_state(base_states, "S059", run_index)
1785                || has_active_state(base_states, "S540", run_index)
1786                || has_active_state(base_states, "S128", run_index)
1787        }
1788        _ => false,
1789    }
1790}
1791
1792fn has_active_state(
1793    base_states: &BTreeMap<(String, usize), String>,
1794    feature_name: &str,
1795    run_index: usize,
1796) -> bool {
1797    base_states
1798        .get(&(feature_name.to_string(), run_index))
1799        .is_some_and(|state| matches!(state.as_str(), "review" | "escalate"))
1800}
1801
1802fn feature_scaffold_spec(feature_name: &str) -> Option<&'static FeatureScaffoldSpec> {
1803    FEATURE_SCAFFOLD
1804        .iter()
1805        .find(|spec| spec.feature_name == feature_name)
1806}
1807
1808fn group_scaffold_spec(group_name: &str) -> Option<&'static GroupScaffoldSpec> {
1809    GROUP_SCAFFOLD
1810        .iter()
1811        .find(|spec| spec.group_name == group_name)
1812}
1813
1814fn rescue_priority_rank(priority: &str) -> usize {
1815    match priority {
1816        "high" => 3,
1817        "medium" => 2,
1818        "low" => 1,
1819        _ => 0,
1820    }
1821}
1822
1823fn classify_grammar_label(
1824    grammar_trace: &FeatureGrammarTrace,
1825    run_index: usize,
1826) -> ScaffoldGrammarState {
1827    let confirmed_state = grammar_trace.states[run_index];
1828    let confirmed_reason = grammar_trace.reasons[run_index];
1829    if confirmed_state == GrammarState::Admissible {
1830        if run_index > 0 && grammar_trace.states[run_index - 1] != GrammarState::Admissible {
1831            ScaffoldGrammarState::Recovery
1832        } else {
1833            ScaffoldGrammarState::Admissible
1834        }
1835    } else if grammar_trace.persistent_violation[run_index] {
1836        ScaffoldGrammarState::PersistentViolation
1837    } else if confirmed_state == GrammarState::Violation
1838        || confirmed_reason == GrammarReason::EnvelopeViolation
1839    {
1840        ScaffoldGrammarState::TransientViolation
1841    } else if confirmed_reason == GrammarReason::SustainedOutwardDrift {
1842        ScaffoldGrammarState::SustainedOutwardDrift
1843    } else if confirmed_reason == GrammarReason::RecurrentBoundaryGrazing {
1844        ScaffoldGrammarState::BoundaryGrazing
1845    } else if confirmed_reason == GrammarReason::AbruptSlewViolation {
1846        ScaffoldGrammarState::TransientViolation
1847    } else {
1848        ScaffoldGrammarState::BoundaryGrazing
1849    }
1850}
1851
1852#[derive(Debug, Clone, Copy)]
1853struct ExpandedHeuristicMetadata {
1854    allowed_grammar_states: &'static str,
1855    role_class: &'static str,
1856    feature_scope: &'static str,
1857    ambiguity_note: &'static str,
1858    rescue_eligibility_guidance: &'static str,
1859    burden_contribution_class: &'static str,
1860}
1861
1862fn expanded_heuristic_entry_metadata(heuristic_name: &str) -> ExpandedHeuristicMetadata {
1863    match heuristic_name {
1864        PRE_FAILURE_SLOW_DRIFT => ExpandedHeuristicMetadata {
1865            allowed_grammar_states: "sustained_outward_drift,persistent_violation",
1866            role_class: "primary_precursor",
1867            feature_scope: "S059 plus S133 only when failure-localized slow drift support is present",
1868            ambiguity_note: "Slow drift remains interpretive and does not identify a unique mechanism.",
1869            rescue_eligibility_guidance: "Rescue-eligible on primary precursor features only.",
1870            burden_contribution_class: "review_burden_candidate",
1871        },
1872        RECURRENT_BOUNDARY_APPROACH => ExpandedHeuristicMetadata {
1873            allowed_grammar_states: "boundary_grazing,sustained_outward_drift",
1874            role_class: "precursor_or_support",
1875            feature_scope: "S059 and compatible recurrent-boundary precursor features",
1876            ambiguity_note: "Repeated boundary approach is structurally meaningful but not uniquely causal.",
1877            rescue_eligibility_guidance: "Use for bounded Review promotion only when persistence is visible.",
1878            burden_contribution_class: "watch_to_review_pressure",
1879        },
1880        TRANSITION_EXCURSION => ExpandedHeuristicMetadata {
1881            allowed_grammar_states: "transient_violation,persistent_violation",
1882            role_class: "transition_instability",
1883            feature_scope: "S123 and compatible transition-instability features",
1884            ambiguity_note: "Transition excursions indicate abrupt structural change, not root-cause identity.",
1885            rescue_eligibility_guidance: "Rescue-eligible for transition features with persistent grammar support.",
1886            burden_contribution_class: "escalation_candidate",
1887        },
1888        PERSISTENT_INSTABILITY_CLUSTER => ExpandedHeuristicMetadata {
1889            allowed_grammar_states: "sustained_outward_drift,persistent_violation",
1890            role_class: "instability_cluster",
1891            feature_scope: "S123 and grouped instability clusters with sustained pressure",
1892            ambiguity_note: "Persistent clusters support detectability more strongly than identifiability.",
1893            rescue_eligibility_guidance: "Use to justify escalation only with grouped corroboration.",
1894            burden_contribution_class: "review_or_escalate_cluster",
1895        },
1896        TRANSITION_CLUSTER_SUPPORT => ExpandedHeuristicMetadata {
1897            allowed_grammar_states: "boundary_grazing,sustained_outward_drift,transient_violation",
1898            role_class: "secondary_corroborator",
1899            feature_scope: "S540,S128 and other corroborating support features",
1900            ambiguity_note: "Support motifs indicate alignment with a primary precursor, not a standalone alarm.",
1901            rescue_eligibility_guidance: "Never rescue to escalation from support features alone.",
1902            burden_contribution_class: "corroboration_support",
1903        },
1904        WATCH_ONLY_BOUNDARY_GRAZING => ExpandedHeuristicMetadata {
1905            allowed_grammar_states: "boundary_grazing",
1906            role_class: "sentinel",
1907            feature_scope: "S104 and low-amplitude sentinel features",
1908            ambiguity_note: "Boundary grazing is deliberately treated as semantically ambiguous and low-confidence.",
1909            rescue_eligibility_guidance: "Not rescue-eligible; retain as watch-only.",
1910            burden_contribution_class: "watch_only_burden",
1911        },
1912        _ => ExpandedHeuristicMetadata {
1913            allowed_grammar_states: "grammar_filtered",
1914            role_class: "generic",
1915            feature_scope: "all_features",
1916            ambiguity_note: "Structural semantics remain non-unique.",
1917            rescue_eligibility_guidance: "Apply only after grammar filtering.",
1918            burden_contribution_class: "generic",
1919        },
1920    }
1921}
1922
1923fn semantic_regime_conditions(heuristic_name: &str) -> &'static str {
1924    match heuristic_name {
1925        PRE_FAILURE_SLOW_DRIFT => {
1926            "persistent signed drift, moderate residual growth, and limited abrupt slew"
1927        }
1928        RECURRENT_BOUNDARY_APPROACH => {
1929            "repeated boundary proximity with outward tendency and bounded fragmentation"
1930        }
1931        TRANSITION_EXCURSION => {
1932            "elevated slew burst aligned with a grammar transition or violation onset"
1933        }
1934        PERSISTENT_INSTABILITY_CLUSTER => {
1935            "repeated outward grammar pressure that is not reducible to isolated spikes"
1936        }
1937        TRANSITION_CLUSTER_SUPPORT => {
1938            "corroborating burst or pressure feature aligned with a grouped primary feature"
1939        }
1940        WATCH_ONLY_BOUNDARY_GRAZING => {
1941            "boundary proximity without sufficient persistence or corroboration for Review"
1942        }
1943        _ => "deterministic structural regime",
1944    }
1945}
1946
1947fn semantic_applicability_rules(heuristic_name: &str) -> &'static str {
1948    match heuristic_name {
1949        PRE_FAILURE_SLOW_DRIFT => {
1950            "filter by grammar first, then apply only on slow structural precursor features"
1951        }
1952        RECURRENT_BOUNDARY_APPROACH => {
1953            "filter by grammar first, then apply on recurrent boundary-pressure features"
1954        }
1955        TRANSITION_EXCURSION => "filter by grammar first, then apply on abrupt transition features",
1956        PERSISTENT_INSTABILITY_CLUSTER => {
1957            "filter by grammar first, then require sustained grouped pressure"
1958        }
1959        TRANSITION_CLUSTER_SUPPORT => {
1960            "filter by grammar first, then require corroborator scope compatibility"
1961        }
1962        WATCH_ONLY_BOUNDARY_GRAZING => {
1963            "filter by grammar first, then confine to watch-only sentinel handling"
1964        }
1965        _ => "apply only after grammar filtering",
1966    }
1967}
1968
1969fn compute_structural_delta_metrics(
1970    residuals: &ResidualSet,
1971    grammar: &GrammarSet,
1972    semantic_matches: &[SemanticMatchRecord],
1973    failure_window_mask: &[bool],
1974) -> StructuralDeltaMetrics {
1975    let total_violation_points = grammar
1976        .traces
1977        .iter()
1978        .flat_map(|trace| trace.raw_states.iter().copied().enumerate())
1979        .filter(|(_, state)| *state == GrammarState::Violation)
1980        .count();
1981    let pre_failure_violation_points = grammar
1982        .traces
1983        .iter()
1984        .flat_map(|trace| trace.raw_states.iter().copied().enumerate())
1985        .filter(|(run_index, state)| {
1986            *state == GrammarState::Violation && failure_window_mask[*run_index]
1987        })
1988        .count();
1989    let grammar_violation_precision = (total_violation_points > 0)
1990        .then_some(pre_failure_violation_points as f64 / total_violation_points as f64);
1991
1992    let motif_precision_pre_failure = if semantic_matches.is_empty() {
1993        None
1994    } else {
1995        Some(
1996            semantic_matches
1997                .iter()
1998                .filter(|row| failure_window_mask[row.run_index])
1999                .count() as f64
2000                / semantic_matches.len() as f64,
2001        )
2002    };
2003
2004    let failure_window_points = failure_window_mask.iter().filter(|flag| **flag).count();
2005    let pass_window_points = failure_window_mask
2006        .len()
2007        .saturating_sub(failure_window_points);
2008    let failure_semantic_hits = semantic_matches
2009        .iter()
2010        .filter(|row| failure_window_mask[row.run_index])
2011        .count();
2012    let pass_semantic_hits = semantic_matches
2013        .iter()
2014        .filter(|row| !failure_window_mask[row.run_index])
2015        .count();
2016    let failure_semantic_density = (failure_window_points > 0).then_some(
2017        failure_semantic_hits as f64
2018            / (failure_window_points * residuals.traces.len().max(1)) as f64,
2019    );
2020    let pass_semantic_density = (pass_window_points > 0).then_some(
2021        pass_semantic_hits as f64 / (pass_window_points * residuals.traces.len().max(1)) as f64,
2022    );
2023    let structural_separation = failure_semantic_density
2024        .zip(pass_semantic_density)
2025        .map(|(failure, pass)| failure - pass);
2026
2027    let precursor_stability = if semantic_matches.is_empty() {
2028        None
2029    } else {
2030        let mut grouped = BTreeMap::<(&str, usize), Vec<usize>>::new();
2031        for row in semantic_matches {
2032            grouped
2033                .entry((row.heuristic_name.as_str(), row.feature_index))
2034                .or_default()
2035                .push(row.run_index);
2036        }
2037        let mut matched_pre_failure_points = 0usize;
2038        let mut stable_pre_failure_points = 0usize;
2039        for runs in grouped.values_mut() {
2040            runs.sort_unstable();
2041            let mut episode_len = 0usize;
2042            let mut previous: Option<usize> = None;
2043            for &run_index in runs.iter() {
2044                if previous.is_some_and(|previous| run_index == previous + 1) {
2045                    episode_len += 1;
2046                } else {
2047                    episode_len = 1;
2048                }
2049                if failure_window_mask[run_index] {
2050                    matched_pre_failure_points += 1;
2051                    if episode_len >= 2 {
2052                        stable_pre_failure_points += 1;
2053                    }
2054                }
2055                previous = Some(run_index);
2056            }
2057        }
2058        (matched_pre_failure_points > 0)
2059            .then_some(stable_pre_failure_points as f64 / matched_pre_failure_points as f64)
2060    };
2061
2062    StructuralDeltaMetrics {
2063        grammar_violation_precision,
2064        motif_precision_pre_failure,
2065        structural_separation,
2066        precursor_stability,
2067        episode_precision: None,
2068        compression_ratio: None,
2069    }
2070}
2071
2072fn failure_window_mask(
2073    run_count: usize,
2074    labels: &[i8],
2075    pre_failure_lookback_runs: usize,
2076) -> Vec<bool> {
2077    let failure_indices = labels
2078        .iter()
2079        .enumerate()
2080        .filter_map(|(index, label)| (*label == 1).then_some(index))
2081        .collect::<Vec<_>>();
2082    let mut mask = vec![false; run_count];
2083    for failure_index in failure_indices {
2084        let start = failure_index.saturating_sub(pre_failure_lookback_runs);
2085        for slot in &mut mask[start..failure_index] {
2086            *slot = true;
2087        }
2088    }
2089    mask
2090}
2091
2092fn mean(values: &[f64]) -> Option<f64> {
2093    (!values.is_empty()).then_some(values.iter().sum::<f64>() / values.len() as f64)
2094}
2095
2096fn mean_of_records<T>(records: &[T], project: impl Fn(&T) -> f64) -> f64 {
2097    if records.is_empty() {
2098        0.0
2099    } else {
2100        records.iter().map(project).sum::<f64>() / records.len() as f64
2101    }
2102}
2103
2104#[cfg(test)]
2105mod tests {
2106    use super::*;
2107
2108    #[test]
2109    fn semantic_candidates_are_grammar_conditioned() {
2110        let candidates = semantic_candidates_for_run(
2111            "S059",
2112            ScaffoldGrammarState::SustainedOutwardDrift,
2113            GrammarReason::SustainedOutwardDrift,
2114            DsfbMotifClass::PreFailureSlowDrift,
2115        );
2116        assert_eq!(candidates, vec![PRE_FAILURE_SLOW_DRIFT]);
2117
2118        let candidates = semantic_candidates_for_run(
2119            "S104",
2120            ScaffoldGrammarState::Admissible,
2121            GrammarReason::Admissible,
2122            DsfbMotifClass::StableAdmissible,
2123        );
2124        assert!(candidates.is_empty());
2125    }
2126
2127    #[test]
2128    fn scaffold_feature_specs_are_deterministic() {
2129        let s059 = feature_scaffold_spec("S059").unwrap();
2130        assert_eq!(s059.role, "primary recurrent-boundary precursor");
2131        assert_eq!(s059.default_policy_ceiling, HeuristicAlertClass::Review);
2132        assert_eq!(s059.preferred_motifs[0], RECURRENT_BOUNDARY_APPROACH);
2133
2134        let s104 = feature_scaffold_spec("S104").unwrap();
2135        assert_eq!(s104.default_policy_ceiling, HeuristicAlertClass::Watch);
2136        assert_eq!(s104.rescue_priority, "none");
2137
2138        let s134 = feature_scaffold_spec("S134").unwrap();
2139        assert_eq!(s134.role, "recall-rescue feature");
2140        assert_eq!(s134.group_name, "ungrouped");
2141    }
2142}