From 9949b309b1c575f152a72c1e7f04ac7ad4cad8e8 Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 31 Jul 2026 14:27:08 +0400 Subject: [PATCH] Don't let a time word blind the third-person check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The check asks whether anyone else was named before "он". Time words were not stoplisted, so "сегодня он не ел" read "сегодня" as the person being talked about and passed — which is the recorded break with a word in front of it, and nudges open with those words constantly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ --- internal/phraser/eval/address_time_test.go | 26 ++++++++++++++++++++++ internal/phraser/eval/checks.go | 12 +++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 internal/phraser/eval/address_time_test.go diff --git a/internal/phraser/eval/address_time_test.go b/internal/phraser/eval/address_time_test.go new file mode 100644 index 0000000..894a05c --- /dev/null +++ b/internal/phraser/eval/address_time_test.go @@ -0,0 +1,26 @@ +package eval + +import "testing" + +func TestAddressTimeWordDoesNotBlind(t *testing.T) { + // A nudge that opens with a time word must still be caught. Without the + // time words in the stoplist, "сегодня" was read as the third party. + for _, s := range []string{ + "сегодня он не ел 11 дней", + "вчера он не пил воду", + "опять он забыл про таблетки", + } { + if r := checkAddress(s); r.Pass { + t.Errorf("checkAddress(%q) passed, want a third-person failure", s) + } + } + // Still must not fire when a third party really is named. + for _, s := range []string{ + "сегодня сервис упал, он не отвечает", + "ты не пил воду четыре часа", + } { + if r := checkAddress(s); !r.Pass { + t.Errorf("checkAddress(%q) failed: %s", s, r.Detail) + } + } +} diff --git a/internal/phraser/eval/checks.go b/internal/phraser/eval/checks.go index 3a90ffe..838cbd3 100644 --- a/internal/phraser/eval/checks.go +++ b/internal/phraser/eval/checks.go @@ -340,7 +340,11 @@ func prevWord(words []string, i int) string { // - it only looks BACKWARD. "Он не отвечает, сервис упал" names the subject // after the pronoun and is flagged wrongly. // - any noun earlier in the message counts as an antecedent, even when it is -// not one ("после обеда он не ел" reads as legitimate and is missed). +// not one ("после обеда он не ел" reads as legitimate and is missed). The +// common time words are stoplisted so the usual nudge opening does not +// blind it, but a message with any other noun in front still slips through. +// This is the check's real hole; widening it further would start flagging +// legitimate third-party messages, so it stops here. // - a message that opens with "ты" and only later slips into "он" is missed, // because "ты" itself is skipped but the words around it are not. // - formal address outside these endings (short adjectives, "вашими" style @@ -392,6 +396,12 @@ var notAnAntecedent = map[string]bool{ "ещё": true, "еще": true, "тоже": true, "там": true, "тут": true, "здесь": true, "это": true, "что": true, "как": true, "когда": true, "чтобы": true, "потому": true, "сейчас": true, "потом": true, + // Time words. A nudge almost always opens with one ("сегодня он не ел"), + // and without them the very next word is read as the person being talked + // about, so the check misses the exact break it was written for. + "сегодня": true, "вчера": true, "завтра": true, "послезавтра": true, + "утром": true, "днём": true, "днем": true, "вечером": true, "ночью": true, + "опять": true, "снова": true, "весь": true, "всю": true, "целый": true, "я": true, "мне": true, "меня": true, "мной": true, "мы": true, "нас": true, "ты": true, "тебя": true, "тебе": true, "тобой": true, "твой": true, "твоя": true, "твоё": true, "твое": true, "твои": true, "твою": true,