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}