f6a8752d00
--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
63 lines
2.1 KiB
Go
63 lines
2.1 KiB
Go
package router
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/kami/maven/internal/lexicon"
|
|
)
|
|
|
|
// 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
|
|
// nothing and never depends on the model that produced the routing decision.
|
|
//
|
|
// It exists because a mis-routed question used to be persisted as a fact
|
|
// about the owner, with the model's invented answer as the value (#470). The
|
|
// predicate is deliberately blunt: refusing to store a question is cheap and
|
|
// reversible, storing an invented fact about him is neither.
|
|
func IsQuestionShaped(text string) bool {
|
|
t := strings.TrimSpace(text)
|
|
if t == "" {
|
|
return false
|
|
}
|
|
toks := planTokens(t)
|
|
for _, v := range captureVerbs {
|
|
if hasTok(toks, v) {
|
|
return false
|
|
}
|
|
}
|
|
if strings.HasSuffix(t, "?") {
|
|
return true
|
|
}
|
|
for _, w := range interrogatives {
|
|
if hasTok(toks, w) {
|
|
return true
|
|
}
|
|
}
|
|
for _, w := range narrativeRequests {
|
|
if hasTok(toks, w) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|