feat(adr-118/p6.12): examples/bfld_minimal.rs operator quickstart (315/315 GREEN)

Iter 47. Ships the operator-facing quickstart as doc-as-code. Three
goals:

1. New operators reading the crate get a 50-line working example
   instead of having to assemble pipeline + config + hasher + inputs
   + embedding + JSON publish themselves.
2. CI proves the example COMPILES and RUNS end-to-end via a
   separate test that re-executes the same flow inline.
3. The example output is the canonical BfldEvent JSON, demonstrating
   every documented field (presence/motion/count/conf/zone/class/
   identity_risk_score/rf_signature_hash) for a typical Anonymous
   class publish.

Added:
- v2/crates/wifi-densepose-bfld/examples/bfld_minimal.rs (~70 LOC):
    * Per-site secret salt
    * BfldPipeline::new(BfldConfig::new(...).with_signature_hasher(...))
    * SensingInputs with low-risk factors so the gate emits
    * IdentityEmbedding from a deterministic ramp
    * pipeline.process(...).ok_or(...) for the gate-drop case
    * event.to_json() printed to stdout
    * Run command in the doc comment:
        cargo run -p wifi-densepose-bfld --example bfld_minimal

- v2/crates/wifi-densepose-bfld/tests/example_minimal.rs (4 tests):
    minimal_example_documents_the_operator_quickstart_flow
      (asserts file contains BfldPipeline, SignatureHasher,
       SensingInputs, IdentityEmbedding, BfldConfig, .process(,
       to_json — catches doc drift if the example removes a key
       symbol)
    minimal_example_carries_run_instructions_in_doc_comments
      (the cargo run --example line must be present)
    minimal_example_flow_produces_valid_json_with_documented_fields
      *** Re-runs the example flow inline and asserts every
          documented JSON field appears in the output ***
    example_returns_box_dyn_error_for_main_signature
      (canonical Rust-example main signature)

- v2/crates/wifi-densepose-bfld/Cargo.toml:
    [[example]] name = "bfld_minimal", required-features = ["serde-json"]
    so `cargo test --no-default-features` doesn't try to build the
    example (which needs to_json gated on serde-json).

Example run output (sanity check before commit):
  {"type":"bfld_update","node_id":"seed-example","timestamp_ns":...,
   "presence":true,"motion":0.42,"person_count":1,"confidence":0.91,
   "privacy_class":"anonymous","identity_risk_score":0.0016000001,
   "rf_signature_hash":"blake3:cc3615c7aaab9d0867a0c15327444b8f...bf"}

ADR-124 status (iter step 0 sibling check):
- docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md unchanged
  at 431 lines. SENSE-BRIDGE scope remains orthogonal.

ACs progressed:
- ADR-118 §2.1 documentation surface — first operator-facing example
  shipped as part of the crate. Discoverable via
  `cargo run --example bfld_minimal` and verified via cargo test.

Test config:
- cargo test --no-default-features → 101 passed (example_minimal cfg-out)
- cargo test                       → 315 passed (311 + 4 example_minimal)

Out of scope (next iter target):
- PR-readiness pivot still pending: CHANGELOG, witness bundle,
  AC closeout table. External-resource-gated work still skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-05-24 19:49:16 -04:00
parent 354829ec81
commit ea7b5711a1
3 changed files with 175 additions and 0 deletions

View File

@ -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"

View File

@ -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<dyn std::error::Error>> {
// 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(())
}

View File

@ -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<dyn std::error::Error>>` 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<dyn std::error::Error>>"),
);
}