Skip to main content

dsfb_semiconductor/policy/
mod.rs

1#[cfg(feature = "std")]
2use crate::error::Result;
3use crate::grammar::layer::GrammarState;
4use crate::semantics::SemanticMatch;
5use serde::{Deserialize, Serialize};
6#[cfg(feature = "std")]
7use std::collections::BTreeMap;
8#[cfg(not(feature = "std"))]
9use alloc::{collections::BTreeMap, string::{String, ToString}, vec::Vec};
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct PolicyDecision {
13    pub timestamp: f64,
14    pub decision: String,
15}
16
17pub fn derive_policy(
18    semantic_matches: &[SemanticMatch],
19    grammar_states: &[GrammarState],
20) -> Vec<PolicyDecision> {
21    let mut by_timestamp = BTreeMap::<u64, String>::new();
22
23    for state in grammar_states {
24        let fallback = if state.state == "Admissible" {
25            "Silent"
26        } else {
27            "Watch"
28        };
29        by_timestamp
30            .entry(state.timestamp.to_bits())
31            .and_modify(|current| {
32                if decision_rank(fallback) > decision_rank(current) {
33                    *current = fallback.to_string();
34                }
35            })
36            .or_insert_with(|| fallback.to_string());
37    }
38
39    for semantic in semantic_matches {
40        by_timestamp
41            .entry(semantic.timestamp.to_bits())
42            .and_modify(|current| {
43                if decision_rank(&semantic.action) > decision_rank(current) {
44                    *current = semantic.action.clone();
45                }
46            })
47            .or_insert_with(|| semantic.action.clone());
48    }
49
50    by_timestamp
51        .into_iter()
52        .map(|(timestamp_bits, decision)| PolicyDecision {
53            timestamp: f64::from_bits(timestamp_bits),
54            decision,
55        })
56        .collect()
57}
58
59#[cfg(feature = "std")]
60pub fn write_policy_decisions_csv(
61    path: &std::path::Path,
62    rows: &[PolicyDecision],
63) -> Result<()> {
64    let mut writer = csv::Writer::from_path(path)?;
65    for row in rows {
66        writer.serialize(row)?;
67    }
68    writer.flush()?;
69    Ok(())
70}
71
72fn decision_rank(decision: &str) -> usize {
73    match decision {
74        "Escalate" => 3,
75        "Review" => 2,
76        "Watch" => 1,
77        _ => 0,
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn policy_uses_semantics_and_grammar_only() {
87        let grammar = vec![
88            GrammarState {
89                feature_id: "S059".into(),
90                state: "BoundaryGrazing".into(),
91                timestamp: 1.0,
92            },
93            GrammarState {
94                feature_id: "S059".into(),
95                state: "Admissible".into(),
96                timestamp: 2.0,
97            },
98        ];
99        let semantics = vec![SemanticMatch {
100            timestamp: 1.0,
101            feature_id: "S059".into(),
102            heuristic_id: "failure_slow_drift_review".into(),
103            motif_type: "slow_drift_precursor".into(),
104            grammar_state: "SustainedDrift".into(),
105            action: "Review".into(),
106        }];
107        let decisions = derive_policy(&semantics, &grammar);
108        assert_eq!(decisions[0].decision, "Review");
109        assert_eq!(decisions[1].decision, "Silent");
110    }
111}