a surface can ask Nexus for the id behind a name (V-511)
blocked_on stores a canonical entity id, so the form that fills it needs a way to turn "Kate" into one. ipc.ResolveEntity is that seam: the store adapter refuses it, because identity is not the store's to answer, and the daemon overrides it with the Nexus client the voice path already holds. Three outcomes are kept apart, because a caller deciding whether to store an id has to tell them apart. No nexus block is ErrNotImplemented. A miss is ErrNoEntity. Several matches come back Ambiguous with the names, and the caller asks — picking one is how a task ends up blocked on the wrong person with nobody able to see it happened. An outage stays the transport error. "There is no such person" and "Nexus is down" must not read the same.
This commit is contained in:
@@ -549,6 +549,31 @@ type setTaskStatusReq struct {
|
||||
By string `json:"by,omitempty"`
|
||||
}
|
||||
|
||||
// EntityRef — one canonical entity from Nexus. Maven never mints these: an id
|
||||
// exists because Nexus resolved a name to it.
|
||||
//
|
||||
// Ambiguous is the answer when the name matched more than one entity. It is a
|
||||
// separate state from "not found" because the surface handles them
|
||||
// differently: an unknown name may be a typo, and an ambiguous one has to be
|
||||
// asked about, never picked (ECOSYSTEM-SPEC, and the same rule the mutating
|
||||
// Hexis path follows).
|
||||
type EntityRef struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Ambiguous bool `json:"ambiguous,omitempty"`
|
||||
Candidates []string `json:"candidates,omitempty"`
|
||||
}
|
||||
|
||||
type resolveEntityReq struct {
|
||||
Query string `json:"query"`
|
||||
Types []string `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
type resolveEntityResp struct {
|
||||
Ref EntityRef `json:"ref"`
|
||||
}
|
||||
|
||||
// editTaskReq — the rewrite of the three fields capture set (Vikunja #509).
|
||||
// Due nil clears the date, so "no date given" and "remove the date" cannot be
|
||||
// the same request.
|
||||
@@ -886,6 +911,11 @@ var ErrTaskNoDoneWhen = errors.New("ipc: task has no definition of done")
|
||||
// text (Vikunja #509). The surface says which row holds it rather than merging.
|
||||
var ErrTaskDuplicate = errors.New("ipc: another live task already has this text")
|
||||
|
||||
// ErrNoEntity — Nexus resolved the name to nothing. Distinct from an outage,
|
||||
// which surfaces as the transport error: "there is no such person" and "Nexus
|
||||
// is down" must not read the same to a caller deciding whether to store an id.
|
||||
var ErrNoEntity = errors.New("ipc: no such entity")
|
||||
|
||||
// ErrTaskResolved — a resolved task is not editable.
|
||||
var ErrTaskResolved = errors.New("ipc: task is resolved")
|
||||
|
||||
|
||||
@@ -515,6 +515,14 @@ func (c *Client) SetTaskStatus(ctx context.Context, id int64, status string, ts
|
||||
return c.call(ctx, MethodSetTaskStatus, setTaskStatusReq{ID: id, Status: status, Ts: ts, By: by}, nil)
|
||||
}
|
||||
|
||||
func (c *Client) ResolveEntity(ctx context.Context, query string, types []string) (EntityRef, error) {
|
||||
var r resolveEntityResp
|
||||
if err := c.call(ctx, MethodResolveEntity, resolveEntityReq{Query: query, Types: types}, &r); err != nil {
|
||||
return EntityRef{}, err
|
||||
}
|
||||
return r.Ref, nil
|
||||
}
|
||||
|
||||
func (c *Client) EditTask(ctx context.Context, id int64, text string, due *time.Time, weight int) error {
|
||||
return c.call(ctx, MethodEditTask, editTaskReq{ID: id, Text: text, Due: due, Weight: weight}, nil)
|
||||
}
|
||||
|
||||
@@ -142,6 +142,13 @@ type TaskAPI interface {
|
||||
// SetTaskStatus moves a task forward once: candidate→open|dropped,
|
||||
// open→done|dropped. Any other move is refused.
|
||||
SetTaskStatus(ctx context.Context, id int64, status string, ts time.Time, by string) error
|
||||
// ResolveEntity asks Nexus for the canonical id behind a name. It is how a
|
||||
// surface turns "Kate" into an entity id before storing one, because
|
||||
// identity lives in Nexus and a local name is a second answer to a
|
||||
// question Nexus already owns. ErrNotImplemented when no Nexus is
|
||||
// configured, ErrNoEntity when the name matched nothing, and an Ambiguous
|
||||
// ref when it matched several — the caller asks, it does not pick.
|
||||
ResolveEntity(ctx context.Context, query string, types []string) (EntityRef, error)
|
||||
// EditTask rewrites the three fields capture set: text, due date and
|
||||
// weight. Status is not among them — that ladder is one-way and belongs to
|
||||
// SetTaskStatus. A resolved task is refused, and a text edit that would
|
||||
|
||||
@@ -540,6 +540,13 @@ var methodTable = map[Method]handlerFunc{
|
||||
MethodSetTaskStatus: withParamsVoid(func(ctx context.Context, api CoreAPI, p setTaskStatusReq) error {
|
||||
return api.SetTaskStatus(ctx, p.ID, p.Status, p.Ts, p.By)
|
||||
}),
|
||||
MethodResolveEntity: withParams(func(ctx context.Context, api CoreAPI, p resolveEntityReq) (resolveEntityResp, error) {
|
||||
ref, err := api.ResolveEntity(ctx, p.Query, p.Types)
|
||||
if err != nil {
|
||||
return resolveEntityResp{}, err
|
||||
}
|
||||
return resolveEntityResp{Ref: ref}, nil
|
||||
}),
|
||||
MethodEditTask: withParamsVoid(func(ctx context.Context, api CoreAPI, p editTaskReq) error {
|
||||
return api.EditTask(ctx, p.ID, p.Text, p.Due, p.Weight)
|
||||
}),
|
||||
|
||||
@@ -356,6 +356,13 @@ func (a *storeAPI) SetTaskStatus(ctx context.Context, id int64, status string, t
|
||||
return mapErr(a.s.SetTaskStatus(ctx, id, status, ts, by))
|
||||
}
|
||||
|
||||
// ResolveEntity is not the store's to answer: identity lives in Nexus and this
|
||||
// adapter has no client. The daemon overrides it (cmd/mavend/tick_api.go), and
|
||||
// a deployment with no nexus block keeps this refusal.
|
||||
func (a *storeAPI) ResolveEntity(ctx context.Context, query string, types []string) (EntityRef, error) {
|
||||
return EntityRef{}, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (a *storeAPI) EditTask(ctx context.Context, id int64, text string, due *time.Time, weight int) error {
|
||||
return mapErr(a.s.EditTask(ctx, id, text, due, weight))
|
||||
}
|
||||
|
||||
@@ -114,6 +114,9 @@ func (UnimplementedCoreAPI) ListTasks(ctx context.Context, status string) ([]Tas
|
||||
func (UnimplementedCoreAPI) SetTaskStatus(ctx context.Context, id int64, status string, ts time.Time, by string) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
func (UnimplementedCoreAPI) ResolveEntity(ctx context.Context, query string, types []string) (EntityRef, error) {
|
||||
return EntityRef{}, ErrNotImplemented
|
||||
}
|
||||
func (UnimplementedCoreAPI) EditTask(ctx context.Context, id int64, text string, due *time.Time, weight int) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ const (
|
||||
MethodSetTaskStatus Method = "set_task_status"
|
||||
MethodSetTaskFields Method = "set_task_fields"
|
||||
MethodEditTask Method = "edit_task"
|
||||
MethodResolveEntity Method = "resolve_entity"
|
||||
MethodIngestMail Method = "ingest_mail"
|
||||
MethodSwapModel Method = "swap_model"
|
||||
MethodModelStatus Method = "model_status"
|
||||
|
||||
Reference in New Issue
Block a user