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>
120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
package router
|
||
|
||
import (
|
||
"strings"
|
||
"unicode"
|
||
|
||
"github.com/kami/maven/internal/lexicon"
|
||
"github.com/kami/maven/internal/morph"
|
||
)
|
||
|
||
// ParseNoteCapture extracts the user's note body from a leading capture
|
||
// command. It is the durable-write boundary: the router's model may label a
|
||
// turn as a note, but it may not rewrite what the notes table holds.
|
||
//
|
||
// The parser is structural, not a phrase pattern. It tracks Unicode word spans
|
||
// in the original utterance, accepts an optional wake word and leading
|
||
// particles, then requires one exact imperative from the capture lexicon. It
|
||
// removes only that command frame and returns the untouched remainder. A
|
||
// capture verb later in an ordinary sentence does not authorize a rewrite.
|
||
//
|
||
// Russian "что" is removed only when morphology proves that what follows is a
|
||
// clause with an inflected verb. When the evidence is ambiguous, as in "что
|
||
// такое TCP" or "что купить к ужину", it remains part of the note.
|
||
func ParseNoteCapture(utterance string) (string, bool) {
|
||
text := strings.TrimSpace(utterance)
|
||
if stripped, ok := StripWakeToken(text); ok {
|
||
text = stripped
|
||
}
|
||
words := noteCaptureWords(text)
|
||
if len(words) == 0 {
|
||
return "", false
|
||
}
|
||
|
||
i := 0
|
||
for i < len(words) && lexicon.IsFillerParticle(words[i].text) {
|
||
i++
|
||
}
|
||
if i >= len(words) || !isCaptureImperative(words[i].text) {
|
||
return "", false
|
||
}
|
||
|
||
end := words[i].end
|
||
i++
|
||
for i < len(words) && lexicon.IsCaptureFrameParticle(words[i].text) {
|
||
end = words[i].end
|
||
i++
|
||
}
|
||
if i < len(words) && words[i].text == "что" && hasInflectedClauseVerb(words[i+1:]) {
|
||
end = words[i].end
|
||
}
|
||
|
||
body := strings.TrimLeftFunc(text[end:], isNoteCaptureDelimiter)
|
||
if strings.TrimSpace(body) == "" {
|
||
return "", false
|
||
}
|
||
return body, true
|
||
}
|
||
|
||
type noteCaptureWord struct {
|
||
text string
|
||
start, end int
|
||
}
|
||
|
||
// noteCaptureWords tokenizes only far enough to locate safe cut points. Byte
|
||
// offsets keep the returned body in the user's original case and punctuation.
|
||
func noteCaptureWords(text string) []noteCaptureWord {
|
||
var out []noteCaptureWord
|
||
start := -1
|
||
for at, r := range text {
|
||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||
if start < 0 {
|
||
start = at
|
||
}
|
||
continue
|
||
}
|
||
if start >= 0 {
|
||
out = append(out, noteCaptureWord{
|
||
text: strings.ToLower(text[start:at]), start: start, end: at,
|
||
})
|
||
start = -1
|
||
}
|
||
}
|
||
if start >= 0 {
|
||
out = append(out, noteCaptureWord{
|
||
text: strings.ToLower(text[start:]), start: start, end: len(text),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func isCaptureImperative(word string) bool {
|
||
for _, candidate := range captureVerbs {
|
||
if word == candidate {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func hasInflectedClauseVerb(words []noteCaptureWord) bool {
|
||
for _, word := range words {
|
||
if morph.IsVerbForm(word.text) && morph.Lemma(word.text) != word.text {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func isNoteCaptureDelimiter(r rune) bool {
|
||
if unicode.IsSpace(r) {
|
||
return true
|
||
}
|
||
switch r {
|
||
case ',', ':', ';', '.', '!', '?', '-', '–', '—':
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|