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 } }