diff --git a/v2/crates/wifi-densepose-bfld/Cargo.toml b/v2/crates/wifi-densepose-bfld/Cargo.toml index 8aa52636..5f41f1f9 100644 --- a/v2/crates/wifi-densepose-bfld/Cargo.toml +++ b/v2/crates/wifi-densepose-bfld/Cargo.toml @@ -39,6 +39,13 @@ rumqttc = { version = "0.24", default-features = false, features = ["use-rustls" [dev-dependencies] proptest.workspace = true +# The minimal example uses BfldEvent::to_json(), which is gated on serde-json. +# Without this declaration, `cargo test --no-default-features` tries to build +# the example and fails on the missing to_json() method. +[[example]] +name = "bfld_minimal" +required-features = ["serde-json"] + [lints.rust] unsafe_code = "forbid" missing_docs = "warn" diff --git a/v2/crates/wifi-densepose-bfld/examples/bfld_minimal.rs b/v2/crates/wifi-densepose-bfld/examples/bfld_minimal.rs new file mode 100644 index 00000000..559d321d --- /dev/null +++ b/v2/crates/wifi-densepose-bfld/examples/bfld_minimal.rs @@ -0,0 +1,70 @@ +//! Minimal end-to-end BFLD pipeline example. Demonstrates the operator-facing +//! flow: construct a `BfldPipeline` with a `SignatureHasher`, feed one +//! `SensingInputs` + `IdentityEmbedding`, and print the resulting privacy- +//! gated `BfldEvent` as JSON. +//! +//! Run with: +//! ```sh +//! cargo run -p wifi-densepose-bfld --example bfld_minimal +//! ``` +//! +//! Expected output: one JSON line on stdout matching the BfldEvent schema +//! (presence, motion, person_count, identity_risk_score, rf_signature_hash, +//! privacy_class = "anonymous"). + +use wifi_densepose_bfld::{ + BfldConfig, BfldPipeline, IdentityEmbedding, SensingInputs, SignatureHasher, EMBEDDING_DIM, + SITE_SALT_LEN, +}; + +fn main() -> Result<(), Box> { + // 1. Per-site secret (in production: loaded from TPM / KMS / secret file). + let site_salt: [u8; SITE_SALT_LEN] = [ + 0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x07, 0x18, 0x29, 0x3A, 0x4B, 0x5C, 0x6D, 0x7E, 0x8F, + 0x90, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, + 0xFF, 0x00, + ]; + + // 2. Build the pipeline. Default class = Anonymous, no zone, hasher + // installed so rf_signature_hash gets derived from the embedding. + let mut pipeline = BfldPipeline::new( + BfldConfig::new("seed-example") + .with_signature_hasher(SignatureHasher::new(site_salt)), + ); + + // 3. One per-frame sensing observation. In production these come from + // the BFI extractor + RuvSense feature engine. + let inputs = SensingInputs { + timestamp_ns: 1_700_000_000_000_000_000, + presence: true, + motion: 0.42, + person_count: 1, + sensing_confidence: 0.91, + // Low risk — gate stays in Accept; event is published. + sep: 0.2, + stab: 0.2, + consist: 0.2, + risk_conf: 0.2, + rf_signature_hash: None, // hasher will derive + }; + + // 4. Embedding from the AETHER encoder (ADR-024). For the example we + // fill with a deterministic ramp; production uses real model output. + let mut emb_values = [0.0f32; EMBEDDING_DIM]; + for (i, v) in emb_values.iter_mut().enumerate() { + *v = (i as f32) * 0.0073; + } + let embedding = IdentityEmbedding::from_raw(emb_values); + + // 5. Drive the pipeline. Returns Some(BfldEvent) when the gate permits; + // None on Reject / Recalibrate. + let event = pipeline + .process(inputs, Some(embedding)) + .ok_or("gate dropped the event — should not happen at this risk level")?; + + // 6. Publish JSON. Real deployments would feed this to MQTT via the + // iter-22 publish_event(&publisher, &event) helper. + let json = event.to_json()?; + println!("{json}"); + Ok(()) +} diff --git a/v2/crates/wifi-densepose-bfld/tests/example_minimal.rs b/v2/crates/wifi-densepose-bfld/tests/example_minimal.rs new file mode 100644 index 00000000..3479634b --- /dev/null +++ b/v2/crates/wifi-densepose-bfld/tests/example_minimal.rs @@ -0,0 +1,98 @@ +//! Validates the `examples/bfld_minimal.rs` operator-quickstart contract. +//! The example file embeds via include_str! for documentation-drift checks, +//! then a separate test re-executes the same end-to-end flow inline so we +//! get a CI-runnable proof that the operator workflow produces valid JSON. + +#![cfg(feature = "std")] + +use wifi_densepose_bfld::{ + BfldConfig, BfldPipeline, IdentityEmbedding, SensingInputs, SignatureHasher, EMBEDDING_DIM, + SITE_SALT_LEN, +}; + +const MINIMAL_EXAMPLE: &str = include_str!("../examples/bfld_minimal.rs"); + +#[test] +fn minimal_example_documents_the_operator_quickstart_flow() { + // The example must call out the canonical operator-facing types so + // anyone reading it sees the right entry points. + assert!(MINIMAL_EXAMPLE.contains("BfldPipeline")); + assert!(MINIMAL_EXAMPLE.contains("SignatureHasher")); + assert!(MINIMAL_EXAMPLE.contains("SensingInputs")); + assert!(MINIMAL_EXAMPLE.contains("IdentityEmbedding")); + assert!(MINIMAL_EXAMPLE.contains("BfldConfig")); + assert!( + MINIMAL_EXAMPLE.contains(".process("), + "example must invoke pipeline.process(...) — method-chain style OK", + ); + assert!(MINIMAL_EXAMPLE.contains("to_json")); +} + +#[test] +fn minimal_example_carries_run_instructions_in_doc_comments() { + assert!( + MINIMAL_EXAMPLE.contains("cargo run -p wifi-densepose-bfld --example bfld_minimal"), + "example must document its own run command", + ); +} + +#[test] +fn minimal_example_flow_produces_valid_json_with_documented_fields() { + // Re-execute the same logic the example does so CI proves the flow + // works end-to-end without needing `cargo run --example`. + let site_salt: [u8; SITE_SALT_LEN] = [0xAB; SITE_SALT_LEN]; + let mut pipeline = BfldPipeline::new( + BfldConfig::new("seed-example") + .with_signature_hasher(SignatureHasher::new(site_salt)), + ); + let inputs = SensingInputs { + timestamp_ns: 1_700_000_000_000_000_000, + presence: true, + motion: 0.42, + person_count: 1, + sensing_confidence: 0.91, + sep: 0.2, + stab: 0.2, + consist: 0.2, + risk_conf: 0.2, + rf_signature_hash: None, + }; + let mut emb_values = [0.0f32; EMBEDDING_DIM]; + for (i, v) in emb_values.iter_mut().enumerate() { + *v = (i as f32) * 0.0073; + } + let embedding = IdentityEmbedding::from_raw(emb_values); + + let event = pipeline + .process(inputs, Some(embedding)) + .expect("low-risk emit must succeed"); + let json = event.to_json().expect("JSON serialization must succeed"); + + // The published JSON should carry every documented anonymous-class field. + for needle in [ + "\"type\":\"bfld_update\"", + "\"node_id\":\"seed-example\"", + "\"presence\":true", + "\"motion\":", + "\"person_count\":1", + "\"confidence\":", + "\"privacy_class\":\"anonymous\"", + "\"identity_risk_score\":", + "\"rf_signature_hash\":\"blake3:", + ] { + assert!( + json.contains(needle), + "example JSON missing expected snippet `{needle}`\nfull JSON: {json}", + ); + } +} + +#[test] +fn example_returns_box_dyn_error_for_main_signature() { + // `main() -> Result<(), Box>` is the standard + // Rust-example pattern. Confirm the file uses it so future copy-paste + // doesn't drop error propagation. + assert!( + MINIMAL_EXAMPLE.contains("fn main() -> Result<(), Box>"), + ); +}