fail the pocket veto open on a double-tap burst
suppress_wake is a pure function of placement with no memory, and its only measured effect was refusing Casey: 17 double-tap refusals in 10 s, zero true pockets in ~10 MB of trail (DUMP-taskdocs-2026-08-14 §2). FTS reports DBLTAP only for the deliberate gesture, so three refused double-taps inside five seconds is a person insisting — the veto now fails open on the third and says input-burst-allowed in the trail. Spaced taps never sum; the memory clears on allow.
This commit is contained in:
parent
c31fa10ec9
commit
af801b885f
1 changed files with 110 additions and 0 deletions
|
|
@ -1420,6 +1420,23 @@ pub struct Placement {
|
||||||
/// and not the shape.
|
/// and not the shape.
|
||||||
const POCKET_VETO_CONFIDENCE: f32 = 0.45;
|
const POCKET_VETO_CONFIDENCE: f32 = 0.45;
|
||||||
|
|
||||||
|
/// How close together refused double-taps must fall to read as a burst.
|
||||||
|
///
|
||||||
|
/// Off the trail, 2026-08-14 (DUMP-taskdocs §2): the only refusal burst on
|
||||||
|
/// record — all three generations, ~10 MB — is 17 `DoubleTapToWake` refusals
|
||||||
|
/// in 10 s, tightening from one per 2 s to two per second. That is Casey at a
|
||||||
|
/// dark screen tapping harder because nothing happened. The veto's true
|
||||||
|
/// positives are zero; it has never once refused an actual pocket.
|
||||||
|
pub const DOUBLE_TAP_BURST_WINDOW: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// How many refused double-taps inside the window override the veto.
|
||||||
|
///
|
||||||
|
/// Three. The FTS controller reports DBLTAP only for the deliberate gesture
|
||||||
|
/// (PAF/touch.md), so an accelerating run of them is a person insisting, and
|
||||||
|
/// a pocket produces nothing of the kind. The veto keeps the first two — a
|
||||||
|
/// tap or two against fabric is plausible — and fails open on the third.
|
||||||
|
pub const DOUBLE_TAP_BURST_COUNT: usize = 3;
|
||||||
|
|
||||||
pub fn is_locked(state: DeviceState) -> bool {
|
pub fn is_locked(state: DeviceState) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
state,
|
state,
|
||||||
|
|
@ -1541,6 +1558,12 @@ pub struct DeviceStateMachine {
|
||||||
/// Whether the tunnel was deaf last time we looked, so the trail records
|
/// Whether the tunnel was deaf last time we looked, so the trail records
|
||||||
/// the edge rather than one entry per tick.
|
/// the edge rather than one entry per tick.
|
||||||
bearer_deaf: bool,
|
bearer_deaf: bool,
|
||||||
|
/// When double-tap wakes were refused, one entry each, pruned to
|
||||||
|
/// [`DOUBLE_TAP_BURST_WINDOW`]. The memory [`suppress_wake`] does not
|
||||||
|
/// have: the veto is a pure function of placement, so the burst escape
|
||||||
|
/// lives in `note_input_gated`. Cleared when a wake is allowed, so an
|
||||||
|
/// allow never counts toward the next burst.
|
||||||
|
double_tap_refusals: std::collections::VecDeque<Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sensor inputs that feed the Observed state. Each is a reading, not
|
/// Sensor inputs that feed the Observed state. Each is a reading, not
|
||||||
|
|
@ -1622,6 +1645,7 @@ impl DeviceStateMachine {
|
||||||
bearer_applied: None,
|
bearer_applied: None,
|
||||||
bearer_candidate_since: None,
|
bearer_candidate_since: None,
|
||||||
bearer_deaf: false,
|
bearer_deaf: false,
|
||||||
|
double_tap_refusals: std::collections::VecDeque::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// First entry of the run, and the only one that proves the trail is
|
// First entry of the run, and the only one that proves the trail is
|
||||||
|
|
@ -2371,7 +2395,41 @@ impl DeviceStateMachine {
|
||||||
/// that absence must never be mistaken for a negative; a producer whose
|
/// that absence must never be mistaken for a negative; a producer whose
|
||||||
/// reports vanish silently is indistinguishable from a dead one.
|
/// reports vanish silently is indistinguishable from a dead one.
|
||||||
pub fn note_input_gated(&mut self, trigger: InputTrigger) -> Option<Vec<Action>> {
|
pub fn note_input_gated(&mut self, trigger: InputTrigger) -> Option<Vec<Action>> {
|
||||||
|
self.note_input_gated_at(trigger, Instant::now())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `note_input_gated` with the clock passed in, so the burst window is
|
||||||
|
/// testable without sleeping — the same split `tick`/`tick_at` has.
|
||||||
|
pub fn note_input_gated_at(
|
||||||
|
&mut self,
|
||||||
|
trigger: InputTrigger,
|
||||||
|
now: Instant,
|
||||||
|
) -> Option<Vec<Action>> {
|
||||||
if self.suppress_wake(trigger) {
|
if self.suppress_wake(trigger) {
|
||||||
|
if matches!(trigger, InputTrigger::DoubleTapToWake) {
|
||||||
|
while self
|
||||||
|
.double_tap_refusals
|
||||||
|
.front()
|
||||||
|
.is_some_and(|t| now.saturating_duration_since(*t) > DOUBLE_TAP_BURST_WINDOW)
|
||||||
|
{
|
||||||
|
self.double_tap_refusals.pop_front();
|
||||||
|
}
|
||||||
|
self.double_tap_refusals.push_back(now);
|
||||||
|
if self.double_tap_refusals.len() >= DOUBLE_TAP_BURST_COUNT {
|
||||||
|
self.double_tap_refusals.clear();
|
||||||
|
self.record_decision(
|
||||||
|
"input-burst-allowed",
|
||||||
|
serde_json::json!({
|
||||||
|
"trigger": format!("{:?}", trigger),
|
||||||
|
"placement": self.placement(),
|
||||||
|
"count": DOUBLE_TAP_BURST_COUNT,
|
||||||
|
"window_secs": DOUBLE_TAP_BURST_WINDOW.as_secs(),
|
||||||
|
}),
|
||||||
|
"an accelerating run of deliberate double-taps overrides the pocket belief — fail-open, and the trail says so (DUMP 2026-08-14 §2)",
|
||||||
|
);
|
||||||
|
return Some(self.note_input(trigger));
|
||||||
|
}
|
||||||
|
}
|
||||||
let placement = self.placement();
|
let placement = self.placement();
|
||||||
warn!(
|
warn!(
|
||||||
"[device-state] {:?} refused — believed {:?} at {:.2}",
|
"[device-state] {:?} refused — believed {:?} at {:.2}",
|
||||||
|
|
@ -3959,6 +4017,58 @@ mod tests {
|
||||||
assert!(!sm.suppress_wake(InputTrigger::DoubleTapToWake));
|
assert!(!sm.suppress_wake(InputTrigger::DoubleTapToWake));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_burst_of_double_taps_fails_the_pocket_veto_open() {
|
||||||
|
let (mut sm, t0) = locked_and_lit();
|
||||||
|
sm.sensor_evidence.proximity_near = true;
|
||||||
|
sm.mark_evidence_seen(SensorSource::Proximity);
|
||||||
|
assert!(sm.suppress_wake(InputTrigger::DoubleTapToWake));
|
||||||
|
|
||||||
|
assert!(sm
|
||||||
|
.note_input_gated_at(InputTrigger::DoubleTapToWake, t0)
|
||||||
|
.is_none());
|
||||||
|
assert!(sm
|
||||||
|
.note_input_gated_at(InputTrigger::DoubleTapToWake, t0 + Duration::from_secs(1))
|
||||||
|
.is_none());
|
||||||
|
// The third inside the window is a person insisting: fail open.
|
||||||
|
assert!(sm
|
||||||
|
.note_input_gated_at(InputTrigger::DoubleTapToWake, t0 + Duration::from_secs(2))
|
||||||
|
.is_some());
|
||||||
|
|
||||||
|
let bursts: Vec<_> = sm
|
||||||
|
.forensic
|
||||||
|
.recent(50)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|e| matches!(&e.event, ForensicEvent::Decision { decision, .. } if decision == "input-burst-allowed"))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(bursts.len(), 1, "the override is said, once, in the trail");
|
||||||
|
|
||||||
|
// The memory discharged with the allow: the next tap starts a fresh
|
||||||
|
// count rather than riding the burst.
|
||||||
|
assert!(sm
|
||||||
|
.note_input_gated_at(InputTrigger::DoubleTapToWake, t0 + Duration::from_secs(3))
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spaced_double_taps_do_not_sum_into_a_burst() {
|
||||||
|
let (mut sm, t0) = locked_and_lit();
|
||||||
|
sm.sensor_evidence.proximity_near = true;
|
||||||
|
sm.mark_evidence_seen(SensorSource::Proximity);
|
||||||
|
|
||||||
|
// 6 s apart is outside the window: each stands alone, so a pocket
|
||||||
|
// brushing the screen twice in a minute never opens it.
|
||||||
|
assert!(sm
|
||||||
|
.note_input_gated_at(InputTrigger::DoubleTapToWake, t0)
|
||||||
|
.is_none());
|
||||||
|
assert!(sm
|
||||||
|
.note_input_gated_at(InputTrigger::DoubleTapToWake, t0 + Duration::from_secs(6))
|
||||||
|
.is_none());
|
||||||
|
assert!(sm
|
||||||
|
.note_input_gated_at(InputTrigger::DoubleTapToWake, t0 + Duration::from_secs(12))
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn waking_from_a_dimmed_blank_restores_brightness_first() {
|
fn waking_from_a_dimmed_blank_restores_brightness_first() {
|
||||||
let (mut sm, t0) = locked_and_lit();
|
let (mut sm, t0) = locked_and_lit();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue