8015fdbb79
Replace nearest-neighbour personal routing with a frozen class-balanced linear head measured on historical, stratified, cross-validation, holdout, and fresh challenge gates (V-702). Close the four repair handoff holes, preserve nested clarification flows, and route Russian possession statements through structural grammar rather than lexical exceptions (V-573). Owner explicitly requested direct commits to master.
71 lines
4.4 KiB
Go
71 lines
4.4 KiB
Go
package router
|
||
|
||
// StageZeroGrammars — the stage 0 rule set, in the order the daemon runs it.
|
||
//
|
||
// It lives here because it used to live in two places (V-693). `buildRouter` in
|
||
// cmd/mavend/voicewire.go held the real set and `baselineGrammars` in
|
||
// internal/router/eval/eval_test.go restated it by hand, in the daemon's order,
|
||
// with its own comment saying so. Three test files score against the fixture,
|
||
// and nothing compared the two lists. By 2026-08-11 they had already drifted:
|
||
// BareCaptureGrammar was in the daemon and not in the fixture, so every routing
|
||
// measurement scored a set nobody ran. That is the failure CLAUDE.md warned
|
||
// about by name, and a diff test would have caught it one grammar late. One
|
||
// list cannot drift from itself.
|
||
//
|
||
// The order is the contract, not the membership. Each rule below says why it
|
||
// sits where it sits, and a rule inserted in the wrong place changes which
|
||
// utterances the cascade never reaches. Read the comment above a line before
|
||
// moving it, and read docs/routing.md before adding one.
|
||
//
|
||
// The classifier, the extractor, the threshold and the model arm are the
|
||
// daemon's to assemble. This function returns the rules and nothing else, so
|
||
// the fixture can also run them one at a time and see which of them contend for
|
||
// the same utterance, which the cascade hides by stopping at the first match.
|
||
func StageZeroGrammars(acts ActMatcher) []Grammar {
|
||
grammars := DefaultGrammars(acts)
|
||
grammars = append(grammars, SystemTimeDateGrammars()...)
|
||
// After the time/date rules on purpose: "какой сегодня день" is a clock
|
||
// question and must keep reaching replySystem, while "что у меня сегодня"
|
||
// is an agenda question and must not.
|
||
grammars = append(grammars, AgendaQueryGrammars()...)
|
||
// Same reason as the agenda rules, for the feeds: "что нового в лентах?"
|
||
// routed system and answered "пока не умею" (Vikunja #474).
|
||
// After the agenda rules, which are the narrower claim, and BEFORE the feed
|
||
// and list rules, which are not: "что такое лента" is a definition question
|
||
// and the feed rule would take it on the noun alone (V-655).
|
||
grammars = append(grammars, WorldQueryGrammars()...)
|
||
grammars = append(grammars, FeedQueryGrammar())
|
||
// The list side of the same exposure: a phrasing with no possessive in it
|
||
// ("список дел") routed system and never reached queryTasks (Vikunja #467).
|
||
grammars = append(grammars, TaskListGrammar())
|
||
grammars = append(grammars, ListGrammars()...)
|
||
grammars = append(grammars, ReminderGrammar())
|
||
// Before the capture marker, because "отметь" is a capture verb and "отметь
|
||
// второй пункт" is not a note. The Praxis rules are the narrower claim — a
|
||
// lifecycle verb AND an item named — so they get first refusal (Vikunja #516).
|
||
grammars = append(grammars, PraxisGrammars()...)
|
||
// After Praxis, whose bare "закрой" claim this rule cannot reach (it needs the
|
||
// board noun), and before the capture marker, which would otherwise read
|
||
// "убери из задач купить молоко" as a new task (Vikunja #512).
|
||
grammars = append(grammars, TaskStatusGrammar())
|
||
// Before the capture markers, which all need an object. A capture verb
|
||
// alone is a fact with no key, and the clarify path asks for it rather than
|
||
// letting the model invent an answer (Vikunja #557).
|
||
grammars = append(grammars, BareCaptureGrammar()...)
|
||
// Last, and it matches any utterance shape — its Build is the filter. An
|
||
// explicit capture marker beats the model, which called it an act and
|
||
// rewrote the task text (Vikunja #467). After the rules above because a
|
||
// marker never collides with a clock or agenda question.
|
||
grammars = append(grammars, TaskCaptureGrammar())
|
||
// After the capture marker, so "запиши" still wins over "расскажи", and
|
||
// last overall because it matches on the first word alone: "расскажи про
|
||
// X" is a world question the model called a fact (Vikunja #498).
|
||
grammars = append(grammars, NarrativeQueryGrammars()...)
|
||
// Last because it is deliberately broad over the open remainder of a
|
||
// grammatical declaration. Every explicit question, command, capture and
|
||
// narrative request above gets first refusal; this catches the Russian
|
||
// present-tense possession statement the statistical floors call a query.
|
||
grammars = append(grammars, PossessionStatementGrammar())
|
||
return grammars
|
||
}
|