dsfb-rf 1.0.1

DSFB-RF Structural Semiotics Engine for RF Signal Monitoring - A Deterministic, Non-Intrusive Observer Layer for Typed Structural Interpretation of IQ Residual Streams in Electronic Warfare, Spectrum Monitoring, and Cognitive Radio
Documentation
//! Artifact serialization: traceability chain, benchmark metrics, run manifest.
//!
//! Implements the artifact discipline described in paper §XIII:
//! every DSFB run emits a closed, deterministic audit chain that allows
//! any episode to be reconstructed offline without re-running upstream systems.
//!
//! ## Output Artifacts
//!
//! | File | Contents |
//! |------|----------|
//! | `dsfb_traceability.json` | Per-observation trace: r, d, s, motif, grammar, semantic, policy |
//! | `benchmark_metrics.json` | Episode precision, recall, compression, precision gain |
//! | `negative_control_report.json` | False episode rates on clean windows |
//! | `dsfb_run_manifest.json` | Software version, timestamp, protocol parameters |
//! | `run_bundle.zip` | All artifacts in a single reproducibility bundle |

extern crate std;

use std::string::String;
use std::vec::Vec;
use std::format;
use serde::{Deserialize, Serialize};

use crate::engine::ObservationResult;
use crate::grammar::GrammarState;
use crate::policy::PolicyDecision;
use crate::pipeline::{EvaluationResult, Episode};

// ── Traceability chain ──────────────────────────────────────────────────────

/// One entry in the deterministic trace chain.
///
/// This is the `(r, d, s)` tuple chain described in paper §XIII-D.
/// The complete chain is: Residual → Sign → Motif → Grammar → Semantic → Policy.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceEntry {
    /// Observation index k.
    pub k: u64,
    /// Residual norm ‖r(k)‖.
    pub residual_norm: f32,
    /// Drift ṙ(k).
    pub drift: f32,
    /// Slew r̈(k).
    pub slew: f32,
    /// Motif class name.
    pub motif: String,
    /// Grammar state.
    pub grammar: String,
    /// Semantic disposition.
    pub semantic: String,
    /// Policy decision.
    pub policy: String,
    /// Finite-time Lyapunov exponent λ(k).
    pub lyapunov_lambda: f32,
    /// Lyapunov stability classification.
    pub lyapunov_stability: String,
    /// Estimated observations to envelope exit (None → null).
    pub lyapunov_time_to_exit: Option<f32>,
    /// Sub-threshold flag.
    pub sub_threshold: bool,
    /// Integration mode (always "read_only_side_channel").
    pub integration_mode: String,
}

impl TraceEntry {
    /// Construct from an ObservationResult.
    pub fn from_result(r: &ObservationResult) -> Self {
        Self {
            k: r.k,
            residual_norm: r.residual_norm,
            drift: r.sign.drift,
            slew: r.sign.slew,
            motif: format!("{:?}", r.motif),
            grammar: grammar_to_str(r.grammar),
            semantic: format!("{:?}", r.semantic),
            policy: policy_to_str(r.policy),
            lyapunov_lambda: r.lyapunov.lambda,
            lyapunov_stability: format!("{:?}", r.lyapunov.stability),
            lyapunov_time_to_exit: r.lyapunov.time_to_exit,
            sub_threshold: r.sub_threshold,
            integration_mode: "read_only_side_channel".into(),
        }
    }
}

fn grammar_to_str(g: GrammarState) -> String {
    match g {
        GrammarState::Admissible => "Admissible".into(),
        GrammarState::Boundary(r) => format!("Boundary[{:?}]", r),
        GrammarState::Violation => "Violation".into(),
    }
}

fn policy_to_str(p: PolicyDecision) -> String {
    format!("{:?}", p)
}

// ── Benchmark metrics ───────────────────────────────────────────────────────

/// Machine-readable benchmark metrics — paper Table IV.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkMetrics {
    /// Dataset.
    pub dataset: String,
    /// Raw boundary count.
    pub raw_boundary_count: usize,
    /// Dsfb episode count.
    pub dsfb_episode_count: usize,
    /// Episode precision.
    pub episode_precision: f32,
    /// Recall numerator.
    pub recall_numerator: usize,
    /// Recall denominator.
    pub recall_denominator: usize,
    /// Recall fraction.
    pub recall_fraction: f32,
    /// Compression factor.
    pub compression_factor: f32,
    /// Precision gain.
    pub precision_gain: f32,
    /// Raw precision proxy.
    pub raw_precision_proxy: f32,
    /// False episode rate clean.
    pub false_episode_rate_clean: f32,
    /// Wpred.
    pub wpred: usize,
    /// Healthy window.
    pub healthy_window: usize,
    /// Snr floor db.
    pub snr_floor_db: f32,
}

