From bddf52d1ee188c21fa7beb29d9a19f4b124abf98 Mon Sep 17 00:00:00 2001 From: kami Date: Sat, 1 Aug 2026 14:07:43 +0400 Subject: [PATCH] router: refuse a plan question about a day that is not today MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IsDayPlanQuery only rejected the сегодня family, so "какие планы на понедельник?" carried no other-day token, did carry "планы", and the plan claimed it ahead of the calendar listing and recited today under today's date. Weekday names, week, weekend and month join the refusal list. This is a refusal and not a feature: it stands until the plan can build a day other than the clock's own. isRestOfDayQuery also lived in cmd/mavend and matched by substring while IsDayPlanQuery tokenized, so the two predicates deciding one utterance could disagree, and "проверь nextcloud" read as a request for the rest of the day. It moves to the router and tokenizes. Found in review of #58. --- internal/router/calendar.go | 33 ++++++++++++++++++++++++++---- internal/router/calendar_test.go | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/internal/router/calendar.go b/internal/router/calendar.go index 3f86976..4fd7870 100644 --- a/internal/router/calendar.go +++ b/internal/router/calendar.go @@ -29,13 +29,26 @@ var dayPlanWords = []string{ "plan", "plans", "schedule", "agenda", } -// otherDayWords — a day that is not today. The plan is built for the clock's -// own day only, so an utterance naming another one belongs to the calendar -// listing instead. Claiming it here would answer the wrong day, which is worse -// than answering more tersely. +// otherDayWords — a span that is not the clock's own day. The plan can only be +// built for today, so an utterance naming another day, a weekday, a week or a +// weekend belongs to the calendar listing instead. Claiming it here would +// answer today and stamp it with today's date, which is a wrong answer where +// falling through is only a terse one. +// +// The weekday names are here as a refusal, not as a feature. "какие планы на +// понедельник?" carries no other-day token in the сегодня family and does carry +// "планы", so the plan used to claim it and recite today. var otherDayWords = []string{ "завтра", "послезавтра", "вчера", "позавчера", "tomorrow", "yesterday", + "понедельник", "вторник", "среду", "среда", "четверг", "пятницу", "пятница", + "субботу", "суббота", "воскресенье", + "понедельника", "вторника", "четверга", "пятницы", "субботы", "воскресенья", + "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", + "неделю", "неделя", "недели", "неделе", + "выходные", "выходных", "выходным", + "месяц", "месяца", "месяце", + "week", "weekend", "month", } // IsDayPlanQuery reports whether an utterance asks for today's plan (Vikunja @@ -67,6 +80,18 @@ func IsDayPlanQuery(text string) bool { (hasTok(toks, "what") && hasTok(toks, "next")) } +// IsRestOfDayQuery reports whether the utterance asks for what is left of the +// day rather than for the whole of it — "что дальше?" and its English form. +// +// Tokenized for the same reason IsDayPlanQuery is: the substring form matched +// "дальше" inside longer words and "next" inside "nextcloud", and the two +// predicates deciding the same utterance differently is worse than either +// being wrong on its own. +func IsRestOfDayQuery(text string) bool { + toks := planTokens(text) + return hasTok(toks, "дальше") || hasTok(toks, "next") +} + func hasTok(toks []string, w string) bool { for _, t := range toks { if t == w { diff --git a/internal/router/calendar_test.go b/internal/router/calendar_test.go index 4772733..7937a94 100644 --- a/internal/router/calendar_test.go +++ b/internal/router/calendar_test.go @@ -81,3 +81,38 @@ func TestIsDayPlanQuery(t *testing.T) { } } } + +// The plan is built for the clock's own day. A weekday, a week or a weekend +// carries no сегодня-family token, so the plan used to claim the utterance and +// recite today under today's date. Refusing is the right answer until the plan +// can build a day that is not the clock's own. +func TestIsDayPlanQueryRefusesOtherSpans(t *testing.T) { + for _, s := range []string{ + "какие планы на понедельник?", + "планы на пятницу", + "какие планы на неделю?", + "планы на выходные", + "какие планы на месяц?", + "what are my plans for friday?", + "my plan for the week", + } { + if IsDayPlanQuery(s) { + t.Errorf("IsDayPlanQuery(%q) = true, want false", s) + } + } +} + +// The rest-of-day test tokenizes like IsDayPlanQuery does. The substring form +// it replaced fired on any word containing "next" or "дальше". +func TestIsRestOfDayQuery(t *testing.T) { + for _, s := range []string{"что дальше?", "и что потом, дальше?", "what's next", "NEXT"} { + if !IsRestOfDayQuery(s) { + t.Errorf("IsRestOfDayQuery(%q) = false, want true", s) + } + } + for _, s := range []string{"какие планы на сегодня?", "проверь nextcloud", "дальшесъезд", ""} { + if IsRestOfDayQuery(s) { + t.Errorf("IsRestOfDayQuery(%q) = true, want false", s) + } + } +}