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 pub residual: f64,
35 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#[derive(Debug, Clone, Serialize)]
55pub struct DsfbRunManifest {
56 pub software_version: String,
58 pub run_timestamp: String,
60 pub uom_scales: UomScales,
62 pub process_context_tag: String,
65 pub traceability_entry_count: usize,
67 pub trace_chain: &'static str,
69 pub integration_mode: &'static str,
71 pub missingness_summary: Option<serde_json::Value>,
73}
74
75impl DsfbRunManifest {
76 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 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}