///|
/// A threshold policy turns posterior summaries into application-level events.
pub struct EventPolicy {
probability_threshold : Double
minimum_run_length : Int
cooldown : Int
}
///|
/// Build a policy without embedding alert transport or side effects in the library.
pub fn EventPolicy::new(
probability_threshold : Double,
minimum_run_length : Int,
cooldown : Int,
) -> Result[EventPolicy, DetectError] {
if !finite(probability_threshold) ||
probability_threshold <= 0.0 ||
probability_threshold >= 1.0 {
return Err(InvalidHazard)
}
if minimum_run_length < 0 || cooldown < 0 {
return Err(InvalidRunLengthLimit)
}
Ok({ probability_threshold, minimum_run_length, cooldown, })
}
///|
/// An event contains the evidence needed by a caller to make an alert decision.
pub struct ChangeEvent {
step : Int
observation : Double
probability : Double
prior_run_length : Int
}
///|
pub fn ChangeEvent::step(self : ChangeEvent) -> Int {
self.step
}
///|
pub fn ChangeEvent::probability(self : ChangeEvent) -> Double {
self.probability
}
///|
/// Stateful debounce helper. It never mutates the detector itself.
pub struct EventGate {
policy : EventPolicy
mut last_emitted_step : Int
}
///|
pub fn EventGate::new(policy : EventPolicy) -> EventGate {
{ policy, last_emitted_step: -1000000000, }
}
///|
/// Emit at most one event per cooldown interval after a sufficiently long segment.
pub fn EventGate::observe(self : EventGate, update : Update) -> ChangeEvent? {
let since_last = update.step - self.last_emitted_step
if update.change_probability >= self.policy.probability_threshold &&
update.most_likely_run_length <= self.policy.minimum_run_length &&
since_last > self.policy.cooldown {
self.last_emitted_step = update.step
Some({
step: update.step,
observation: update.observation,
probability: update.change_probability,
prior_run_length: update.most_likely_run_length,
})
} else {
None
}
}
///|
/// Reset only the debounce state, retaining threshold configuration.
pub fn EventGate::reset(self : EventGate) -> Unit {
self.last_emitted_step = -1000000000
}