impl BenchmarkMetrics {
    /// Build from an EvaluationResult.
    pub fn from_result(r: &EvaluationResult) -> Self {
        Self {
            dataset: r.dataset.into(),
            raw_boundary_count: r.raw_boundary_count,
            dsfb_episode_count: r.dsfb_episode_count,
            episode_precision: r.episode_precision,
            recall_numerator: r.recall_numerator,
            recall_denominator: r.recall_denominator,
            recall_fraction: r.recall(),
            compression_factor: r.compression_factor,
            precision_gain: r.precision_gain,
            raw_precision_proxy: r.raw_precision_proxy,
            false_episode_rate_clean: r.false_episode_rate_clean,
            wpred: crate::pipeline::WPRED,
            healthy_window: crate::pipeline::HEALTHY_WINDOW_SIZE,
            snr_floor_db: crate::pipeline::SNR_FLOOR_DB,
        }
    }
}

// ── Episode precision report ────────────────────────────────────────────────

/// Serializable episode record.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpisodeRecord {
    /// Episode id.
    pub episode_id: usize,
    /// Open k.
    pub open_k: usize,
    /// Close k.
    pub close_k: Option<usize>,
    /// Is precursor.
    pub is_precursor: bool,
}

impl EpisodeRecord {
    /// Build episode records from a slice of pipeline Episodes.
    pub fn from_episodes(episodes: &[Episode]) -> Vec<Self> {
        episodes.iter().enumerate().map(|(i, ep)| Self {
            episode_id: i,
            open_k: ep.open_k,
            close_k: ep.close_k,
            is_precursor: ep.is_precursor,
        }).collect()
    }
}

// ── Run manifest ────────────────────────────────────────────────────────────

/// Run manifest — provenance record for a complete evaluation run.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunManifest {
    /// Crate version from Cargo.toml.
    pub crate_version: String,
    /// ISO 8601 run timestamp.
    pub run_timestamp: String,
    /// Protocol identifier.
    pub protocol: String,
    /// DSA window W.
    pub dsa_w: usize,
    /// Grammar K.
    pub grammar_k: usize,
    /// DSA threshold τ.
    pub dsa_tau: f32,
    /// Corroboration m.
    pub corroboration_m: u8,
    /// EWMA λ.
    pub ewma_lambda: f32,
    /// CUSUM κ multiplier.
    pub cusum_kappa_sigma: f32,
    /// CUSUM h multiplier.
    pub cusum_h_sigma: f32,
    /// W_pred.
    pub wpred: usize,
    /// Selected configuration name.
    pub configuration: String,
    /// Total traceability entries.
    pub trace_entry_count: usize,
    /// Non-intrusion contract enforcement method.
    pub non_intrusion: String,
}

impl RunManifest {
    /// Build the manifest for a complete evaluation run.
    pub fn build(trace_len: usize) -> Self {
        use crate::pipeline::*;
        Self {
            crate_version: env!("CARGO_PKG_VERSION").into(),
            run_timestamp: chrono_now_iso8601(),
            protocol: "Stage_III_fixed_read_only".into(),
            dsa_w: DSA_WINDOW_W,
            grammar_k: GRAMMAR_K,
            dsa_tau: DSA_TAU,
            corroboration_m: CORROBORATION_M,
            ewma_lambda: EWMA_LAMBDA,
            cusum_kappa_sigma: CUSUM_KAPPA_SIGMA,
            cusum_h_sigma: CUSUM_H_SIGMA,
            wpred: WPRED,
            configuration: "all_features[compression_biased]".into(),
            trace_entry_count: trace_len,
            non_intrusion: "enforced_by_rust_type_system_no_unsafe".into(),
        }
    }
}

/// Simple ISO 8601 timestamp — no chrono dependency needed.
fn chrono_now_iso8601() -> String {
    // Use a fixed placeholder — the actual timestamp is determined at runtime
    // by the pipeline CLI which can read std::time::SystemTime.
    "2026-04-08T00:00:00Z".into()
}

// ── JSON serialization helpers ──────────────────────────────────────────────

/// Serialize a value to a pretty-printed JSON string.
pub fn to_json_pretty<T: Serialize>(val: &T) -> Result<String, String> {
    serde_json::to_string_pretty(val).map_err(|e| std::format!("{}", e))
}

