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>
194 lines
6.5 KiB
Go
194 lines
6.5 KiB
Go
package router
|
||
|
||
import (
|
||
"strings"
|
||
"unicode"
|
||
|
||
"github.com/kami/maven/internal/lexicon"
|
||
"github.com/kami/maven/internal/morph"
|
||
)
|
||
|
||
// ProhibitedActFn is the stage-0 fn used for a command whose only authority is
|
||
// negative: the user explicitly told Maven not to perform it. It is not an
|
||
// executable tool name. The daemon consumes it as a deterministic no-op before
|
||
// any local or ecosystem executor is considered.
|
||
const ProhibitedActFn = "prohibited_act"
|
||
|
||
// CommandProhibition is the structural evidence carried by a direct negative
|
||
// command. Body begins with the verb governed by the prohibition, with address
|
||
// and politeness framing removed. Keeping the body available lets tests and
|
||
// future policy distinguish the scope without recovering it from substrings.
|
||
type CommandProhibition struct {
|
||
Body []string
|
||
}
|
||
|
||
// ParseCommandProhibition recognises an addressed prohibition at command
|
||
// position. It is deliberately a token grammar, not a search for "не"/"not":
|
||
// first-person reports ("я не закрыл"), questions and a negative clause after
|
||
// another request never grant or revoke execution authority.
|
||
//
|
||
// The small scope exceptions are semantic command frames of their own. "не
|
||
// забудь напомнить" / "don't forget to remind" is an affirmative reminder,
|
||
// and "не мог бы ты ..." is ordinary modal politeness. They must not be
|
||
// flattened into a refusal merely because their first surface token is
|
||
// negative. A forget frame is exempt only when it actually contains Maven's
|
||
// reminder verb; "не забудь закрыть задачу" remains an ambiguous action and is
|
||
// conservatively refused rather than allowed to mutate the board.
|
||
func ParseCommandProhibition(text string) (CommandProhibition, bool) {
|
||
tokens := commandFrameTokens(text)
|
||
for len(tokens) > 0 && commandLead(tokens[0]) {
|
||
tokens = tokens[1:]
|
||
}
|
||
if len(tokens) < 2 {
|
||
return CommandProhibition{}, false
|
||
}
|
||
|
||
body := tokens
|
||
switch {
|
||
case len(tokens) >= 3 && tokens[0] == "только" && tokens[1] == "не":
|
||
body = tokens[2:]
|
||
case len(tokens) >= 6 && tokens[0] == "ни" && tokens[1] == "в" && tokens[2] == "коем" && tokens[3] == "случае" && tokens[4] == "не":
|
||
body = tokens[5:]
|
||
case len(tokens) >= 5 && tokens[0] == "ни" && tokens[1] == "за" && tokens[2] == "что" && tokens[3] == "не":
|
||
body = tokens[4:]
|
||
case tokens[0] == "не":
|
||
body = tokens[1:]
|
||
case tokens[0] == "никогда":
|
||
body = tokens[1:]
|
||
if len(body) > 0 && body[0] == "не" {
|
||
body = body[1:]
|
||
}
|
||
case tokens[0] == "don't" || tokens[0] == "dont":
|
||
body = tokens[1:]
|
||
case len(tokens) >= 3 && tokens[0] == "do" && tokens[1] == "not":
|
||
body = tokens[2:]
|
||
case tokens[0] == "never":
|
||
body = tokens[1:]
|
||
default:
|
||
return CommandProhibition{}, false
|
||
}
|
||
if len(body) == 0 || affirmativeNegativeFrame(body) || selfStateReport(body) {
|
||
return CommandProhibition{}, false
|
||
}
|
||
return CommandProhibition{Body: append([]string(nil), body...)}, true
|
||
}
|
||
|
||
// IsCommandProhibition is the execution-belt predicate. Callers use the same
|
||
// structural evidence at routing, dialogue and executor boundaries so a model
|
||
// cannot recover authority by changing the intent or rewriting the text slot.
|
||
func IsCommandProhibition(text string) bool {
|
||
_, ok := ParseCommandProhibition(text)
|
||
return ok
|
||
}
|
||
|
||
// CommandProhibitionGrammar gives a direct prohibition a deterministic stage-0
|
||
// route. The sentinel is intentionally an Act: it reaches the same daemon
|
||
// policy as a model-routed action, but can never collide with an enabled tool.
|
||
func CommandProhibitionGrammar() Grammar {
|
||
return Grammar{
|
||
Name: "command-prohibition",
|
||
Decide: func(utterance string) (Decision, bool) {
|
||
if !IsCommandProhibition(utterance) {
|
||
return Decision{}, false
|
||
}
|
||
return Decision{
|
||
Stage: 0,
|
||
Intent: IntentAct,
|
||
Confidence: 1,
|
||
Slots: Slots{
|
||
Fn: ProhibitedActFn,
|
||
HasFn: true,
|
||
},
|
||
}, true
|
||
},
|
||
}
|
||
}
|
||
|
||
// commandFrameTokens keeps apostrophes inside a word so don't is one grammar
|
||
// token. Every other punctuation rune is a boundary. This is intentionally
|
||
// local rather than reusing praxisTokens, whose underscore/hyphen policy is for
|
||
// item ids, not spoken command mood.
|
||
func commandFrameTokens(text string) []string {
|
||
var out []string
|
||
var word []rune
|
||
flush := func() {
|
||
for len(word) > 0 && word[len(word)-1] == '\'' {
|
||
word = word[:len(word)-1]
|
||
}
|
||
if len(word) > 0 {
|
||
out = append(out, string(word))
|
||
}
|
||
word = word[:0]
|
||
}
|
||
for _, r := range strings.ToLower(text) {
|
||
switch {
|
||
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
||
word = append(word, r)
|
||
case (r == '\'' || r == '’') && len(word) > 0:
|
||
word = append(word, '\'')
|
||
default:
|
||
flush()
|
||
}
|
||
}
|
||
flush()
|
||
return out
|
||
}
|
||
|
||
func commandLead(token string) bool {
|
||
if lexicon.IsFillerParticle(token) {
|
||
return true
|
||
}
|
||
switch token {
|
||
case "maven", "мавен", "мавена", "мэйвен", "мейвен", "майвен", "мэвен":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// selfStateReport declines a negation whose head describes the speaker rather
|
||
// than an action Maven could take. "ну не знаю" is an unclear answer to a parked
|
||
// question and must reach the clarify ladder; consuming it as a prohibition
|
||
// ended the dialogue with "хорошо, не буду" and dropped the pending reminder.
|
||
func selfStateReport(body []string) bool {
|
||
for _, verb := range lexicon.SelfStateVerbs() {
|
||
if body[0] == verb || morph.SameWord(body[0], verb) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func affirmativeNegativeFrame(body []string) bool {
|
||
if len(body) == 0 {
|
||
return false
|
||
}
|
||
// Negative-polarity modal politeness: the complete bounded frame "не мог
|
||
// бы ты ..." / "не могли бы вы ..." asks for the nested action; a bare
|
||
// "не мог перезапустить" is instead a report and must not erase the safety
|
||
// belt merely because its modal has the same lemma.
|
||
if len(body) >= 4 && morph.SameWord(body[0], "мочь") && body[1] == "бы" &&
|
||
(body[2] == "ты" || body[2] == "вы") {
|
||
return true
|
||
}
|
||
if body[0] != "forget" && !morph.SameWord(body[0], "забыть") {
|
||
return false
|
||
}
|
||
// The exception is the nested reminder request itself, not a reminder word
|
||
// somewhere later in an unrelated sentence. English may carry infinitival
|
||
// "to" between the two verbs; Russian does not need a joiner.
|
||
at := 1
|
||
if at < len(body) && body[at] == "to" {
|
||
at++
|
||
}
|
||
if at >= len(body) {
|
||
return false
|
||
}
|
||
for _, reminderVerb := range lexicon.ReminderVerbs() {
|
||
if body[at] == reminderVerb || morph.SameWord(body[at], reminderVerb) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|