the unplaceable word rides a typed error, not the message (V-634)

Recovering it by cutting on quotes in err.Error() meant the reply depended on
the wording of an error string. UnknownTargetError carries the word and
errors.Is still holds.
This commit is contained in:
2026-08-06 20:06:25 +04:00
parent d94ed2e630
commit 0e82cb442f
2 changed files with 22 additions and 18 deletions
+16 -1
View File
@@ -84,6 +84,17 @@ var (
ErrUnknownTarget = errors.New("the act names a target the system cannot have")
)
// UnknownTargetError carries the word the executor could not place, because the
// reply names it: "«роутер» — не знаю такой цели" is actionable and "не
// получилось" sends him to the log. errors.Is(err, ErrUnknownTarget) holds.
type UnknownTargetError struct{ Target string }
func (e *UnknownTargetError) Error() string {
return fmt.Sprintf("%s: %q", ErrUnknownTarget, e.Target)
}
func (e *UnknownTargetError) Unwrap() error { return ErrUnknownTarget }
// MCPCaller is the seam for an act that is an MCP tool call rather than a
// process (Vikunja #251). internal/mcp.Manager satisfies it via CallPositional.
// nil ⇒ MCP is not configured, and an MCP row refuses to run rather than
@@ -160,7 +171,7 @@ func (e *Executor) Exec(ctx context.Context, name string, args []string, confirm
// since a task title is not a target, and a house row drops the spoken args.
if !isMCPRow(t.Cmd) && !isHouseRow(t.Cmd) {
if bad, ok := firstUnknownTarget(args); !ok {
return "", fmt.Errorf("%w: %q", ErrUnknownTarget, bad)
return "", &UnknownTargetError{Target: bad}
}
}
// The tier decides, not the column (Vikunja #449). RiskOf reads the row and
@@ -290,6 +301,10 @@ func (m *Matcher) Match(utterance string) (string, []string, bool) {
// sentence. "перезагрузи роутер" is the case: restart is a real tool and
// "роутер" is a real word, and `systemctl restart роутер` is neither.
//
// Every process row this box enables takes a system identifier (systemctl,
// docker, journalctl, df). A process row that legitimately wanted Russian text
// would want a different dispatch, not a hole in this check.
//
// It deliberately does not try to guess the right target. Identity is Nexus's
// (CLAUDE.md § "The ecosystem"), and a target Nexus resolves reaches Hexis
// through handleHexisAct before this executor is asked.