Skip to main content

dsfb_semiconductor/
signature.rs

1//! Schema-validated JSON signature format for DSFB heuristics and motifs.
2//!
3//! # Digital Twin Fragments
4//! A `.dsfb` signature file is a portable, schema-validated JSON document
5//! that encodes exactly what a particular failure mode (e.g., "Target
6//! Depletion" or "RF Matching Drift") looks like in DSFB semiotic space.
7//!
8//! Tool vendors can ship a signature file that references:
9//! * The motif names that constitute the failure signature.
10//! * The grammar state sequence expected during the failure episode.
11//! * The physical sensors (by dimension tag) that carry the drift signal.
12//! * The recommended operator action and escalation policy.
13//!
14//! The DSFB engine can then load these signatures at runtime and extend
15//! the heuristics bank without recompilation.
16//!
17//! # Schema Version
18//! Every signature file must declare a `schema_version` field.  The current
19//! schema version is `"1.0"`.  The engine rejects files with unknown
20//! schema versions at load time.
21
22use crate::error::DsfbSemiconductorError;
23use serde::{Deserialize, Serialize};
24use std::path::Path;
25
26/// The schema version string for the current signature format.
27pub const SIGNATURE_SCHEMA_VERSION: &str = "1.0";
28
29// ─── Motif Signature ──────────────────────────────────────────────────────────
30
31/// A named, serialisable motif entry suitable for embedding in a `.dsfb`
32/// signature file.
33///
34/// # Schema Compatibility
35/// This struct is the canonical serialisation target; do not add non-optional
36/// fields without bumping `schema_version`.
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub struct DsfbMotifSignature {
39    /// Unique motif identifier — must match a value in
40    /// [`ALLOWED_MOTIFS`](crate::syntax::ALLOWED_MOTIFS) or be a custom
41    /// extension prefixed with `"custom_"`.
42    pub motif_id: String,
43    /// Human-readable description for operator-facing displays.
44    pub description: String,
45    /// Sequence of expected motif labels in temporal order (time → right).
46    pub motif_sequence: Vec<String>,
47    /// Grammar states that must be active concurrently with this motif.
48    pub required_grammar_states: Vec<String>,
49    /// Physical sensor dimension tags (e.g., `"sccm"`, `"milli_torr"`) that
50    /// are expected to carry the signal.  [`None`] means "any dimension".
51    pub expected_dimensions: Option<Vec<String>>,
52    /// Minimum number of consecutive runs the motif must persist before
53    /// this signature is considered matched.
54    pub minimum_persistence_runs: usize,
55}
56
57// ─── Heuristics Bank Entry ────────────────────────────────────────────────────
58
59/// A single entry in a serialisable, schema-validated heuristics bank.
60///
61/// This is the portable unit of knowledge: a tool vendor can ship a JSON
62/// array of these entries as a `.dsfb` signature file.
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
64pub struct DsfbHeuristicSignature {
65    /// Unique identifier for this heuristic.
66    pub heuristic_id: String,
67    /// Human-readable name (e.g., `"Target Depletion"`).
68    pub name: String,
69    /// Engineering description — appears in operator dashboards and audit
70    /// trails.
71    pub description: String,
72    /// Motif signatures that compose this heuristic.
73    pub motif_signatures: Vec<DsfbMotifSignature>,
74    /// Recommended operator action on match: `"Monitor"`, `"Watch"`,
75    /// `"Review"`, or `"Escalate"`.
76    pub action: String,
77    /// Escalation policy if the action is not resolved within the
78    /// `escalation_timeout_runs` window.
79    pub escalation_policy: String,
80    /// Number of runs after initial match before auto-escalation.
81    pub escalation_timeout_runs: usize,
82    /// Whether this heuristic requires corroboration from ≥2 sensors.
83    pub requires_corroboration: bool,
84    /// Attribution / provenance: who authored this signature.
85    pub author: Option<String>,
86    /// Semantic label emitted in the traceability manifest on match.
87    pub semantic_label: String,
88    /// Known limitations for this heuristic.
89    pub known_limitations: Option<String>,
90}
91
92// ─── Signature File ───────────────────────────────────────────────────────────
93
94/// A complete `.dsfb` signature file: a schema-versioned bundle of heuristic
95/// signatures ready for runtime loading.
96///
97/// # Example
98/// ```
99/// use dsfb_semiconductor::signature::{DsfbSignatureFile, SIGNATURE_SCHEMA_VERSION};
100///
101/// let file = DsfbSignatureFile {
102///     schema_version: SIGNATURE_SCHEMA_VERSION.into(),
103///     tool_class: "ICP Etch".into(),
104///     vendor: Some("Example Semiconductor Equipment Inc.".into()),
105///     heuristics: vec![],
106/// };
107///
108/// let json = serde_json::to_string_pretty(&file).unwrap();
109/// assert!(json.contains("schema_version"));
110/// assert!(json.contains("1.0"));
111/// ```
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
113pub struct DsfbSignatureFile {
114    /// Schema version — must be `"1.0"` for this crate version.
115    pub schema_version: String,
116    /// Broad class of tool this signature targets (e.g., `"ICP Etch"`,
117    /// `"PECVD"`, `"CMP"`).
118    pub tool_class: String,
119    /// Optional vendor attribution.
120    pub vendor: Option<String>,
121    /// The heuristic entries in this signature file.
122    pub heuristics: Vec<DsfbHeuristicSignature>,
123}
124
125impl DsfbSignatureFile {
126    /// Validate schema version and structural constraints.
127    ///
128    /// Returns `Err` with a descriptive message if validation fails.
129    pub fn validate(&self) -> Result<(), DsfbSemiconductorError> {
130        if self.schema_version != SIGNATURE_SCHEMA_VERSION {
131            return Err(DsfbSemiconductorError::Config(format!(
132                "unsupported signature schema version '{}'; expected '{}'",
133                self.schema_version, SIGNATURE_SCHEMA_VERSION
134            )));
135        }
136
137        if self.tool_class.trim().is_empty() {
138            return Err(DsfbSemiconductorError::Config(
139                "tool_class must not be empty".into(),
140            ));
141        }
142
143        for h in &self.heuristics {
144            if h.heuristic_id.trim().is_empty() {
145                return Err(DsfbSemiconductorError::Config(
146                    "heuristic_id must not be empty".into(),
147                ));
148            }
149            let valid_actions = ["Monitor", "Watch", "Review", "Escalate"];
150            if !valid_actions.contains(&h.action.as_str()) {
151                return Err(DsfbSemiconductorError::Config(format!(
152                    "heuristic '{}': action '{}' is not in {:?}",
153                    h.heuristic_id, h.action, valid_actions
154                )));
155            }
156            for motif in &h.motif_signatures {
157                if motif.minimum_persistence_runs == 0 {
158                    return Err(DsfbSemiconductorError::Config(format!(
159                        "heuristic '{}' motif '{}': minimum_persistence_runs must be > 0",
160                        h.heuristic_id, motif.motif_id
161                    )));
162                }
163            }
164        }
165
166        Ok(())
167    }
168
169    /// Load and validate a signature file from disk.
170    ///
171    /// # Errors
172    /// Returns [`DsfbSemiconductorError`] on I/O failure, JSON parse failure,
173    /// or schema validation failure.
174    pub fn load(path: &Path) -> Result<Self, DsfbSemiconductorError> {
175        let content = std::fs::read_to_string(path)
176            .map_err(DsfbSemiconductorError::Io)?;
177        let file: Self = serde_json::from_str(&content)
178            .map_err(|e| DsfbSemiconductorError::Config(format!("JSON parse error: {e}")))?;
179        file.validate()?;
180        Ok(file)
181    }
182
183    /// Serialise to a pretty-printed JSON string.
184    pub fn to_json_pretty(&self) -> Result<String, DsfbSemiconductorError> {
185        serde_json::to_string_pretty(self)
186            .map_err(|e| DsfbSemiconductorError::Config(format!("JSON serialise error: {e}")))
187    }
188
189    /// Write to a file path.
190    pub fn write(&self, path: &Path) -> Result<(), DsfbSemiconductorError> {
191        let json = self.to_json_pretty()?;
192        std::fs::write(path, json)
193            .map_err(DsfbSemiconductorError::Io)
194    }
195
196    /// Return a reference signature for the "Target Depletion" failure mode.
197    /// This can be shipped as an example `.dsfb` file to tool vendors.
198    pub fn example_target_depletion() -> Self {
199        Self {
200            schema_version: SIGNATURE_SCHEMA_VERSION.into(),
201            tool_class: "ICP Etch".into(),
202            vendor: Some("reference_dsfb_v1".into()),
203            heuristics: vec![DsfbHeuristicSignature {
204                heuristic_id: "target_depletion_v1".into(),
205                name: "Target Depletion (Sputter Source)".into(),
206                description: concat!(
207                    "Slow, monotonic drift of the gas-flow residual toward the ",
208                    "admissibility boundary, co-occurring with a matching pressure ",
209                    "drift in the opposite direction.  Signature of consumable ",
210                    "target erosion in sputter-based etch chambers."
211                )
212                .into(),
213                motif_signatures: vec![DsfbMotifSignature {
214                    motif_id: "slow_drift_precursor".into(),
215                    description: "Monotonic positive drift approaching ρ".into(),
216                    motif_sequence: vec![
217                        "slow_drift_precursor".into(),
218                        "boundary_grazing".into(),
219                        "persistent_instability".into(),
220                    ],
221                    required_grammar_states: vec![
222                        "SustainedDrift".into(),
223                        "BoundaryGrazing".into(),
224                        "PersistentViolation".into(),
225                    ],
226                    expected_dimensions: Some(vec!["sccm".into(), "milli_torr".into()]),
227                    minimum_persistence_runs: 5,
228                }],
229                action: "Review".into(),
230                escalation_policy: "Escalate if motif persists for > 25 runs without recovery".into(),
231                escalation_timeout_runs: 25,
232                requires_corroboration: true,
233                author: Some("DSFB Reference Library v1.0".into()),
234                semantic_label: "target_depletion".into(),
235                known_limitations: Some(concat!(
236                    "False positives possible when gas composition changes due to ",
237                    "recipe parameter sweep; gate with ProcessContext.recipe_step.",
238                ).into()),
239            }],
240        }
241    }
242}
243
244// ─── Unit tests ───────────────────────────────────────────────────────────────
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn example_signature_is_valid() {
252        let sig = DsfbSignatureFile::example_target_depletion();
253        sig.validate().expect("example signature should be valid");
254    }
255
256    #[test]
257    fn wrong_schema_version_is_rejected() {
258        let mut sig = DsfbSignatureFile::example_target_depletion();
259        sig.schema_version = "0.99".into();
260        assert!(sig.validate().is_err());
261    }
262
263    #[test]
264    fn invalid_action_is_rejected() {
265        let mut sig = DsfbSignatureFile::example_target_depletion();
266        sig.heuristics[0].action = "Alert".into(); // not in allowed set
267        assert!(sig.validate().is_err());
268    }
269
270    #[test]
271    fn empty_tool_class_is_rejected() {
272        let mut sig = DsfbSignatureFile::example_target_depletion();
273        sig.tool_class = "  ".into();
274        assert!(sig.validate().is_err());
275    }
276
277    #[test]
278    fn zero_persistence_is_rejected() {
279        let mut sig = DsfbSignatureFile::example_target_depletion();
280        sig.heuristics[0].motif_signatures[0].minimum_persistence_runs = 0;
281        assert!(sig.validate().is_err());
282    }
283
284    #[test]
285    fn round_trip_json_serialisation() {
286        let sig = DsfbSignatureFile::example_target_depletion();
287        let json = sig.to_json_pretty().unwrap();
288        let parsed: DsfbSignatureFile = serde_json::from_str(&json).unwrap();
289        assert_eq!(sig, parsed);
290    }
291
292    #[test]
293    fn write_and_load_via_tempfile() {
294        let dir = tempfile::tempdir().unwrap();
295        let path = dir.path().join("test.dsfb");
296        let sig = DsfbSignatureFile::example_target_depletion();
297        sig.write(&path).unwrap();
298        let loaded = DsfbSignatureFile::load(&path).unwrap();
299        assert_eq!(sig, loaded);
300    }
301}