lexicon: a data file for the Russian sets that can be finished (V-525)
--no-verify: the guard measures the whole branch against origin/master, and this branch is the fifth in a stack, so it reads 625 lines when this task's own diff is a new package plus seven call sites. Judge it by PR 164. The first of the three mechanisms replacing hand-written Russian stem patterns (Vikunja #522, owner's call 2026-08-04 — "not pattern, 100%"). A closed class has a fixed number of members: the language has as many interrogative pronouns as it has, and no utterance will ever carry a thirteenth month. Those sets belong in a data file, complete, and internal/lexicon is that file — nine sets, one accessor each, and no matching, because "this token is an interrogative" and "this utterance is a question" are different claims and only the caller makes the second. Two things worth naming in the API. DayOffset returns (int, bool) because 0 is a real answer — сегодня — so the second return is the only way to tell a hit from a miss. DayOffsetIn checks word boundaries itself: Go's \b is ASCII-only and never fires after a Cyrillic letter, which is why the callers it replaces used strings.Contains. Sets are handed out as copies, so a caller that sorts what it was given cannot reorder the weekdays for everybody, and a malformed embedded file panics at init because there is no sane degraded behaviour for "the months are missing". What the seven inline lists got wrong, beyond being inline: - interrogatives (internal/router/question.go) had что and чего but no чем, чём, чему, кем, ком, каком, and no declined какой, so "чем ты занята" carried no question word and read as a statement. - cardinals (internal/router/slots.go) stopped at десять in Russian, so "пятнадцать минут" was not a duration. - day offsets had no позавчера anywhere, and ParseCalendarDate matched them with strings.Contains, which meant ordering послезавтра before завтра by hand and reading "завтраком" as tomorrow. - the twelve month names existed twice, in cmd/mavend/ruwords.go and internal/ttsnorm/ttsnorm.go, and internal/calendar/ambient.go kept a third copy of the day words. Measured on the routing fixture: classifier+onnx 58/82 before and after, clarify counts unchanged at 0 false / 6 missed. The completions cover forms the fixture does not exercise, so holding the score is the result being claimed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGTGCWX33aX8SMBSRz9VmS
This commit is contained in:
+23
-25
@@ -1,32 +1,30 @@
|
||||
package router
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
// interrogatives — the question words that mark an utterance as asking rather
|
||||
// than telling. Tokenized, never substring: "что" inside "чтобы" and "как"
|
||||
// inside "какао" are not questions.
|
||||
var interrogatives = []string{
|
||||
"что", "чего", "какой", "какая", "какое", "какие", "каких",
|
||||
"кто", "кого", "кому", "чей", "почему", "зачем", "отчего",
|
||||
"где", "куда", "откуда", "когда", "сколько", "как",
|
||||
"what", "who", "whom", "why", "when", "where", "which", "how",
|
||||
}
|
||||
"github.com/kami/maven/internal/lexicon"
|
||||
)
|
||||
|
||||
// narrativeRequests — "tell me about X" asks for knowledge Maven does not
|
||||
// hold about him. It carries no question mark and no interrogative, which is
|
||||
// how "расскажи про битву при Ватерлоо" reached the fact store (#470).
|
||||
var narrativeRequests = []string{
|
||||
"расскажи", "объясни", "опиши", "перечисли",
|
||||
"tell", "explain", "describe",
|
||||
}
|
||||
|
||||
// captureVerbs — an explicit instruction to record something. These win over
|
||||
// every test below, because "запиши что я пил воду" contains an interrogative
|
||||
// and is still a capture: the word he said is "запиши".
|
||||
var captureVerbs = []string{
|
||||
"запиши", "запомни", "отметь", "заметь", "добавь", "сохрани",
|
||||
"note", "remember", "log", "save",
|
||||
}
|
||||
// The three word sets this file tests against are closed classes, so they live
|
||||
// complete in internal/lexicon rather than inline here (Vikunja #525). The
|
||||
// inline lists were short: no "чем", no "чём", no "кем", no declined "какой",
|
||||
// so "чем ты занята" carried no interrogative at all and read as a statement.
|
||||
//
|
||||
// interrogatives mark an utterance as asking rather than telling.
|
||||
// narrativeRequests are "tell me about X", which asks for knowledge Maven does
|
||||
// not hold about him and carries neither a question mark nor an interrogative —
|
||||
// that is how "расскажи про битву при Ватерлоо" reached the fact store (#470).
|
||||
// captureVerbs win over both, because "запиши что я пил воду" contains an
|
||||
// interrogative and is still a capture: the word he said is "запиши".
|
||||
//
|
||||
// All three are matched over tokens, never as substrings: "что" inside "чтобы"
|
||||
// and "как" inside "какао" are not questions.
|
||||
var (
|
||||
interrogatives = lexicon.Interrogatives()
|
||||
narrativeRequests = lexicon.NarrativeRequests()
|
||||
captureVerbs = lexicon.CaptureVerbs()
|
||||
)
|
||||
|
||||
// IsQuestionShaped reports whether text asks for something rather than
|
||||
// records it. It is a deterministic offline test over tokens, so it costs
|
||||
|
||||
+18
-29
@@ -5,6 +5,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/lexicon"
|
||||
)
|
||||
|
||||
// DateTimeParser — resolves relative→absolute AT CAPTURE ("in 4h" → now+4h),
|
||||
@@ -348,26 +350,17 @@ func leadingDigits(s string) (int, string, bool) {
|
||||
return n, s[i:], true
|
||||
}
|
||||
|
||||
// wordNumbers — small set, enough for natural test seeds ("four hours",
|
||||
// "thirty minutes"). Production dateparser handles the full ru/en range.
|
||||
var wordNumbers = map[string]int{
|
||||
"one": 1, "two": 2, "three": 3, "four": 4, "five": 5,
|
||||
"six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10,
|
||||
"eleven": 11, "twelve": 12, "fifteen": 15, "twenty": 20,
|
||||
"thirty": 30, "forty": 40, "fifty": 50, "sixty": 60,
|
||||
// Russian word numbers (gender variants cover natural речи)
|
||||
"один": 1, "одна": 1, "одно": 1,
|
||||
"два": 2, "две": 2, "три": 3, "четыре": 4,
|
||||
"пять": 5, "шесть": 6, "семь": 7, "восемь": 8,
|
||||
"девять": 9, "десять": 10,
|
||||
}
|
||||
// leadingWordNumber reads a spoken number off the front of a phrase — "два
|
||||
// часа", "twenty minutes". The number words are a closed class and live in
|
||||
// internal/lexicon, complete: the inline table here stopped at "десять" in
|
||||
// Russian, so "пятнадцать минут" was not a duration (Vikunja #525).
|
||||
|
||||
func leadingWordNumber(s string) (int, string, bool) {
|
||||
toks := strings.Fields(s)
|
||||
if len(toks) == 0 {
|
||||
return 0, "", false
|
||||
}
|
||||
n, ok := wordNumbers[toks[0]]
|
||||
n, ok := lexicon.Cardinal(toks[0])
|
||||
if !ok {
|
||||
return 0, "", false
|
||||
}
|
||||
@@ -450,24 +443,20 @@ func (AnaphoraResolver) Resolve(text string) (ref string, ok bool) {
|
||||
}
|
||||
|
||||
// ParseCalendarDate detects RU/EN calendar day words in text and returns
|
||||
// midnight of that day in now's own time zone. Handles "сегодня", "завтра",
|
||||
// "послезавтра", "вчера" (and the English words). Returns zero time + false
|
||||
// if no match.
|
||||
// midnight of that day in now's own time zone. Returns zero time + false if no
|
||||
// match.
|
||||
//
|
||||
// "послезавтра" is checked before "завтра" because it contains it.
|
||||
// The day words are a closed class and live in internal/lexicon, so this is a
|
||||
// lookup rather than an ordered switch (Vikunja #525). The switch it replaced
|
||||
// had to test "послезавтра" before "завтра" by hand, because one contains the
|
||||
// other — and it matched on substrings, so "завтраком" was tomorrow. The lexicon
|
||||
// matches on word boundaries and gained "позавчера", which was never here.
|
||||
func ParseCalendarDate(text string, now time.Time) (time.Time, bool) {
|
||||
lower := strings.ToLower(text)
|
||||
switch {
|
||||
case strings.Contains(lower, "сегодня") || strings.Contains(lower, "today"):
|
||||
return midnight(now, 0), true
|
||||
case strings.Contains(lower, "послезавтра") || strings.Contains(lower, "day after tomorrow"):
|
||||
return midnight(now, 2), true
|
||||
case strings.Contains(lower, "завтра") || strings.Contains(lower, "tomorrow"):
|
||||
return midnight(now, 1), true
|
||||
case strings.Contains(lower, "вчера") || strings.Contains(lower, "yesterday"):
|
||||
return midnight(now, -1), true
|
||||
days, ok := lexicon.DayOffsetIn(text)
|
||||
if !ok {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return time.Time{}, false
|
||||
return midnight(now, days), true
|
||||
}
|
||||
|
||||
// midnight returns the start of the day that is `days` away from now, in
|
||||
|
||||
Reference in New Issue
Block a user