// Guard is the slice-21 deterministic execution-frame engine (experiment-only). // // It answers one question: given an utterance, what is its execution-frame // eligibility as a three-way gate — permissive, blocked, ambiguous — and why. // It never decides what an utterance IS (that stays with the route classifier); // it only decides whether an utterance may become an executable action at all. // The policy is asymmetric on purpose: blocked and ambiguous must never // execute, and permissive only means "no blocking speech-act evidence exists", // not "execute this". // // It reuses the shipped deterministic routers rather than inventing new ones: // // router.ParseCommandProhibition / IsCommandProhibition direct negative commands // morph.IsVerbForm / morph.Lemma verb mood and finiteness // lexicon.IsFillerParticle / FirstPerson() politeness and first-person frames // // Everything else is closed-class evidence measured on the frozen slice-20 dev // pool (§"measured discriminators" in the brief): 126 capability-question rows // split 42/42/42 across ты-addressed, bare ability (умеешь), and bare future // (сможешь) modality; 127 bare "можешь, пожалуйста" rows are 100% action; // "can you … , please" (English frame + Russian imperative) is 100% action. // The rules below are the encoding of precisely those numbers. // // The reason vocabulary is a closed set. Additions are design decisions that // must land in the report, not silent new branches. package main import ( "strings" "unicode" "github.com/kami/maven/internal/lexicon" "github.com/kami/maven/internal/morph" "github.com/kami/maven/internal/router" ) // Eligibility is the three-way execution-frame verdict. type Eligibility int const ( Permissive Eligibility = iota // no blocking speech-act evidence; downstream route decides Blocked // a speech act forbids execution (negation, question, report, …) Ambiguous // not enough evidence either way; must not execute ) func (e Eligibility) String() string { switch e { case Permissive: return "permissive" case Blocked: return "blocked" default: return "ambiguous" } } // Reason is a closed set of structural explanations for a verdict. type Reason string const ( ReasonCommandProhibition Reason = "command_prohibition" ReasonCapabilityQuestion Reason = "capability_question" ReasonReportedSpeech Reason = "reported_speech" ReasonQuotation Reason = "quotation" ReasonHypothetical Reason = "hypothetical" ReasonNegatedCommand Reason = "negated_command" ReasonExplicitRequest Reason = "explicit_request" ReasonAmbiguousModal Reason = "ambiguous_modal" ReasonNoRequestEvidence Reason = "no_request_evidence" ) func (r Reason) String() string { return string(r) } // Frame is the verdict for one utterance. Eligibility decides; Reasons explain. // A frame may carry more than one reason (e.g. a quoted reported command). type Frame struct { Eligibility Eligibility Reasons []Reason } // maybeWord is a single-token or multi-token closed expression, e.g. the // token "не мог бы" covers the three tokens не мog бы when matched as a // contiguous run ("бы" is itself a bound marker). Multi-token members are // matched over the reconstructed token text, never over raw text, so // punctuation boundaries do not defeat them. type maybeWord struct { single []string multi []string // matched as contiguous lowercased token runs } func (w maybeWord) in(toks []string, joined string) bool { if hasAny(toks, w.single) { return true } for _, m := range w.multi { tm := strings.Join(tokens(m), " ") if tm != "" && strings.Contains(joined, tm) { return true } } return false } // ── closed evidence sets (all measured on the slice-20 dev pool) ────────── // wakeAddr is stripped from the left of an utterance before command-form // detection: "мавен, выключи свет" and "выключи свет" must ride the same // frame. Closed: the names Maven answers to in the dev pool. var wakeAddr = []string{"мавен", "maven", "мавэн", "алекса", "алиса", "окей", "эй", "hey"} // ruAddress are the second-person Russian address tokens. "ты можешь …" // (with or without politeness) is 42/42 capability-question in the dev pool, // so any addressed Russian can-form is a capability question, never a request. var ruAddress = []string{"ты", "тебе", "тебя", "тобой", "тобою", "вы", "вас", "вам", "вами"} // enAddress is the English second-person address. Unlike Russian, "can you … // , please" is 96/96 action in the dev pool (English modal frame around a // Russian imperative), so English address alone never blocks: it routes to the // politeness arm. var enAddress = []string{"you", "u", "your"} // ruCanForms are the present-can verb forms. Bare (no address) "можешь …, // пожалуйста" is 127/127 action; bare "можешь …" with no politeness is the // ambiguous bucket (no such rows exist in dev — conservative default). var ruCanForms = []string{"можешь", "можете", "могу", "можем"} // ruAbilityForms are future/ability modal forms that read as a question of // capability regardless of politeness: "сможешь открыть окно, пожалуйста" and // "умеешь ли ты …" are 0/84 action in the dev pool, so even a polite bare // form never grants execution. "мог(ла) бы …" and "смог(ла) бы …" are the // conditional-politeness mask over the same boundary — except the leading // politeness construction "не мог бы ты …", which the prohibition parser // already classifies as ordinary modal politeness and must stay permissive. var ruAbilityForms = maybeWord{ single: []string{ "сможешь", "сможете", "смогу", "сможем", "сумеешь", "сумеете", "умеешь", "умеете", "способна", "способен", "способно", "способны", "смог", "смогла", "смогли", "мог", "могла", "могли", }, multi: []string{ "смог бы", "смогла бы", "смогли бы", "мог бы", "могла бы", "могли бы", "смочь бы", "мочь бы", }, } // politeNegativeModal is the leading "не мог бы ты/вы …" politeness framing the // prohibition parser exempts as ordinary modal politeness. When it leads the // utterance the capability stage declines and the frame reads as a request. var politeNegativeModal = []string{ "не мог бы", "не могла бы", "не могли бы", "не смог бы", "не смогла бы", "не смогли бы", } // enCanForms are the English modal can/could tokens. var enCanForms = []string{"can", "could"} // politeness is the closed set of politeness fillers. пожалуйста/плиз/please // are already closed-class filler particles in the lexicon; the добр-forms // are the only additions the dev pool exercises. var politeness = maybeWord{ single: []string{"пожалуйста", "плиз", "please"}, multi: []string{"будь добр", "будьте добры", "был бы добр", "были бы добры"}, } // reportVerbs are the past/third-person report verbs — the frame that reports // a command rather than issuing it. Second-person imperatives ("скажи", // "расскажи", "напомни") are deliberately absent: those are requests to // report, and their clause forms part of the current utterance, not a // replayed order. Matched as closed list (a report verb outside it is a data // gap, noted in the report). var reportVerbs = []string{ "сказал", "сказала", "сказали", "говорил", "говорила", "говорили", "говорит", "говорят", "попросил", "попросила", "попросили", "просил", "просила", "просили", "написал", "написала", "написали", "пишет", "приказал", "приказала", "приказали", "велел", "велела", "велели", "скомандовал", "скомандовала", "рекомендовал", "рекомендовала", "посоветовал", "посоветовала", "сообщил", "сообщила", "сообщили", "объявил", "объявила", "велено", "сказано", "написано", "записано", } // reportNouns name a quoted or reported text: "фраза «выключи свет»" is a // quotation, not a command. var reportNouns = []string{ "фраза", "фразы", "фразе", "фразу", "слово", "слова", "слове", "словом", "выражение", "выражения", "цитата", "цитату", "цитате", "название", "текст", "сообщение", "письмо", "заметка", "заметку", } // hypothesisMarkers open a conditional scope. var hypothesisMarkers = []string{"если", "ежели", "коли", "кабы", "if"} // illocutionVerbs make a first-person or impersonal clause a request even // without an imperative form ("я хочу …", "мне нужно …", "надо …"). var illocutionVerbs = maybeWord{ single: []string{ "хочу", "хотел", "хотела", "хотелось", "желаю", "прошу", "просим", "просил", "просила", "просили", "попросить", "надо", "нужно", "следует", "пора", "требуется", "придётся", "придется", "могу", "давай", "давайте", }, multi: []string{ "хотел бы", "хотела бы", "хочу чтобы", "хотел чтобы", "хотела чтобы", "могу ли", }, } // ── token helpers ───────────────────────────────────────────────────────── // tokens lowercases and splits on anything that is not a letter or digit, // matching the router's planTokens discipline ("что-дальше" tokenises like // "что дальше"). func tokens(text string) []string { return strings.FieldsFunc(strings.ToLower(text), func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }) } func hasTok(toks []string, w string) bool { for _, t := range toks { if t == w { return true } } return false } func hasAny(toks, ws []string) bool { for _, w := range ws { if hasTok(toks, w) { return true } } return false } func indexTok(toks []string, w string) int { for i, t := range toks { if t == w { return i } } return -1 } // isFiniteVerb reports a verb form that is not the dictionary (infinitive) // form: "выключи" is finite, "выключить" is not. A finite verb at command // position is positive request evidence; an infinitive is not. func isFiniteVerb(tok string) bool { if !morph.IsVerbForm(tok) { return false } return morph.Lemma(tok) != tok } // isInfinitive reports a token that morph resolves to its own dictionary form // (the lemma ends in the infinitive ending by construction). func isInfinitive(tok string) bool { if !morph.IsVerbForm(tok) { return false } return morph.Lemma(tok) == tok } func anyInfinitive(toks []string) bool { for _, t := range toks { if isInfinitive(t) { return true } } return false } func anyFiniteVerb(toks []string) bool { for _, t := range toks { if isFiniteVerb(t) { return true } } return false } func hasAnyVerb(toks []string) bool { for _, t := range toks { if morph.IsVerbForm(t) { return true } } return false } func blocked(rs ...Reason) Frame { return Frame{Eligibility: Blocked, Reasons: rs} } func ambiguous(rs ...Reason) Frame { return Frame{Eligibility: Ambiguous, Reasons: rs} } func permissive(rs ...Reason) Frame { return Frame{Eligibility: Permissive, Reasons: rs} } // ── quoted spans ───────────────────────────────────────────────────────── // quotedSpan is a maximal quoted interval in the normalized text. type quotedSpan struct{ content string } // quotePairs covers the quoting styles the dev pool and brief fixtures use: // Russian guillemets, curly double/single quotes, and straight quotes. var quotePairs = []struct{ open, close string }{ {"«", "»"}, {"„", "\""}, {"“", "”"}, {"‚", "‘"}, {"‘", "’"}, {"'", "'"}, {"\"", "\""}, } // extractQuotedSpans returns the contents of quoted spans in order, in rune // index space (the text is normalized, so glyphs are single runes). An // unbalanced delimiter yields no span (best-effort; the conservative // fallback then applies). func extractQuotedSpans(t string) []quotedSpan { runes := []rune(t) var out []quotedSpan i := 0 for i < len(runes) { matched := false for _, p := range quotePairs { po := []rune(p.open) pc := []rune(p.close) if i+len(po) > len(runes) || string(runes[i:i+len(po)]) != p.open { continue } j := i + len(po) for j+len(pc) <= len(runes) && string(runes[j:j+len(pc)]) != p.close { j++ } out = append(out, quotedSpan{content: string(runes[i+len(po) : j])}) i = j + len(pc) matched = true break } if !matched { i++ } } return out } // ── rule stages (evaluated in this order; a decision is final) ──────────── // Evaluate derives the execution-frame verdict for one utterance. func Evaluate(text string) Frame { t := router.NormalizeMatchText(text) if strings.TrimSpace(t) == "" { return ambiguous(ReasonNoRequestEvidence) } toks := tokens(t) joined := strings.Join(toks, " ") // 1. Quotation: a command inside a quoted span is not a command being // issued now. With a reporting frame outside it is a quotation; a bare // quote is at best ambiguous. if f, ok := stageQuotation(t, toks, joined); ok { return f } // 2. Reported speech: a past/third-person report verb governing a command // clause reports an order to someone else, it does not issue one. if f, ok := stageReport(t, toks, joined); ok { return f } // 3. Hypothetical: a command scope opened by "если/if" that does not // continue as a real condition→command is not an execution request. if f, ok := stageHypothesis(toks, joined); ok { return f } // 4. Direct negative commands: the shipped prohibition parser. if router.IsCommandProhibition(t) { return blocked(ReasonCommandProhibition) } // 5. Advisory negatives: "не надо/не стоит/не нужно …". if f, ok := stageAdvisoryNegation(toks); ok { return f } // 6. Capability and permission modality (the measured core). if f, ok := stageCapability(toks, joined); ok { return f } // 7. Trailing question mark with no modal at play: an uncertain posture, // never a confirmed executable request. if strings.HasSuffix(t, "?") { return ambiguous(ReasonAmbiguousModal) } // 8. Positive request evidence. if hasRequestEvidence(toks, joined) { return permissive(ReasonExplicitRequest) } // 9. No execution pressure at all. return ambiguous(ReasonNoRequestEvidence) } // stageQuotation blocks a quoted command when a reporting frame surrounds it. func stageQuotation(t string, toks []string, joined string) (Frame, bool) { spans := extractQuotedSpans(t) if len(spans) == 0 { return Frame{}, false } commandSpan := false for _, sp := range spans { if isCommandishWithin(tokens(sp.content), strings.Join(tokens(sp.content), " ")) { commandSpan = true break } } if !commandSpan { return Frame{}, false } reasons := []Reason{ReasonQuotation} if hasReportFrame(toks, joined) { reasons = append(reasons, ReasonReportedSpeech) return Frame{Eligibility: Blocked, Reasons: reasons}, true } // a bare quoted command has no reporting frame: refusable but not a // definite prohibition either (it is at least ambiguous) return Frame{Eligibility: Ambiguous, Reasons: reasons}, true } // isCommandishWithin reports the span content carrying command or capability // polarity itself — imperative, prohibition, or a can-form. func isCommandishWithin(toks []string, joined string) bool { if len(toks) == 0 { return false } if router.IsCommandProhibition(strings.Join(toks, " ")) { return true } if hasAny(toks, ruCanForms) || ruAbilityForms.in(toks, joined) || hasAny(toks, enCanForms) { return true } return anyFiniteVerb(toks) } func hasReportFrame(toks []string, joined string) bool { if hasAny(toks, reportVerbs) { return true } return hasAny(toks, reportNouns) } // stageReport blocks when a report frame governs a command clause: an // infinitive after the report verb, or a quoted imperative. Second-person // imperatives like "скажи/расскажи" are not in reportVerbs, so a request to // report ("расскажи мне, что сказал папа") passes through. func stageReport(t string, toks []string, joined string) (Frame, bool) { if !hasReportFrame(toks, joined) { return Frame{}, false } last := -1 for i, w := range toks { if hasTok(reportVerbs, w) || hasTok(reportNouns, w) { last = i } } if last < 0 { return Frame{}, false } after := toks[last+1:] if len(after) == 0 { return Frame{}, false } // a quoted command after the frame counts as the governed clause for _, sp := range extractQuotedSpans(t) { if isCommandishWithin(tokens(sp.content), strings.Join(tokens(sp.content), " ")) { return blocked(ReasonReportedSpeech, ReasonQuotation), true } } if anyInfinitive(after) || hasAny(after, []string{"что", "чтобы", "чтоб"}) { return blocked(ReasonReportedSpeech), true } return Frame{}, false } // stageHypothesis blocks a conditional scope whose clauses are hypothetical // (infinitive or subjunctive "бы") rather than a real condition→command. // "если будет дождь, выключи полив" keeps its imperative continuation and // passes through; it is a real conditional request, not a hypothetical. func stageHypothesis(toks []string, joined string) (Frame, bool) { idx := -1 for _, m := range hypothesisMarkers { if i := indexTok(toks, m); i >= 0 && (idx < 0 || i < idx) { idx = i } } if idx < 0 { return Frame{}, false } post := toks[idx+1:] if len(post) == 0 || hasTok(post, "бы") || anyInfinitive(post) { return blocked(ReasonHypothetical), true } // a real condition clause is not hypothetical: «если будет дождь, // выключи полив» is a request. The dev dict does not cover «будет», so // the imperative is looked for anywhere, not just after the marker // («выключи свет, если будет дождь»). if !anyFiniteVerb(toks) { return blocked(ReasonHypothetical), true } return permissive(ReasonExplicitRequest), true } // stageAdvisoryNegation blocks "не надо/не нужно/не стоит/не следует …". // (absent from the dev pool; covered by brief fixtures) func stageAdvisoryNegation(toks []string) (Frame, bool) { if len(toks) < 3 || toks[0] != "не" { return Frame{}, false } if !hasTok(toks[1:2], "надо") && !hasTok(toks[1:2], "нужно") && !hasTok(toks[1:2], "стоит") && !hasTok(toks[1:2], "следует") && !hasTok(toks[1:2], "требуется") { return Frame{}, false } rest := toks[2:] if anyInfinitive(rest) || anyFiniteVerb(rest) || hasAnyVerb(rest) { return blocked(ReasonNegatedCommand), true } return Frame{}, false } // stageCapability encodes the measured modal matrix. Returns a decision when // modality alone settles the frame. func stageCapability(toks []string, joined string) (Frame, bool) { // "не мог бы ты …, пожалуйста" style conditional politeness is ordinary // modal politeness (the prohibition parser exempts it as such): a request. for _, pref := range politeNegativeModal { if strings.HasPrefix(joined, pref) { return permissive(ReasonExplicitRequest), true } } // "… ли" directly after a can-form is a polar capability question: // "могу ли я …", "можешь ли ты …", "умеешь ли ты …", "можно ли …". // Checked before the modality arms so the polar reading wins. if hasTok(toks, "ли") { for i := 1; i < len(toks); i++ { if toks[i] != "ли" { continue } prev := toks[i-1] if hasTok(ruCanForms, prev) || prev == "можно" || hasTok(ruAbilityForms.single, prev) { return blocked(ReasonCapabilityQuestion), true } } } ruAddr := hasAny(toks, ruAddress) ruCan := hasAny(toks, ruCanForms) ruAbil := ruAbilityForms.in(toks, joined) enCan := hasAny(toks, enCanForms) polite := politeness.in(toks, joined) // addressed Russian can-form: capability question, always blocked. // ("ты можешь выключить свет, пожалуйста" included — 42/42 non-action.) if ruAddr && (ruCan || ruAbil) { return blocked(ReasonCapabilityQuestion), true } // ability forms (future/conditional/умеешь) are capability even bare and // even polite: "сможешь открыть окно, пожалуйста" is 7/7 non-action. if ruAbil && !ruCan { return blocked(ReasonCapabilityQuestion), true } // bare Russian present can-form: politeness is the request marker. if ruCan && !ruAddr { if polite { return Frame{}, false // modal-request positive evidence is found later } return ambiguous(ReasonAmbiguousModal), true } // English can/could: "can you …, please" is a request (96/96 action in the // dev pool; the frame wraps a Russian imperative). Without politeness it // reads as a capability question and stays ambiguous. if enCan && !ruAddr { if polite { return Frame{}, false // positive modal-request evidence later } return ambiguous(ReasonAmbiguousModal), true } // "можно" (permission): "можно ли …" is a permission question; a bare // "можно …" is a politeness-implicature request. if hasTok(toks, "можно") { if hasTok(toks, "ли") { return blocked(ReasonCapabilityQuestion), true } return Frame{}, false } // bare "могу": a self-capability statement, not a request. if hasTok(toks, "могу") && !hasTok(toks, "ли") { return ambiguous(ReasonAmbiguousModal), true } return Frame{}, false } // hasRequestEvidence is the positive permissive trigger, reached only after // every block/ambiguity stage above has declined. func hasRequestEvidence(toks []string, joined string) bool { polite := politeness.in(toks, joined) enCan := hasAny(toks, enCanForms) // 1. politeness + a verb (or an English modal) is explicit request // evidence: "можешь выключить свет, пожалуйста", "can you останови …, // please", "выключи свет, пожалуйста". if polite && (hasAnyVerb(toks) || enCan) { return true } // 2. first-person illocution frame: "я хочу …", "мне нужно …". if hasAny(toks, lexicon.FirstPerson()) && illocutionVerbs.in(toks, joined) { return true } // 3. impersonal need: "надо …", "нужно …", "пора …". if hasAny(toks, []string{"надо", "нужно", "следует", "пора", "требуется", "придётся", "придется"}) { return true } // 4. permission-implicature request: "можно выключить свет". if hasTok(toks, "можно") && !hasTok(toks, "ли") { return true } // 5. reminder request in the parser's own exemption scope: the // prohibition parser declines «не забудь напомнить про свет» as a // reminder, not a prohibition — carry that into a request. if strings.HasPrefix(joined, "не забудь") && hasAny(toks, lexicon.ReminderVerbs()) { return true } // 6. leading finite verb (imperative or otherwise tensed verb at command // position): "выключи свет", "покажи что запущено". Address and filler // particles are stripped first, so "мавен, выключи свет" rides the same // frame. lead := toks for len(lead) > 0 { first := lead[0] if !lexicon.IsFillerParticle(first) && !hasTok(wakeAddr, first) && !hasTok(ruAddress, first) { break } lead = lead[1:] } if len(lead) > 0 && isFiniteVerb(lead[0]) { return true } return false }