Iter 11. Wraps the stateless GateAction classifier from iter 10 with two
stabilizing mechanisms per ADR-121 §2.5:
* ±0.05 HYSTERESIS — a score must clear the current band's edge by
HYSTERESIS before the gate considers the next band.
* 5-second DEBOUNCE_NS — a different action must persist that long
before it becomes current; returning to the current band cancels it.
Added (no_std-compatible):
- src/coherence_gate.rs:
* HYSTERESIS const (0.05) + DEBOUNCE_NS const (5_000_000_000)
* CoherenceGate { current, pending: Option<(GateAction, u64)> }
* new() / Default / current() / pending() (diagnostic accessors)
* evaluate(score, timestamp_ns) -> GateAction
Algorithm: compute effective_target via per-direction hysteresis check,
promote pending after DEBOUNCE_NS elapsed, cancel pending on return to
current band, reset debounce clock if pending target changes
* Private helpers effective_target / action_idx / upper_edge_of / lower_edge_of
- pub use CoherenceGate from lib.rs
tests/coherence_gate.rs (13 named tests, all green):
fresh_gate_starts_in_accept_with_no_pending
low_score_stays_in_accept_with_no_pending
score_just_past_boundary_but_within_hysteresis_does_not_pend
(0.52: above 0.5 but inside hysteresis envelope — no pending)
score_clearly_past_hysteresis_starts_pending
(0.6: past 0.55 hysteresis edge — pending PredictOnly registered)
pending_action_promotes_after_full_debounce
pending_action_does_not_promote_before_debounce
(verified at DEBOUNCE_NS - 1)
returning_to_current_band_cancels_pending
changing_pending_target_resets_the_debounce_clock
(PredictOnly pending at t=0, then Recalibrate at t=1s — clock resets,
must wait until t=1s+DEBOUNCE_NS before Recalibrate is current)
downward_transitions_also_require_hysteresis
(from PredictOnly, 0.48 stays put; 0.44 pends Accept)
spike_to_one_then_back_to_zero_never_promotes_to_recalibrate
(transient spike + return to baseline produces no transition)
boundary_value_with_hysteresis_does_not_promote (0.5+0.05-epsilon)
boundary_value_at_hysteresis_exact_does_pend (0.5+0.05)
nan_score_stays_in_current_action_with_no_pending
ACs progressed:
- ADR-121 AC4 — Recalibrate fires when score >= 0.9 for >= DEBOUNCE_NS (5s).
The debounce test above directly exercises this.
- ADR-121 AC5 — hysteresis test confirms action does not oscillate across
± 0.05 of a threshold within a 5-second window.
Test config:
- cargo test --no-default-features → 56 passed (43 + 13)
- cargo test → 85 passed (72 + 13)
Out of scope (next iter target):
- SoulMatchOracle stub trait (ADR-121 §2.6) + Recalibrate exemption —
when --features soul-signature is enabled and the oracle reports a known
enrolled person_id match, the gate downgrades Recalibrate → PredictOnly.
- BfldEvent struct (ADR-121 §2.1 output event) — first downstream consumer
of the gate action.
Co-Authored-By: claude-flow <ruv@ruv.net>