dsfb-robotics 0.1.0

DSFB Structural Semiotics Engine for Robotics Health Monitoring — a deterministic, non-intrusive observer layer that reads the residuals existing robot control, kinematic identification, and whole-body balance observers already compute, and structures them into a human-readable grammar of typed episodes. Read-only augmentation, not replacement, of incumbent PHM / FDD / MPC / observer pipelines.
Documentation
//! Kani formal-verification harnesses.
//!
//! Compiled **only** when the crate is built with `#[cfg(kani)]`
//! (which Kani itself sets). Under stock `cargo build` these
//! harnesses are invisible — they add no runtime code or binary
//! weight. Verify with:
//!
//! ```bash
//! cargo kani --manifest-path crates/dsfb-robotics/Cargo.toml --no-default-features --lib
//! ```
//!
//! ## Design rationale: API-boundary + finite-enum properties only
//!
//! Kani's CBMC backend is most effective on **bounded-structure,
//! finite-enum** properties. It struggles with floating-point
//! Newton-Raphson loops (e.g. `sqrt_f64`) because each iteration
//! produces a large SAT formula that compounds across 64 unwinds.
//!
//! For this reason the harnesses below avoid the
//! [`crate::envelope::AdmissibilityEnvelope::calibrate_from_window`]
//! path (which calls `sqrt_f64`) and instead construct envelopes
//! directly. Numerical properties of the math helpers are exercised
//! by `tests/proptest_invariants.rs` (256 randomised inputs per
//! invariant) — proptest and Kani are complementary, not competing.
//!
//! ## Harness inventory
//!
//! 1. `proof_engine_observe_bounded` — `DsfbRoboticsEngine::observe`
//!    writes at most `out.len()` episodes.
//! 2. `proof_engine_observe_is_pure` — two engine invocations with
//!    identical inputs produce identical outputs.
//! 3. `proof_grammar_severity_is_total_order` — Admissible < Boundary
//!    < Violation across all `ReasonCode` values.
//! 4. `proof_policy_from_grammar_is_total` — every `GrammarState`
//!    maps to a valid `PolicyDecision`.
//! 5. `proof_envelope_violation_is_monotone_in_norm` — the envelope
//!    violation predicate is non-decreasing in `norm` at fixed ρ.

#![cfg(kani)]

use crate::engine::DsfbRoboticsEngine;
use crate::envelope::AdmissibilityEnvelope;
use crate::grammar::{GrammarState, ReasonCode};
use crate::platform::RobotContext;
use crate::policy::PolicyDecision;
use crate::Episode;

/// Small bounded sizes keep Kani's symbolic search tractable; the
/// properties generalise from small-N to arbitrary-N because the
/// observer code paths are shape-invariant.
const N: usize = 3;

#[kani::proof]
#[kani::unwind(4)]
fn proof_engine_observe_bounded() {
    let residuals: [f64; N] = kani::any();
    // Fixed-envelope path skips the sqrt-heavy calibration; we're
    // verifying the engine's bounded-output property, not the
    // envelope's numerical construction.
    let env = AdmissibilityEnvelope::new(0.1);
    let mut eng = DsfbRoboticsEngine::<2, 2>::from_envelope(env);

    let mut out: [Episode; N] = [Episode::empty(); N];
    let cap: usize = kani::any();
    kani::assume(cap <= N);

    let n = eng.observe(&residuals, &mut out[..cap], RobotContext::ArmOperating);
    assert!(n <= cap, "observe wrote past capacity");
}

// NOTE: Observer-purity property (two engines with identical state +
// identical input → identical output episode) is a design goal that
// Kani's CBMC backend cannot close within a reasonable time budget
// against the `DsfbRoboticsEngine` code path. The property is
// covered by two independent test vectors:
//   - `tests/proptest_invariants.rs::observe_is_deterministic` runs
//     256 randomised inputs per invocation and checks equality.
//   - `tests/paper_lock_binary.rs::fixture_output_is_bit_exact_across_repeat_invocations`
//     runs the full paper-lock binary for all ten datasets three
//     times each, asserting byte-identical stdout.
// This combined coverage is stronger in practice than a Kani-only
// proof that cannot terminate; documented here so a future reviewer
// knows the property is monitored rather than abandoned.

#[kani::proof]
fn proof_grammar_severity_is_total_order() {
    let reason_idx: u8 = kani::any();
    kani::assume(reason_idx < 4);
    let reason = match reason_idx {
        0 => ReasonCode::SustainedOutwardDrift,
        1 => ReasonCode::AbruptSlewViolation,
        2 => ReasonCode::RecurrentBoundaryGrazing,
        _ => ReasonCode::EnvelopeViolation,
    };
    let adm = GrammarState::Admissible.severity();
    let bnd = GrammarState::Boundary(reason).severity();
    let vio = GrammarState::Violation.severity();
    assert!(adm < bnd);
    assert!(bnd < vio);
}

