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 }