a97764c5f7
MavenHelpGrammar keeps "как отменить напоминание" on SourceSelf, where the answer names the command Maven accepts, instead of leaking to search. PublicCurrentVersionGrammar anchors an explicitly current release on SourceWorld and declines first-person ownership. AmbiguousFragmentGrammar refuses filler plus an unresolved demonstrative rather than letting a statistical head invent context. ImplicitElapsedQueryGrammar reads Russian question word order in "давно я не тренировался" as recall; the declarative order stays a statement. ReminderCancellationReportGrammar keeps "я отменил напоминание" in the non-mutating chat lane. CommandProhibitionGrammar routes a direct negative command to a sentinel fn that can never collide with an enabled tool. ActHasEntityTarget stops a bare verb or a demonstrative-only tail from crossing into Nexus. Praxis attention now accepts "что там с X" for the four service names only. taskstatus separates command mood from result words so a first-person report cannot mutate the board. question.go exports the open-question and locative shapes the recall gate reads. --no-verify: master is the working branch this session by the owner's call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
5.8 KiB
Go
92 lines
5.8 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 {
|
||
// Negative authority gets first refusal. This structural grammar reads the
|
||
// command frame, not a substring, and emits a non-executable sentinel. It
|
||
// must precede the wake-word allowlist fast path: an unusually permissive
|
||
// matcher may recognise the verb inside "Maven, don't restart nginx", but
|
||
// an allowlist match cannot turn an explicit prohibition into permission.
|
||
grammars := []Grammar{CommandProhibitionGrammar()}
|
||
grammars = append(grammars, DefaultGrammars(acts)...)
|
||
// A refusal, not a guessed intent. It is safe ahead of every positive rule
|
||
// because it accepts only filler plus unresolved-reference words.
|
||
grammars = append(grammars, AmbiguousFragmentGrammar())
|
||
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()...)
|
||
// Russian can ask "how long since" through word order rather than a
|
||
// question word. This must land in recall before a statistical head reads
|
||
// the same past-tense verb as a fact.
|
||
grammars = append(grammars, ImplicitElapsedQueryGrammar())
|
||
// 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()...)
|
||
// Questions about operating Maven herself are local product help. They sit
|
||
// beside the world anchors because both decide which side of the personal
|
||
// boundary owns an answer, in opposite directions.
|
||
grammars = append(grammars, MavenHelpGrammar())
|
||
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())
|
||
// A first-person report about a cancellation is neither another mutation
|
||
// nor a memory query. The imperative cancellation pre-route has already had
|
||
// first refusal before the router runs.
|
||
grammars = append(grammars, ReminderCancellationReportGrammar())
|
||
// 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
|
||
}
|