Files
ruvnet--RuView/v2/crates/wifi-densepose-bfld/tests/soul_match_oracle.rs
T
ruv ae6fd75095 feat(adr-118/p3.4): SoulMatchOracle + Recalibrate exemption (93/93 GREEN)
Iter 12. Wires the ADR-121 §2.6 Recalibrate exemption: when an enrolled
person_id matches the current high-separability cluster, the gate
downgrades the would-be Recalibrate to PredictOnly. The high score is
the *intended* outcome of a Soul Signature match, not an attacker-grade
sniffer arrival — so site_salt rotation is suppressed.

Added (no_std-compatible):
- src/coherence_gate.rs additions:
  * MatchOutcome enum: Match { person_id: u64 } | NotEnrolled | Suppressed
  * SoulMatchOracle trait with matches_enrolled() -> MatchOutcome
  * NullOracle (default-constructible, always reports NotEnrolled)
  * CoherenceGate::evaluate_with_oracle(score, ts, &O: SoulMatchOracle)
    — same hysteresis/debounce as evaluate(), but downgrades Recalibrate
    to PredictOnly when oracle returns Match { .. }
  * Refactored evaluate(): extracted advance_state(target, ts) shared with
    evaluate_with_oracle. evaluate is now a 4-line wrapper.
- pub use MatchOutcome, NullOracle, SoulMatchOracle from lib.rs

tests/soul_match_oracle.rs (8 named tests, all green):
  null_oracle_matches_default_evaluate_behavior
    (parameterized over 5 score points; oracle-aware and oracle-free
     gates produce identical trajectories)
  match_outcome_downgrades_recalibrate_to_predict_only
    (score=0.95 pends PredictOnly instead of Recalibrate)
  match_exemption_promotes_predict_only_after_debounce_not_recalibrate
    (after DEBOUNCE_NS, current is PredictOnly — never Recalibrate)
  match_outcome_does_not_affect_lower_actions
    (Reject pending stays Reject; oracle only intercepts Recalibrate)
  suppressed_outcome_does_not_exempt_recalibrate
    (Suppressed is functionally equivalent to NotEnrolled at the gate)
  not_enrolled_outcome_does_not_exempt_recalibrate
  match_outcome_carries_person_id
  null_oracle_default_constructor_works

ACs progressed:
- ADR-121 §2.6 fully covered as a stateless integration point — the
  hook is in place for the `--features soul-signature` Soul Signature
  crate (TBD) to plug in a real RaBitQ-backed oracle.
- ADR-118 §1.4 Soul Signature companion contract is now structurally
  enforced at the gate boundary: enrolled subjects do not trigger
  site_salt rotation; everyone else does.

Test config:
- cargo test --no-default-features → 64 passed (56 + 8)
- cargo test                       → 93 passed (85 + 8)

Out of scope (next iter target):
- BfldEvent struct (ADR-121 §2.1 output event JSON) — the downstream
  consumer of GateAction. Pairs the gate decision with presence/motion/
  person_count sensing fields.
- Optional: connect SoulMatchOracle into the actual `--features
  soul-signature` build (compile-time gate around a re-export).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 15:17:24 -04:00

99 lines
3.3 KiB
Rust

//! Acceptance tests for ADR-121 §2.6 — `SoulMatchOracle` Recalibrate exemption.
use wifi_densepose_bfld::coherence_gate::DEBOUNCE_NS;
use wifi_densepose_bfld::{
CoherenceGate, GateAction, MatchOutcome, NullOracle, SoulMatchOracle,
};
/// Oracle that always claims an enrolled match.
struct AlwaysMatch;
impl SoulMatchOracle for AlwaysMatch {
fn matches_enrolled(&self) -> MatchOutcome {
MatchOutcome::Match { person_id: 0x4242_4242 }
}
}
/// Oracle that reports suppressed (class-3 deployment).
struct AlwaysSuppressed;
impl SoulMatchOracle for AlwaysSuppressed {
fn matches_enrolled(&self) -> MatchOutcome {
MatchOutcome::Suppressed
}
}
#[test]
fn null_oracle_matches_default_evaluate_behavior() {
let mut a = CoherenceGate::new();
let mut b = CoherenceGate::new();
let oracle = NullOracle;
for (i, score) in [0.1, 0.4, 0.6, 0.8, 0.95].iter().enumerate() {
let ts = (i as u64) * 2 * DEBOUNCE_NS;
assert_eq!(a.evaluate(*score, ts), b.evaluate_with_oracle(*score, ts, &oracle));
}
}
#[test]
fn match_outcome_downgrades_recalibrate_to_predict_only() {
let mut g = CoherenceGate::new();
let oracle = AlwaysMatch;
// Score = 0.95 would normally pend Recalibrate. With AlwaysMatch oracle,
// it pends PredictOnly instead.
g.evaluate_with_oracle(0.95, 0, &oracle);
assert_eq!(g.pending(), Some(GateAction::PredictOnly));
}
#[test]
fn match_exemption_promotes_predict_only_after_debounce_not_recalibrate() {
let mut g = CoherenceGate::new();
let oracle = AlwaysMatch;
g.evaluate_with_oracle(0.95, 0, &oracle);
let out = g.evaluate_with_oracle(0.95, DEBOUNCE_NS, &oracle);
assert_eq!(out, GateAction::PredictOnly);
assert_ne!(out, GateAction::Recalibrate, "Match must prevent Recalibrate");
}
#[test]
fn match_outcome_does_not_affect_lower_actions() {
let mut g = CoherenceGate::new();
let oracle = AlwaysMatch;
// Score in the Reject band — oracle exemption does NOT apply (only to Recalibrate).
g.evaluate_with_oracle(0.8, 0, &oracle);
assert_eq!(g.pending(), Some(GateAction::Reject));
// Run to debounce — current must become Reject, not PredictOnly.
let out = g.evaluate_with_oracle(0.8, DEBOUNCE_NS, &oracle);
assert_eq!(out, GateAction::Reject);
}
#[test]
fn suppressed_outcome_does_not_exempt_recalibrate() {
let mut g = CoherenceGate::new();
let oracle = AlwaysSuppressed;
g.evaluate_with_oracle(0.95, 0, &oracle);
// Suppressed is functionally equivalent to NotEnrolled — Recalibrate stays pending.
assert_eq!(g.pending(), Some(GateAction::Recalibrate));
}
#[test]
fn not_enrolled_outcome_does_not_exempt_recalibrate() {
let mut g = CoherenceGate::new();
let oracle = NullOracle; // always NotEnrolled
g.evaluate_with_oracle(0.95, 0, &oracle);
assert_eq!(g.pending(), Some(GateAction::Recalibrate));
}
#[test]
fn match_outcome_carries_person_id() {
let outcome = AlwaysMatch.matches_enrolled();
match outcome {
MatchOutcome::Match { person_id } => assert_eq!(person_id, 0x4242_4242),
other => panic!("expected Match, got {other:?}"),
}
}
#[test]
fn null_oracle_default_constructor_works() {
let oracle = NullOracle;
assert_eq!(oracle.matches_enrolled(), MatchOutcome::NotEnrolled);
}