confirm answers match whole words from the lexicon (V-567)

classifyConfirm was a substring test over bare stems, so "погода",
"дальше", "надо" and "давление" all read as "да", and "покажи" and
"около" read as "ок". resolveConfirm runs before routing, so a question
about the weather executed a parked destructive act. Reproduced on the box:
with "restart nonexistent-xyz" parked, "какая погода" answered "не
получилось выполнить команду".

The yes and no answers are now two closed sets in internal/lexicon, matched
as whole tokens longest-first, and the WHOLE utterance must be answer words
and filler — a leading "давай" does not make "давай посмотрим погоду" an
answer. Anything else is confirmUnknown, which now leaves the confirm parked
instead of disarming it: an utterance that is not an answer is not a
cancellation either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 01:05:50 +04:00
parent b6305f1b6e
commit 31deb7d565
4 changed files with 228 additions and 21 deletions
+11
View File
@@ -65,6 +65,7 @@ func mustLoad() lexiconFile {
"day_offsets", "weekdays", "months_genitive", "hours_spoken",
"not_place_after_v", "parts_of_day", "reminder_verbs", "half_hour",
"filler_particles", "task_done_words", "task_drop_words",
"confirm_yes", "confirm_no",
} {
s, ok := f.Sets[name]
if !ok || (len(s.Words) == 0 && len(s.Values) == 0) {
@@ -121,6 +122,16 @@ func ReminderVerbs() []string { return words("reminder_verbs") }
// notes say: an imperative exactly, a stative by lemma.
func TaskDoneWords() []string { return words("task_done_words") }
// ConfirmYes returns the words that answer a parked confirm with yes, and
// ConfirmNo the ones that answer it with no. Some members are multi-word ("не
// надо"), so a caller matches longest-first over tokens rather than looking up
// one word at a time. See the sets' notes for why neither may be matched as a
// substring.
func ConfirmYes() []string { return words("confirm_yes") }
// ConfirmNo — see ConfirmYes.
func ConfirmNo() []string { return words("confirm_no") }
// TaskDropWords — see TaskDoneWords.
func TaskDropWords() []string { return words("task_drop_words") }
+16
View File
@@ -197,6 +197,22 @@
"drop", "remove", "cancel",
"передумал", "передумала", "неактуально"
]
},
"confirm_yes": {
"note": "The whole vocabulary of saying yes to a parked confirm, Russian and English. Closed because it is her question that is being answered: she asked \"да или нет\", and the answers to that question can be listed. Matched as whole tokens and never as substrings — \"погода\", \"давление\" and \"дальше\" all contain \"да\", and a substring test executed a destructive act when he asked about the weather (V-567). Words that merely sound agreeable — \"хорошо\", \"ладно\", \"точно\" — are deliberately absent: they open a sentence about something else as often as they answer, and an unclear answer must route rather than execute.",
"words": [
"да", "ага", "угу", "давай", "давайте", "конечно",
"подтверждаю", "подтверди", "подтвердить", "выполняй", "валяй",
"yes", "yeah", "yep", "yup", "ok", "okay", "sure", "confirm", "affirmative"
]
},
"confirm_no": {
"note": "The answers that decline a parked confirm. Same matching rule as confirm_yes and the same reason. The multi-word members are here rather than assembled by a caller because \"надо\" alone is not an answer and \"не надо\" is the opposite of one: the two must land on opposite sides, and only the phrase says which. \"не\" on its own is NOT a member — \"не забудь купить хлеб\" is a reminder, not a refusal.",
"words": [
"нет", "неа", "нельзя", "отмена", "отмени", "отменить", "отставить",
"стоп", "стой", "не надо", "не нужно", "не стоит", "не сейчас", "не хочу",
"no", "nope", "nah", "negative", "cancel", "stop", "don't", "dont"
]
}
}
}