pattern: a burst of taps is not a routine (V-468)

Detect had no floor on the interval. Four events minutes apart give gaps
near 0.002 days, every one of them inside the ±50% band, so it proposed a
routine and PhraseRoutine called it "каждый день".

UNIQUE(action, object) makes that unrecoverable: dismissing the bogus
proposal burns the pair, and the real routine behind it can never be
proposed again. It also made hand-QA unsafe — seeding a pattern with four
chat turns poisoned the pair being tested.

The floor is two hours against the median, not a day, because meals, water
and breaks are genuine several-times-a-day habits.
This commit is contained in:
2026-08-04 03:00:11 +04:00
parent c915115096
commit 569991bb15
2 changed files with 67 additions and 2 deletions
+43
View File
@@ -216,3 +216,46 @@ func TestDetectMedianBandNotExtremes(t *testing.T) {
})
}
}
// A burst is not a habit. Four taps of the same key minutes apart give
// intervals near 0.002 days, all inside the ±50% band by construction, so the
// detector called it a daily routine (Vikunja #468). Dismissing that proposal
// burns the action+object pair permanently, which also made hand-QA of the
// detector unsafe.
func TestDetectRejectsABurst(t *testing.T) {
base := time.Date(2026, 8, 4, 9, 0, 0, 0, time.UTC)
var events []Event
for i := 0; i < 4; i++ {
events = append(events, Event{
Action: "refill", Object: "cat_water",
Ts: base.Add(time.Duration(i) * 7 * time.Minute),
})
}
r, err := Detect(events)
if err != nil {
t.Fatalf("Detect: %v", err)
}
if r != nil {
t.Fatalf("four taps minutes apart proposed a routine every %.3f days", r.IntervalDays)
}
}
// The floor is two hours, not a day: a habit that runs several times a day is
// still a habit.
func TestDetectKeepsASeveralTimesADayHabit(t *testing.T) {
base := time.Date(2026, 8, 4, 8, 0, 0, 0, time.UTC)
var events []Event
for i := 0; i < 5; i++ {
events = append(events, Event{
Action: "drink", Object: "water",
Ts: base.Add(time.Duration(i) * 4 * time.Hour),
})
}
r, err := Detect(events)
if err != nil {
t.Fatalf("Detect: %v", err)
}
if r == nil {
t.Fatal("a four-hour rhythm over five events is a habit, got nil")
}
}