Skip to main content

dsfb_semiconductor/
traceability.rs

1use crate::error::Result;
2use crate::semiotics::{
3    FeatureGrammarStateRecord, FeatureMotifTimelineRecord, FeatureSignRecord,
4    ScaffoldSemioticsArtifacts,
5};
6use crate::units::UomScales;
7use serde::Serialize;
8use std::collections::BTreeMap;
9use std::path::Path;
10
11const TRACE_CHAIN: &str = "Residual -> Sign -> Motif -> Grammar -> Semantic -> Policy";
12const INTEGRATION_MODE: &str = "read_only_side_channel";
13
14#[derive(Debug, Clone, Serialize, PartialEq)]
15pub struct TraceabilitySign {
16    pub normalized_residual: f64,
17    pub drift: f64,
18    pub slew: f64,
19    pub normalized_residual_norm: f64,
20    pub sigma_norm: f64,
21    pub is_imputed: bool,
22}
23
24#[derive(Debug, Clone, Serialize, PartialEq)]
25pub struct TraceabilityEntry {
26    pub event_id: String,
27    pub features: Vec<String>,
28    pub feature_role: String,
29    pub group_name: String,
30    pub run_index: usize,
31    pub timestamp: String,
32    pub label: i8,
33    /// Primary residual scalar (normalized). Kept for backward compatibility.
34    pub residual: f64,
35    /// Full residual value vector for this event (one entry per feature in `features`).
36    /// Use this field when iterating over multi-feature episodes.
37    pub residual_values: Vec<f64>,
38    pub sign: TraceabilitySign,
39    pub motif: String,
40    pub grammar: String,
41    pub semantic: String,
42    pub policy: String,
43    pub rationale: String,
44    pub chain: String,
45    pub integration_mode: String,
46}
47
48// ─── Run Manifest (dsfb_run_manifest.json) ────────────────────────────────────
49
50/// The complete run manifest emitted as `dsfb_run_manifest.json` for every
51/// DSFB run.  This satisfies the "Audit Trail" requirement: every alarm can
52/// be traced back to the software version, unit conventions, and process
53/// context used during the run.
54#[derive(Debug, Clone, Serialize)]
55pub struct DsfbRunManifest {
56    /// Crate semver version string.
57    pub software_version: String,
58    /// ISO-8601 timestamp at which the run was initiated.
59    pub run_timestamp: String,
60    /// Physical unit conventions used during this run.
61    pub uom_scales: UomScales,
62    /// Process context active at run start (best-effort; may be empty for
63    /// batch runs without recipe-step metadata).
64    pub process_context_tag: String,
65    /// Total number of traceability entries emitted.
66    pub traceability_entry_count: usize,
67    /// The trace chain string for documentation.
68    pub trace_chain: &'static str,
69    /// Integration mode — always `"read_only_side_channel"`.
70    pub integration_mode: &'static str,
71    /// Abstract summary of missingness across the run.
72    pub missingness_summary: Option<serde_json::Value>,
73}
74
75impl DsfbRunManifest {
76    /// Construct a manifest for a completed DSFB run.
77    pub fn new(
78        run_timestamp: String,
79        process_context_tag: String,
80        traceability_entry_count: usize,
81    ) -> Self {
82        Self {
83            software_version: env!("CARGO_PKG_VERSION").to_string(),
84            run_timestamp,
85            uom_scales: UomScales::default(),
86            process_context_tag,
87            traceability_entry_count,
88            trace_chain: TRACE_CHAIN,
89            integration_mode: INTEGRATION_MODE,
90            missingness_summary: None,
91        }
92    }
93
94    /// Write the manifest to `path` as pretty-printed JSON.
95    pub fn write(&self, path: &Path) -> Result<()> {
96        let file = std::fs::File::create(path)?;
97        serde_json::to_writer_pretty(file, self)?;
98        Ok(())
99    }
100}
101
102pub fn build_traceability_entries(
103    scaffold: &ScaffoldSemioticsArtifacts,
104) -> Vec<TraceabilityEntry> {
105    let sign_rows = index_signs(&scaffold.feature_signs);
106    let motif_rows = index_motifs(&scaffold.feature_motif_timeline);
107    let grammar_rows = index_grammar(&scaffold.feature_grammar_states);
108
109    let mut entries = scaffold
110        .feature_policy_decisions
111        .iter()
112        .filter(|row| {
113            row.investigation_worthy
114                || row.semantic_label.is_some()
115                || row.policy_state != "Silent"
116                || row.grammar_state != "admissible"
117        })
118        .filter_map(|policy_row| {
119            let key = (policy_row.feature_name.as_str(), policy_row.run_index);
120            let sign_row = sign_rows.get(&key)?;
121            let motif_row = motif_rows.get(&key)?;
122            let grammar_row = grammar_rows.get(&key)?;
123            Some(TraceabilityEntry {
124                event_id: format!(
125                    "{}:{}:{}",
126                    policy_row.feature_name,
127                    policy_row.run_index,
128                    policy_row.policy_state.to_lowercase()
129                ),
130                features: vec![policy_row.feature_name.clone()],
131                feature_role: policy_row.feature_role.clone(),
132                group_name: policy_row.group_name.clone(),
133                run_index: policy_row.run_index,
134                timestamp: policy_row.timestamp.clone(),
135                label: policy_row.label,
136                residual: sign_row.normalized_residual,
137                residual_values: vec![sign_row.normalized_residual],
138                sign: TraceabilitySign {
139                    normalized_residual: sign_row.normalized_residual,
140                    drift: sign_row.drift,
141                    slew: sign_row.slew,
142                    normalized_residual_norm: sign_row.normalized_residual_norm,
143                    sigma_norm: sign_row.sigma_norm,
144                    is_imputed: sign_row.is_imputed,
145                },
146                motif: motif_row.motif_label.clone(),
147                grammar: grammar_row.grammar_state.clone(),
148                semantic: policy_row
149                    .semantic_label
150                    .clone()
151                    .unwrap_or_else(|| "no_semantic_match".into()),
152                policy: policy_row.policy_state.clone(),
153                rationale: policy_row.rationale.clone(),
154                chain: TRACE_CHAIN.into(),
155                integration_mode: INTEGRATION_MODE.into(),
156            })
157        })
158        .collect::<Vec<_>>();
159
160    entries.sort_by(|left, right| {
161        left.run_index
162            .cmp(&right.run_index)
163            .then_with(|| left.features[0].cmp(&right.features[0]))
164            .then_with(|| left.event_id.cmp(&right.event_id))
165    });
166    entries
167}
168
169pub fn write_traceability_json(path: &Path, entries: &[TraceabilityEntry]) -> Result<()> {
170    let file = std::fs::File::create(path)?;
171    serde_json::to_writer_pretty(file, entries)?;
172    Ok(())
173}
174
175fn index_signs(rows: &[FeatureSignRecord]) -> BTreeMap<(&str, usize), &FeatureSignRecord> {
176    rows.iter()
177        .map(|row| ((row.feature_name.as_str(), row.run_index), row))
178        .collect()
179}
180
181fn index_motifs(
182    rows: &[FeatureMotifTimelineRecord],
183) -> BTreeMap<(&str, usize), &FeatureMotifTimelineRecord> {
184    rows.iter()
185        .map(|row| ((row.feature_name.as_str(), row.run_index), row))
186        .collect()
187}
188
189fn index_grammar(
190    rows: &[FeatureGrammarStateRecord],
191) -> BTreeMap<(&str, usize), &FeatureGrammarStateRecord> {
192    rows.iter()
193        .map(|row| ((row.feature_name.as_str(), row.run_index), row))
194        .collect()
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::semiotics::{
201        FeatureGrammarStateRecord, FeatureMotifTimelineRecord, FeaturePolicyDecisionRecord,
202        FeatureSignRecord, ScaffoldSemioticsArtifacts,
203    };
204
205    fn empty_scaffold() -> ScaffoldSemioticsArtifacts {
206        ScaffoldSemioticsArtifacts {
207            feature_signs: Vec::new(),
208            feature_motif_timeline: Vec::new(),
209            feature_grammar_states: Vec::new(),
210            envelope_interaction_summary: Vec::new(),
211            heuristics_bank_expanded: Vec::new(),
212            feature_policy_decisions: Vec::new(),
213            group_definitions: Vec::new(),
214            group_signs: Vec::new(),
215            group_grammar_states: Vec::new(),
216            group_semantic_matches: Vec::new(),
217        }
218    }
219
220    #[test]
221    fn traceability_entries_preserve_full_chain() {
222        let mut scaffold = empty_scaffold();
223        scaffold.feature_signs.push(FeatureSignRecord {
224            feature_index: 59,
225            feature_name: "S059".into(),
226            feature_role: "primary recurrent-boundary precursor".into(),
227            group_name: "group_a".into(),
228            run_index: 11,
229            timestamp: "2008-01-01T00:11:00Z".into(),
230            label: 1,
231            normalized_residual: 1.8,
232            drift: 0.3,
233            slew: 0.1,
234            normalized_residual_norm: 1.8,
235            sigma_norm: 1.0,
236            is_imputed: false,
237        });
238        scaffold
239            .feature_motif_timeline
240            .push(FeatureMotifTimelineRecord {
241                feature_index: 59,
242                feature_name: "S059".into(),
243                feature_role: "primary recurrent-boundary precursor".into(),
244                group_name: "group_a".into(),
245                run_index: 11,
246                timestamp: "2008-01-01T00:11:00Z".into(),
247                label: 1,
248                motif_label: "slow_drift_precursor".into(),
249            });
250        scaffold
251            .feature_grammar_states
252            .push(FeatureGrammarStateRecord {
253                feature_index: 59,
254                feature_name: "S059".into(),
255                feature_role: "primary recurrent-boundary precursor".into(),
256                group_name: "group_a".into(),
257                run_index: 11,
258                timestamp: "2008-01-01T00:11:00Z".into(),
259                label: 1,
260                grammar_state: "SustainedDrift".into(),
261                raw_state: "Boundary".into(),
262                confirmed_state: "Boundary".into(),
263                raw_reason: "SustainedOutwardDrift".into(),
264                confirmed_reason: "SustainedOutwardDrift".into(),
265                normalized_envelope_ratio: 0.82,
266                persistent_boundary: true,
267                persistent_violation: false,
268                suppressed_by_imputation: false,
269            });
270        scaffold
271            .feature_policy_decisions
272            .push(FeaturePolicyDecisionRecord {
273                feature_index: 59,
274                feature_name: "S059".into(),
275                feature_role: "primary recurrent-boundary precursor".into(),
276                group_name: "group_a".into(),
277                run_index: 11,
278                timestamp: "2008-01-01T00:11:00Z".into(),
279                label: 1,
280                grammar_state: "SustainedDrift".into(),
281                motif_label: "slow_drift_precursor".into(),
282                semantic_label: Some("pre-failure cluster".into()),
283                policy_ceiling: "Escalate".into(),
284                policy_state: "Escalate".into(),
285                investigation_worthy: true,
286                corroborated: true,
287                corroborated_by: "S133".into(),
288                rationale: "persistent outward drift with corroboration".into(),
289            });
290
291        let entries = build_traceability_entries(&scaffold);
292        assert_eq!(entries.len(), 1);
293        assert_eq!(entries[0].features, vec!["S059".to_string()]);
294        assert_eq!(entries[0].motif, "slow_drift_precursor");
295        assert_eq!(entries[0].grammar, "SustainedDrift");
296        assert_eq!(entries[0].semantic, "pre-failure cluster");
297        assert_eq!(entries[0].policy, "Escalate");
298        assert_eq!(entries[0].chain, TRACE_CHAIN);
299        assert_eq!(entries[0].integration_mode, INTEGRATION_MODE);
300    }
301}