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 }