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.
|
||||
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 {
|
||||
matches!(
|
||||
state,
|
||||
|
|
@ -1541,6 +1558,12 @@ pub struct DeviceStateMachine {
|
|||
/// Whether the tunnel was deaf last time we looked, so the trail records
|
||||
/// the edge rather than one entry per tick.
|
||||
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
|
||||
|
|
@ -1622,6 +1645,7 @@ impl DeviceStateMachine {
|
|||
bearer_applied: None,
|
||||
bearer_candidate_since: None,
|
||||
bearer_deaf: false,
|
||||
double_tap_refusals: std::collections::VecDeque::new(),
|
||||
};
|
||||
|
||||
// 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
|
||||
/// reports vanish silently is indistinguishable from a dead one.
|
||||
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 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();
|
||||
warn!(
|
||||
"[device-state] {:?} refused — believed {:?} at {:.2}",
|
||||
|
|
@ -3959,6 +4017,58 @@ mod tests {
|
|||
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]
|
||||
fn waking_from_a_dimmed_blank_restores_brightness_first() {
|
||||
let (mut sm, t0) = locked_and_lit();
|
||||
|
|
|
|||
Loading…
Reference in a new issue