use crate::error::DsfbSemiconductorError;
use serde::{Deserialize, Serialize};
use std::path::Path;
pub const SIGNATURE_SCHEMA_VERSION: &str = "1.0";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DsfbMotifSignature {
pub motif_id: String,
pub description: String,
pub motif_sequence: Vec<String>,
pub required_grammar_states: Vec<String>,
pub expected_dimensions: Option<Vec<String>>,
pub minimum_persistence_runs: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DsfbHeuristicSignature {
pub heuristic_id: String,
pub name: String,
pub description: String,
pub motif_signatures: Vec<DsfbMotifSignature>,
pub action: String,
pub escalation_policy: String,
pub escalation_timeout_runs: usize,
pub requires_corroboration: bool,
pub author: Option<String>,
pub semantic_label: String,
pub known_limitations: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DsfbSignatureFile {
pub schema_version: String,
pub tool_class: String,
pub vendor: Option<String>,
pub heuristics: Vec<DsfbHeuristicSignature>,
}
impl DsfbSignatureFile {
pub fn validate(&self) -> Result<(), DsfbSemiconductorError> {
if self.schema_version != SIGNATURE_SCHEMA_VERSION {
return Err(DsfbSemiconductorError::Config(format!(
"unsupported signature schema version '{}'; expected '{}'",
self.schema_version, SIGNATURE_SCHEMA_VERSION
)));
}
if self.tool_class.trim().is_empty() {
return Err(DsfbSemiconductorError::Config(
"tool_class must not be empty".into(),
));
}
for h in &self.heuristics {
if h.heuristic_id.trim().is_empty() {
return Err(DsfbSemiconductorError::Config(
"heuristic_id must not be empty".into(),
));
}
let valid_actions = ["Monitor", "Watch", "Review", "Escalate"];
if !valid_actions.contains(&h.action.as_str()) {
return Err(DsfbSemiconductorError::Config(format!(
"heuristic '{}': action '{}' is not in {:?}",
h.heuristic_id, h.action, valid_actions
)));
}
for motif in &h.motif_signatures {
if motif.minimum_persistence_runs == 0 {
return Err(DsfbSemiconductorError::Config(format!(
"heuristic '{}' motif '{}': minimum_persistence_runs must be > 0",
h.heuristic_id, motif.motif_id
)));
}
}
}
Ok(())
}
pub fn load(path: &Path) -> Result<Self, DsfbSemiconductorError> {
let content = std::fs::read_to_string(path)
.map_err(DsfbSemiconductorError::Io)?;
let file: Self = serde_json::from_str(&content)
.map_err(|e| DsfbSemiconductorError::Config(format!("JSON parse error: {e}")))?;
file.validate()?;
Ok(file)
}
pub fn to_json_pretty(&self) -> Result<String, DsfbSemiconductorError> {
serde_json::to_string_pretty(self)
.map_err(|e| DsfbSemiconductorError::Config(format!("JSON serialise error: {e}")))
}
pub fn write(&self, path: &Path) -> Result<(), DsfbSemiconductorError> {
let json = self.to_json_pretty()?;
std::fs::write(path, json)
.map_err(DsfbSemiconductorError::Io)
}
pub fn example_target_depletion() -> Self {
Self {
schema_version: SIGNATURE_SCHEMA_VERSION.into(),
tool_class: "ICP Etch".into(),
vendor: Some("reference_dsfb_v1".into()),
heuristics: vec![DsfbHeuristicSignature {
heuristic_id: "target_depletion_v1".into(),
name: "Target Depletion (Sputter Source)".into(),
description: concat!(
"Slow, monotonic drift of the gas-flow residual toward the ",
"admissibility boundary, co-occurring with a matching pressure ",
"drift in the opposite direction. Signature of consumable ",
"target erosion in sputter-based etch chambers."
)
.into(),
motif_signatures: vec![DsfbMotifSignature {
motif_id: "slow_drift_precursor".into(),
description: "Monotonic positive drift approaching ρ".into(),
motif_sequence: vec![
"slow_drift_precursor".into(),
"boundary_grazing".into(),
"persistent_instability".into(),
],
required_grammar_states: vec![
"SustainedDrift".into(),
"BoundaryGrazing".into(),
"PersistentViolation".into(),
],
expected_dimensions: Some(vec!["sccm".into(), "milli_torr".into()]),
minimum_persistence_runs: 5,
}],
action: "Review".into(),
escalation_policy: "Escalate if motif persists for > 25 runs without recovery".into(),
escalation_timeout_runs: 25,
requires_corroboration: true,
author: Some("DSFB Reference Library v1.0".into()),
semantic_label: "target_depletion".into(),
known_limitations: Some(concat!(
"False positives possible when gas composition changes due to ",
"recipe parameter sweep; gate with ProcessContext.recipe_step.",
).into()),
}],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn example_signature_is_valid() {
let sig = DsfbSignatureFile::example_target_depletion();
sig.validate().expect("example signature should be valid");
}
#[test]
fn wrong_schema_version_is_rejected() {
let mut sig = DsfbSignatureFile::example_target_depletion();
sig.schema_version = "0.99".into();
assert!(sig.validate().is_err());
}
#[test]
fn invalid_action_is_rejected() {
let mut sig = DsfbSignatureFile::example_target_depletion();
sig.heuristics[0].action = "Alert".into(); assert!(sig.validate().is_err());
}
#[test]
fn empty_tool_class_is_rejected() {
let mut sig = DsfbSignatureFile::example_target_depletion();
sig.tool_class = " ".into();
assert!(sig.validate().is_err());
}
#[test]
fn zero_persistence_is_rejected() {
let mut sig = DsfbSignatureFile::example_target_depletion();
sig.heuristics[0].motif_signatures[0].minimum_persistence_runs = 0;
assert!(sig.validate().is_err());
}
#[test]
fn round_trip_json_serialisation() {
let sig = DsfbSignatureFile::example_target_depletion();
let json = sig.to_json_pretty().unwrap();
let parsed: DsfbSignatureFile = serde_json::from_str(&json).unwrap();
assert_eq!(sig, parsed);
}
#[test]
fn write_and_load_via_tempfile() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.dsfb");
let sig = DsfbSignatureFile::example_target_depletion();
sig.write(&path).unwrap();
let loaded = DsfbSignatureFile::load(&path).unwrap();
assert_eq!(sig, loaded);
}
}