package phraser // The confirmation answers — what she says once he has answered a confirm, and // what she says when an act names a verb she may not run. // // Fifth family on the shared deck (deck.go). They were literals in // cmd/mavend/confirm.go. // // The prompt is deliberately not here. act_confirm and act_confirm_entity are in // acts_ru_v1.json, where they belong: the sentence he has to hear before he says // yes is an act line, and it already loads with its {name} placeholder required. // Vikunja #505 asked whether this family should move at all. It moves, but only // the answers, and every entry is fixed. He answered a question about one // specific thing, so variety here buys nothing and costs the one property that // matters: the same act reports the same outcome every time. // // The tool-confirm success and failure lines are not here either. They said // "готово." and "не получилось выполнить команду." in two places, which is the // acts family word for word, so the confirmed run now reports through ActDone // and ActFail rather than keeping a second copy that can drift. import ( _ "embed" "log" "math/rand" "sync" "github.com/kami/maven/internal/say" ) //go:embed confirm_ru_v1.json var confirmJSON []byte // ConfirmSchemaVersion — this family's own version. const ConfirmSchemaVersion = 1 // The entry keys. const ( // ConfirmCancelled — he said no. It names no capability, because the // prompt he answered named one and nothing ran. ConfirmCancelled = "confirm_cancelled" // ConfirmRoutineAuthed — he said yes to a proposed routine out loud, which // is not an acceptance. A room mic cannot hand the tick loop a standing new // reason to speak, so the row stays proposed and this line points at the // page where the accept button is gated (Vikunja #367). ConfirmRoutineAuthed = "confirm_routine_authed" ConfirmRoutineNo = "confirm_routine_no" // The propose-gap lines: an act whose verb is not on the allowlist. She // drafts the registration and says so. She never enables it. ProposeNoVerb = "propose_no_verb" ProposeFailed = "propose_failed" ProposeNew = "propose_new" ProposeAlready = "propose_already" ) var confirmKeys = []string{ ConfirmCancelled, ConfirmRoutineAuthed, ConfirmRoutineNo, ProposeNoVerb, ProposeFailed, ProposeNew, ProposeAlready, } // confirmFloor — the literal each key falls back to when the file is unusable. // These are the exact strings that lived in cmd/mavend/confirm.go. var confirmFloor = map[string]string{ ConfirmCancelled: "отменила.", ConfirmRoutineAuthed: "поняла — подтверди на странице рутин, и начну напоминать.", ConfirmRoutineNo: "хорошо, не буду.", ProposeNoVerb: "не разобрала команду — попробуй иначе.", ProposeFailed: "команды «{name}» нет в списке разрешённых.", ProposeNew: "команды «{name}» нет в списке. Предложила её добавить — включи через клиент.", ProposeAlready: "команды «{name}» пока нет в списке — она уже предложена, включи через клиент.", } // Confirms picks a hand-written Russian confirmation answer. Safe for // concurrent use. type Confirms struct{ d *say.Deck } // LoadConfirms reads the embedded file. Pass a source to make the picking // reproducible in tests; nil seeds from the clock. func LoadConfirms(src rand.Source) (*Confirms, error) { d, err := say.Load(confirmJSON, ConfirmSchemaVersion, confirmKeys, confirmFloor, src) if err != nil { return nil, err } // Every propose line that names the verb must keep naming it. A variant that // dropped {name} would tell him a command is not allowed without saying // which one, and the whole point of the line is that he goes and enables it. for _, key := range []string{ProposeFailed, ProposeNew, ProposeAlready} { if err := d.RequirePlaceholder(key, "{name}"); err != nil { return nil, err } } return &Confirms{d: d}, nil } // deck reads through a nil *Confirms, which is the unloadable-file case. func (c *Confirms) deck() *say.Deck { if c == nil { return say.FloorDeck(confirmFloor) } return c.d } // Say returns the line for key, with the verb filled into the frame. func (c *Confirms) Say(key string, vars map[string]string) string { return c.deck().Text(key, vars) } // Variants returns every line the file can produce, for the persona scorer. func (c *Confirms) Variants() []string { return c.deck().Variants() } var ( confirmOnce sync.Once confirmsDeck *Confirms ) // DefaultConfirms returns the shared instance, loading it on first use. A broken // file logs once and leaves a nil *Confirms, which still answers from // confirmFloor: a daemon must not fail to boot over its own copy deck. func DefaultConfirms() *Confirms { confirmOnce.Do(func() { c, err := LoadConfirms(nil) if err != nil { log.Printf("phraser: confirmation answers unavailable, using the built-in lines: %v", err) return } confirmsDeck = c }) return confirmsDeck } // C — one confirmation answer, the way every caller says it. func C(key string, vars map[string]string) string { return DefaultConfirms().Say(key, vars) } // IsC reports whether text is a line key could have produced, for the tests. func IsC(key string, vars map[string]string, text string) bool { return DefaultConfirms().deck().Matches(key, vars, text) }