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}