Watch
1
0
Fork
You've already forked souveraine
0

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:
Fimeg 2026-08-14 20:30:00 -04:00
commit af801b885f

View file

@ -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();