/// Write all standard artifacts to the given output directory.
///
/// Creates:
/// - `dsfb_traceability.json`
/// - `benchmark_metrics.json`
/// - `episode_precision_metrics.json`
/// - `dsfb_run_manifest.json`
pub fn write_artifacts(
    result: &EvaluationResult,
    out_dir: &std::path::Path,
) -> Result<(), String> {
    use std::fs;
    fs::create_dir_all(out_dir).map_err(|e| std::format!("{}", e))?;

    // Traceability chain
    let trace: Vec<TraceEntry> = result.trace.iter()
        .map(TraceEntry::from_result)
        .collect();
    let trace_json = to_json_pretty(&trace)?;
    fs::write(out_dir.join("dsfb_traceability.json"), trace_json)
        .map_err(|e| std::format!("{}", e))?;

    // Benchmark metrics
    let metrics = BenchmarkMetrics::from_result(result);
    let metrics_json = to_json_pretty(&metrics)?;
    fs::write(out_dir.join("benchmark_metrics.json"), metrics_json)
        .map_err(|e| std::format!("{}", e))?;

    // Episode precision
    let episodes = EpisodeRecord::from_episodes(&result.episodes);
    let ep_json = to_json_pretty(&episodes)?;
    fs::write(out_dir.join("episode_precision_metrics.json"), ep_json)
        .map_err(|e| std::format!("{}", e))?;

    // Negative control
    let neg = serde_json::json!({
        "false_episode_rate_clean": result.false_episode_rate_clean,
        "dataset": result.dataset,
        "note": "bounded to evaluated dataset; do not extrapolate"
    });
    let neg_json = serde_json::to_string_pretty(&neg).map_err(|e| std::format!("{}", e))?;
    fs::write(out_dir.join("negative_control_report.json"), neg_json)
        .map_err(|e| std::format!("{}", e))?;

    // Run manifest
    let manifest = RunManifest::build(result.trace.len());
    let manifest_json = to_json_pretty(&manifest)?;
    fs::write(out_dir.join("dsfb_run_manifest.json"), manifest_json)
        .map_err(|e| std::format!("{}", e))?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::vec;
    use crate::pipeline::{synthetic_radioml_stream, run_stage_iii};

    #[test]
    fn trace_entry_from_observation() {
        let drift_at = vec![150, 300];
        let (obs, events) = synthetic_radioml_stream(400, &drift_at, 15.0);
        let result = run_stage_iii("trace_test", &obs, &events);
        if !result.trace.is_empty() {
            let entry = TraceEntry::from_result(&result.trace[0]);
            assert_eq!(entry.integration_mode, "read_only_side_channel");
        }
    }

    #[test]
    fn benchmark_metrics_json_roundtrip() {
        let drift_at = vec![200];
        let (obs, events) = synthetic_radioml_stream(300, &drift_at, 15.0);
        let result = run_stage_iii("metrics_test", &obs, &events);
        let m = BenchmarkMetrics::from_result(&result);
        let json = to_json_pretty(&m).unwrap();
        let parsed: BenchmarkMetrics = serde_json::from_str(&json).unwrap();
        assert!((parsed.episode_precision - m.episode_precision).abs() < 1e-5);
        assert_eq!(parsed.dataset, m.dataset);
    }

    #[test]
    fn run_manifest_has_correct_version() {
        let m = RunManifest::build(42);
        assert_eq!(m.crate_version, env!("CARGO_PKG_VERSION"));
        assert_eq!(m.trace_entry_count, 42);
        assert_eq!(m.non_intrusion, "enforced_by_rust_type_system_no_unsafe");
        assert_eq!(m.configuration, "all_features[compression_biased]");
    }

    #[test]
    fn write_artifacts_creates_all_files() {
        let drift_at = vec![150, 250, 350];
        let (obs, events) = synthetic_radioml_stream(500, &drift_at, 15.0);
        let result = run_stage_iii("write_test", &obs, &events);

        let tmp = std::env::temp_dir().join("dsfb_rf_test_artifacts");
        write_artifacts(&result, &tmp).expect("write_artifacts must succeed");

        assert!(tmp.join("dsfb_traceability.json").exists());
        assert!(tmp.join("benchmark_metrics.json").exists());
        assert!(tmp.join("episode_precision_metrics.json").exists());
        assert!(tmp.join("negative_control_report.json").exists());
        assert!(tmp.join("dsfb_run_manifest.json").exists());
    }
}