Files
Maven/internal/router/question.go
T
claude a97764c5f7 Add seven stage 0 frames and tighten three more (V-720)
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>
2026-08-15 17:18:48 +04:00

135 lines
4.5 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()
locativeQuestions = lexicon.LocativeInterrogatives()
narrativeRequests = lexicon.NarrativeRequests()
captureVerbs = lexicon.CaptureVerbs()
)
// CarriesCaptureVerb reports whether text tells Maven to write something down.
// Sibling of IsQuestionShaped and matched over the same tokens, and the two do
// not overlap: IsQuestionShaped returns false for anything this returns true
// for, because "запиши что я пил воду" is a capture and not a question.
//
// Both exist together so a caller can ask "is this its own request?" — a
// clarify answer that asks a question or orders a capture is not an answer
// (Vikunja #554).
func CarriesCaptureVerb(text string) bool {
toks := planTokens(strings.TrimSpace(text))
for _, v := range captureVerbs {
if hasTok(toks, v) {
return true
}
}
return false
}
// 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
}
return hasOpenQuestionTokens(toks)
}
// IsOpenQuestionShaped reports whether the words themselves ask for
// information: an interrogative ("где", "как", "which") or a narrative
// request ("расскажи", "explain"). A trailing question mark alone does not
// qualify. That distinction matters to recall: punctuation can turn an
// ordinary first-person proposition into a polar question, but it is not
// evidence that an unrelated stored note answers it.
//
// Capture verbs keep the same precedence as IsQuestionShaped, so "запиши что
// я пил" remains a write request even though it contains an interrogative.
func IsOpenQuestionShaped(text string) bool {
t := strings.TrimSpace(text)
if t == "" {
return false
}
toks := planTokens(t)
for _, v := range captureVerbs {
if hasTok(toks, v) {
return false
}
}
return hasOpenQuestionTokens(toks)
}
// IsLocativeQuestionShaped reports the open-question frame whose answer must
// locate the object or event named by the user. Recall treats that named target
// as evidence: a high cosine to an unrelated single stored note is not enough
// to answer "where is my passport?" with the location of a spare key (V-719).
//
// The words are the complete locative subset of the interrogative lexicon,
// and capture verbs retain precedence exactly as in IsQuestionShaped.
func IsLocativeQuestionShaped(text string) bool {
t := strings.TrimSpace(text)
if t == "" {
return false
}
toks := planTokens(t)
for _, v := range captureVerbs {
if hasTok(toks, v) {
return false
}
}
for _, w := range locativeQuestions {
if hasTok(toks, w) {
return true
}
}
return false
}
func hasOpenQuestionTokens(toks []string) bool {
for _, w := range interrogatives {
if hasTok(toks, w) {
return true
}
}
for _, w := range narrativeRequests {
if hasTok(toks, w) {
return true
}
}
return false
}