#[kani::proof]
fn proof_policy_from_grammar_is_total() {
    let reason_idx: u8 = kani::any();
    kani::assume(reason_idx < 4);
    let reason = match reason_idx {
        0 => ReasonCode::SustainedOutwardDrift,
        1 => ReasonCode::AbruptSlewViolation,
        2 => ReasonCode::RecurrentBoundaryGrazing,
        _ => ReasonCode::EnvelopeViolation,
    };

    for state in [
        GrammarState::Admissible,
        GrammarState::Boundary(reason),
        GrammarState::Violation,
    ] {
        let d = PolicyDecision::from_grammar(state);
        assert!(matches!(
            d,
            PolicyDecision::Silent | PolicyDecision::Review | PolicyDecision::Escalate
        ));
    }
}

#[kani::proof]
fn proof_envelope_violation_is_monotone_in_norm() {
    let rho: f64 = kani::any();
    kani::assume(rho.is_finite() && rho >= 0.0 && rho <= 1.0e6);
    let env = AdmissibilityEnvelope::new(rho);

    let n1: f64 = kani::any();
    let n2: f64 = kani::any();
    kani::assume(n1.is_finite() && n2.is_finite());
    kani::assume(n1 >= 0.0 && n2 >= 0.0);

    if n1 <= n2 {
        // Monotonicity: if the smaller norm is a violation, the
        // larger one must be too.
        assert!(!env.is_violation(n1, 1.0) || env.is_violation(n2, 1.0));
    }
}

/// Property: for every `DatasetId` enum variant, `slug()` returns a
/// non-empty static string with bounded length, and `from_slug` round-
/// trips the slug back to the same variant. This harness covers the
/// new `--csv-path` code path indirectly: `paper-lock` uses
/// `DatasetId::from_slug` to validate the user-supplied slug regardless
/// of whether `--csv-path` is set, so a sound roundtrip on every
/// variant is sufficient to verify the slug-validation surface.
///
/// The 20 variants are enumerated by `idx ∈ 0..20`; this avoids the
/// need for a Kani `Arbitrary` derive on `DatasetId` and keeps the
/// SAT formula tractable.
#[kani::proof]
fn proof_dataset_id_slug_roundtrip_is_total() {
    use crate::datasets::DatasetId;
    let idx: u8 = kani::any();
    kani::assume(idx < 20);
    let id = match idx {
        0 => DatasetId::Cwru,
        1 => DatasetId::Ims,
        2 => DatasetId::KukaLwr,
        3 => DatasetId::FemtoSt,
        4 => DatasetId::PandaGaz,
        5 => DatasetId::DlrJustin,
        6 => DatasetId::Ur10Kufieta,
        7 => DatasetId::Cheetah3,
        8 => DatasetId::IcubPushRecovery,
        9 => DatasetId::Droid,
        10 => DatasetId::Openx,
        11 => DatasetId::AnymalParkour,
        12 => DatasetId::UnitreeG1,
        13 => DatasetId::AlohaStatic,
        14 => DatasetId::Icub3Sorrentino,
        15 => DatasetId::MobileAloha,
        16 => DatasetId::So100,
        17 => DatasetId::AlohaStaticTape,
        18 => DatasetId::AlohaStaticScrewDriver,
        _ => DatasetId::AlohaStaticPingpongTest,
    };
    let slug = id.slug();
    assert!(!slug.is_empty());
    assert!(slug.len() < 64);
    let parsed = DatasetId::from_slug(slug);
    assert!(parsed.is_some());
    assert_eq!(parsed.unwrap(), id);
}

/// Property: every variant of `DatasetId` maps to a valid
/// `DatasetFamily`. There is no failure mode where `family()` returns
/// an unrepresentable family.
#[kani::proof]
fn proof_dataset_id_family_is_total() {
    use crate::datasets::{DatasetFamily, DatasetId};
    let idx: u8 = kani::any();
    kani::assume(idx < 20);
    let id = match idx {
        0 => DatasetId::Cwru,
        1 => DatasetId::Ims,
        2 => DatasetId::KukaLwr,
        3 => DatasetId::FemtoSt,
        4 => DatasetId::PandaGaz,
        5 => DatasetId::DlrJustin,
        6 => DatasetId::Ur10Kufieta,
        7 => DatasetId::Cheetah3,
        8 => DatasetId::IcubPushRecovery,
        9 => DatasetId::Droid,
        10 => DatasetId::Openx,
        11 => DatasetId::AnymalParkour,
        12 => DatasetId::UnitreeG1,
        13 => DatasetId::AlohaStatic,
        14 => DatasetId::Icub3Sorrentino,
        15 => DatasetId::MobileAloha,
        16 => DatasetId::So100,
        17 => DatasetId::AlohaStaticTape,
        18 => DatasetId::AlohaStaticScrewDriver,
        _ => DatasetId::AlohaStaticPingpongTest,
    };
    let family = id.family();
    assert!(matches!(
        family,
        DatasetFamily::Phm | DatasetFamily::Kinematics | DatasetFamily::Balancing
    ));
    assert!(!family.label().is_empty());
}