Files
Maven/internal/phraser/acts.go
T
claude 0ade0ec734 tool, mavend: Hexis owns the tier of a Hexis capability (V-523)
read_only was the whole decision on the Hexis act path, which flattened three
answers into two. A capability that wipes the thing it names got the same
single spoken "да" as one that restarts a service, and requires_confirmation —
which the Hexis contract calls server-derived and never settable by a caller —
was read by nobody. docs/ecosystem.md §17.3 says confirmation follows risk.

RiskOfCapability reads Hexis's risk, read_only and requires_confirmation and
returns one of the three tiers internal/tool already had. It takes plain values
rather than a Capability, so internal/tool keeps no dependency on the Hexis
client. RiskOf keeps deriving, because a shell row the owner ticked on /tools
has no upstream to ask.

Every disagreement between the three fields goes up, never down: safe and
mutating is a contradiction and takes the confirm, an unrecognised tier takes
the confirm, and requires_confirmation may only raise. Same default as an
unrecognised dispatch shape — argue your way down, never up.

The irreversible refusal was a Go literal in two places and is now one deck
entry, act_needs_authed_surface. It lost four words to the persona ceiling.
2026-08-04 18:01:28 +04:00

198 lines
8.1 KiB
Go

package phraser
// The act and smart-home replies — what she says when a capability ran, refused,
// or could not be reached.
//
// Fourth family on the shared deck (deck.go). They were literals in
// ecosystem_acts.go, actions_act.go and smarthome.go, where a reworded line was
// a rebuild of the daemon that executes his house.
//
// The four outcomes stay four entries. Reporting a refusal with the wording of
// a success is the one failure mode this family can have, and a shared variant
// set is how it would happen.
import (
_ "embed"
"log"
"math/rand"
"sync"
"github.com/kami/maven/internal/say"
)
//go:embed acts_ru_v1.json
var actJSON []byte
// ActSchemaVersion — this family's own version.
const ActSchemaVersion = 1
// The entry keys.
const (
ActDone = "act_done"
ActDoneOut = "act_done_out"
ActDoneEntity = "act_done_entity"
ActConfirm = "act_confirm"
ActConfirmEntity = "act_confirm_entity"
ActWhich = "act_which"
ActFail = "act_fail"
ActFailOut = "act_fail_out"
ActFailEntity = "act_fail_entity"
ActServerDown = "act_server_down"
ActWithdrawn = "act_withdrawn"
ActNeedsArgs = "act_needs_args"
// ActNeedsAuthedSurface — the irreversible tier, local row or Hexis
// capability alike. Not a failure and not a refusal to help: a spoken "да"
// is the only authority the voice path can offer, and this is the one act
// it is not enough for (Vikunja #449, #523).
ActNeedsAuthedSurface = "act_needs_authed_surface"
EcoDenied = "eco_denied"
EcoDown = "eco_down"
EcoAmbiguous = "eco_ambiguous"
EcoUnknownEntity = "eco_unknown_entity"
EcoNoNexus = "eco_no_nexus"
EcoAboutWhat = "eco_about_what"
EcoRecall = "eco_recall"
AttentionNone = "attention_none"
AttentionList = "attention_list"
AttentionFail = "attention_fail"
AttentionNoneEntity = "attention_none_entity"
AttentionListEntity = "attention_list_entity"
AttentionFailEntity = "attention_fail_entity"
ChangesNone = "changes_none"
ChangesList = "changes_list"
ChangesFail = "changes_fail"
HomeUnreachable = "home_unreachable"
HomeEmpty = "home_empty"
HomeOn = "home_on"
HomeDark = "home_dark"
)
var actKeys = []string{
ActDone, ActDoneOut, ActDoneEntity, ActConfirm, ActConfirmEntity, ActWhich,
ActFail, ActFailOut, ActFailEntity, ActServerDown, ActWithdrawn, ActNeedsArgs,
ActNeedsAuthedSurface,
EcoDenied, EcoDown, EcoAmbiguous, EcoUnknownEntity, EcoNoNexus, EcoAboutWhat, EcoRecall,
AttentionNone, AttentionList, AttentionFail,
AttentionNoneEntity, AttentionListEntity, AttentionFailEntity,
ChangesNone, ChangesList, ChangesFail,
HomeUnreachable, HomeEmpty, HomeOn, HomeDark,
}
// actFloor — the literal each key falls back to when the file is unusable. It
// started as the exact strings that lived in Go before this file existed and now
// tracks the file's first variant instead, because a floor that keeps the
// wording review threw out would say it back on the one turn nobody is watching.
var actFloor = map[string]string{
ActDone: "готово.",
ActDoneOut: "готово: {out}",
ActDoneEntity: "готово: {name}.",
ActConfirm: "выполнить «{name}»? да или нет.",
ActConfirmEntity: "выполнить «{name}» для {name_entity}? да или нет.",
ActWhich: "какую команду для {name}: {items}?",
ActFail: "не получилось выполнить команду.",
ActFailOut: "не получилось выполнить команду: {out}",
ActFailEntity: "не получилось выполнить команду для {name}.",
ActServerDown: "инструмент есть, но сервер не подключён.",
ActWithdrawn: "сервер больше не отдаёт этот инструмент — сняла его с разрешённых, посмотри /tools.",
ActNeedsArgs: "тут нужны аргументы, из голоса не соберу. угадывать не буду.",
ActNeedsAuthedSurface: "это из голоса не выполню — после него ничего не вернуть. запусти сам.",
EcoDenied: "{name} отклоняет доступ, проверь токен.",
EcoDown: "{name} не отвечает, попробуй ещё раз.",
EcoAmbiguous: "что именно: {items}?",
EcoUnknownEntity: "не знаю, что это.",
EcoNoNexus: "не с чем связать — Nexus не настроен.",
EcoAboutWhat: "про что именно?",
EcoRecall: "я помню: {items}",
AttentionNone: "ничего не требует внимания.",
AttentionList: "требует внимания: {items}",
AttentionFail: "не могу сейчас узнать, что требует внимания.",
AttentionNoneEntity: "по «{name}» ничего нет.",
AttentionListEntity: "по «{name}»: {items}",
AttentionFailEntity: "не могу сейчас узнать, что требует внимания по «{name}».",
ChangesNone: "изменений нет.",
ChangesList: "изменения: {items}",
ChangesFail: "не могу сейчас узнать об изменениях.",
HomeUnreachable: "дом не отвечает.",
HomeEmpty: "дом ничего не отдаёт.",
HomeOn: "включено: {items}",
HomeDark: "не отвечают: {count} {word}.",
}
// Acts picks a hand-written Russian act reply. Safe for concurrent use.
type Acts struct{ d *say.Deck }
// LoadActs reads the embedded file. Pass a source to make the picking
// reproducible in tests; nil seeds from the clock.
func LoadActs(src rand.Source) (*Acts, error) {
d, err := say.Load(actJSON, ActSchemaVersion, actKeys, actFloor, src)
if err != nil {
return nil, err
}
// The entries that name what ran or what he has to choose between. A
// variant that dropped the name would confirm an act without saying which.
for _, req := range []struct{ key, ph string }{
{ActDoneOut, "{out}"}, {ActDoneEntity, "{name}"}, {ActFailOut, "{out}"},
{ActFailEntity, "{name}"}, {ActConfirm, "{name}"},
{ActConfirmEntity, "{name}"}, {ActConfirmEntity, "{name_entity}"},
{ActWhich, "{name}"}, {ActWhich, "{items}"},
{EcoAmbiguous, "{items}"}, {EcoRecall, "{items}"},
{AttentionList, "{items}"}, {ChangesList, "{items}"}, {HomeOn, "{items}"},
{EcoDenied, "{name}"}, {EcoDown, "{name}"},
{HomeDark, "{count}"}, {HomeDark, "{word}"},
{AttentionNoneEntity, "{name}"}, {AttentionListEntity, "{name}"},
{AttentionListEntity, "{items}"}, {AttentionFailEntity, "{name}"},
} {
if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
return nil, err
}
}
return &Acts{d: d}, nil
}
// deck reads through a nil *Acts, which is the unloadable-file case.
func (a *Acts) deck() *say.Deck {
if a == nil {
return say.FloorDeck(actFloor)
}
return a.d
}
// Say returns one line for key, with the names filled into the frame.
func (a *Acts) Say(key string, vars map[string]string) string {
return a.deck().Text(key, vars)
}
// Variants returns every line the file can produce, for the persona scorer.
func (a *Acts) Variants() []string { return a.deck().Variants() }
var (
actOnce sync.Once
actsDeck *Acts
)
// DefaultActs returns the shared instance, loading it on first use. A broken
// file logs once and leaves a nil *Acts, which still answers from actFloor.
func DefaultActs() *Acts {
actOnce.Do(func() {
a, err := LoadActs(nil)
if err != nil {
log.Printf("phraser: act replies unavailable, using the built-in lines: %v", err)
return
}
actsDeck = a
})
return actsDeck
}
// A — one act reply, the way every caller says it.
func A(key string, vars map[string]string) string { return DefaultActs().Say(key, vars) }
// IsA reports whether text is a line key could have produced, for the tests.
func IsA(key string, vars map[string]string, text string) bool {
return DefaultActs().deck().Matches(key, vars, text)
}