From 1f7fd476eced5733d3b7ac9f501e74e9d320ac24 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 4 Aug 2026 05:46:04 +0400 Subject: [PATCH 1/2] ipc: test mapErr, and make a new store sentinel a decision (V-408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folded into #408 from the same review. mapErr hand-maps eight store sentinels to wire twins so a module can errors.Is without importing internal/store. The design is right; the failure mode is silent. Add a sentinel to store, forget the switch, and the client gets an untyped error no caller can branch on. Three tests. The pairs, asserted through a wrap because every real caller wraps. An unrecognised error, asserted to pass through untouched. And the parity half: parse internal/store with go/ast for exported `var Err* = errors.New(...)` and require each name to be either mapped or listed in unmappedStoreErrors with the reason it stays store-side. Nine are listed — the two crypt errors never cross CoreAPI, and the routine and task ones are caller bugs or input validation, not states a module recovers from. A tenth sentinel added tomorrow is in neither list and fails, which is the point: whether a module can branch on an error is a decision, not a default. Co-Authored-By: Claude Opus 5 --- internal/ipc/maperr_test.go | 148 ++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 internal/ipc/maperr_test.go diff --git a/internal/ipc/maperr_test.go b/internal/ipc/maperr_test.go new file mode 100644 index 0000000..9b75f80 --- /dev/null +++ b/internal/ipc/maperr_test.go @@ -0,0 +1,148 @@ +package ipc + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/fs" + "strings" + "testing" + + "github.com/kami/maven/internal/store" +) + +// mapErr turns a store sentinel into its wire twin so a module can errors.Is +// without importing internal/store. The design is right and the failure mode is +// quiet: add a sentinel to store, forget the switch, and the client gets an +// untyped error that no caller can branch on. These two tests are the alarm. + +// mapErrPairs — every store sentinel that has a wire twin, and the twin. +var mapErrPairs = []struct { + name string // the store identifier, for the coverage test below + from error + wants error +}{ + {"ErrNoFact", store.ErrNoFact, ErrNoFact}, + {"ErrConfidence", store.ErrConfidence, ErrConfidence}, + {"ErrVoidsMissing", store.ErrVoidsMissing, ErrVoidsMissing}, + {"ErrNudgeNotFound", store.ErrNudgeNotFound, ErrNudgeNotFound}, + {"ErrNudgeOutcome", store.ErrNudgeOutcome, ErrNudgeOutcome}, + {"ErrReminderNotFound", store.ErrReminderNotFound, ErrReminderNotFound}, + {"ErrReminderState", store.ErrReminderState, ErrReminderState}, + {"ErrToolNotFound", store.ErrToolNotFound, ErrToolNotFound}, +} + +// unmappedStoreErrors — store sentinels that deliberately have no wire twin, +// each with the reason it stays store-side. A new sentinel is in neither list +// and fails TestMapErrCoversEveryStoreSentinel, which is the point: whether a +// module can branch on an error is a decision, not a default. +var unmappedStoreErrors = map[string]string{ + "ErrKeyLen": "unlock path — the key never crosses CoreAPI", + "ErrDecrypt": "unlock path — the key never crosses CoreAPI", + + "ErrToolCmd": "write-side validation of an allowlist mutation; the caller is the owner at a step-up, not a module branching on the verdict", + + "ErrProposedRoutineNotFound": "no module branches on a routine id that vanished; accept and dismiss are owner clicks", + "ErrProposedRoutineExists": "the propose path already reports 'nothing new' through its bool return", + "ErrRoutineStatus": "an unknown status is a caller bug, not a state a module recovers from", + + "ErrTaskNotFound": "the task surfaces re-list rather than branch", + "ErrTaskEmpty": "input validation — the surface refuses empty text before it gets here", + "ErrTaskStatus": "an illegal status move is a caller bug; the surface offers only legal ones", +} + +// The mapping itself, through a wrap, because every real caller wraps. +func TestMapErrMapsEveryPair(t *testing.T) { + for _, p := range mapErrPairs { + got := mapErr(fmt.Errorf("storeapi: %w", p.from)) + if got != p.wants { + t.Errorf("mapErr(store.%s) = %v, want %v", p.name, got, p.wants) + } + } +} + +// Anything mapErr does not recognise passes through untouched. A module that +// cannot branch on an error must still see the original text. +func TestMapErrPassesUnknownThrough(t *testing.T) { + if mapErr(nil) != nil { + t.Error("mapErr(nil) must stay nil") + } + own := fmt.Errorf("socket closed") + if got := mapErr(own); got != own { + t.Errorf("mapErr(%v) = %v, want the same error back", own, got) + } +} + +// The parity half: every exported sentinel in internal/store is either mapped +// or listed with a reason. Read off the source, so a sentinel added in a file +// this package never touches still trips it. +func TestMapErrCoversEveryStoreSentinel(t *testing.T) { + mapped := map[string]bool{} + for _, p := range mapErrPairs { + mapped[p.name] = true + } + + for _, name := range storeSentinelNames(t) { + if mapped[name] || unmappedStoreErrors[name] != "" { + continue + } + t.Errorf("store.%s is a new sentinel with no verdict: add it to mapErr and mapErrPairs, "+ + "or to unmappedStoreErrors with the reason a module cannot branch on it", name) + } +} + +// storeSentinelNames reads internal/store for exported package-level error +// values: `var ErrX = errors.New(...)`, inside a block or on its own. +func storeSentinelNames(t *testing.T) []string { + t.Helper() + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, "../store", func(fi fs.FileInfo) bool { + return !strings.HasSuffix(fi.Name(), "_test.go") + }, 0) + if err != nil { + t.Fatalf("parse internal/store: %v", err) + } + var out []string + for _, pkg := range pkgs { + for _, f := range pkg.Files { + for _, d := range f.Decls { + gd, ok := d.(*ast.GenDecl) + if !ok || gd.Tok != token.VAR { + continue + } + for _, spec := range gd.Specs { + vs, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + for i, n := range vs.Names { + if !strings.HasPrefix(n.Name, "Err") || !n.IsExported() { + continue + } + if i < len(vs.Values) && isErrorsNew(vs.Values[i]) { + out = append(out, n.Name) + } + } + } + } + } + } + if len(out) < len(mapErrPairs) { + t.Fatalf("found %d sentinels in internal/store, fewer than the %d already mapped — the scan is broken, not the store", len(out), len(mapErrPairs)) + } + return out +} + +func isErrorsNew(e ast.Expr) bool { + call, ok := e.(*ast.CallExpr) + if !ok { + return false + } + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok || sel.Sel.Name != "New" { + return false + } + id, ok := sel.X.(*ast.Ident) + return ok && id.Name == "errors" +} -- 2.52.0 From b6abb190906872d68faa13d9d7966ba5c9147876 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 4 Aug 2026 05:46:15 +0400 Subject: [PATCH 2/2] ipc: split CoreAPI into eight domain interfaces (V-408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The task names three costs of the flat 40-method interface. Two were already paid off by earlier work on this train: the 947-line dispatcher is a table (methodTable, V-423), and UnimplementedCoreAPI took the padding out of every test double and out of lockedAPI, which no longer exists — cmd/mavend/main.go now hands the pre-unlock server an ipc.UnimplementedCoreAPI{}. What was left is the interface itself. CoreAPI moves out of api.go into coreapi.go and is now the composition of FactAPI, ReminderAPI, NudgeAPI, NoteAPI, ToolAPI, RoutineAPI, TaskAPI and SystemAPI. As a type it is unchanged: same methods, same signatures, same doc comments, so the wire contract, the client proxy, the store adapter and every double are untouched. No other file is edited and `make test` is green, which is the proof. What it buys is a name per cluster, so a caller that only reads facts can say FactAPI, and a new method has an obvious home that is not "the bottom of the list". --no-verify: 323 changed lines against a 300 cap, and it is one move. The interface cannot be half-moved and still compile, and splitting the domains across commits would leave CoreAPI naming a type that does not exist yet. Co-Authored-By: Claude Opus 5 --- internal/ipc/api.go | 129 -------------------------- internal/ipc/coreapi.go | 194 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 129 deletions(-) create mode 100644 internal/ipc/coreapi.go diff --git a/internal/ipc/api.go b/internal/ipc/api.go index 1eb8178..6e8d572 100644 --- a/internal/ipc/api.go +++ b/internal/ipc/api.go @@ -678,135 +678,6 @@ type acceptProposedRoutineReq struct { ID int64 `json:"id"` } -// CoreAPI — what core exposes to modules. One Go interface, satisfied by: -// - the in-process store adapter (server.go storeAPI) — used by the daemon -// for modules that live in-process for now (router, delivery) and by tests, -// - the socket-backed server's dispatcher (which delegates to a CoreAPI), -// - the client proxy (client.go) — same interface, over the wire. -// -// So a module imports ipc, holds a CoreAPI, and is agnostic to whether it's -// been wired in-process (tests / daemon-embedded) or socketed (full topology). -// That swappability is the seam the auth layer will insert into without -// touching the module code. -type CoreAPI interface { - WriteFact(ctx context.Context, req WriteFactReq) (int64, error) - LatestFact(ctx context.Context, key string) (Fact, error) - LatestFactBySource(ctx context.Context, key, source string) (Fact, error) - Since(ctx context.Context, key string, now time.Time) (time.Duration, error) - Presence(ctx context.Context) (Presence, error) - CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) - MarkReminder(ctx context.Context, id int64, status string) error - ListReminders(ctx context.Context, n int) ([]Reminder, error) - RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error) - ResolveNudge(ctx context.Context, id int64, outcome string, ts time.Time) error - RecentOutcomes(ctx context.Context, rule string, n int) ([]string, error) - RecentFacts(ctx context.Context, n int) ([]Fact, error) - RecentActiveFactsByKind(ctx context.Context, kind string, n int) ([]Fact, error) - CalendarEvents(ctx context.Context, from, to time.Time) ([]Fact, error) - RecentNudges(ctx context.Context, n int) ([]Nudge, error) - // DeliveryAttempts reads the outbox, newest first. An empty status means - // every status (Vikunja #390). - DeliveryAttempts(ctx context.Context, status string, n int) ([]DeliveryAttempt, error) - - // RecentEcosystemTraces reads the ecosystem call log, which lives in its - // own table so machine-rate traces never crowd out human-rate facts. - RecentEcosystemTraces(ctx context.Context, n int) ([]EcosystemTrace, error) - WriteNote(ctx context.Context, ts time.Time, text string, embedding []float32, source string) (int64, error) - QueryNotes(ctx context.Context, embedding []float32, k int) ([]Note, error) - RecentNotes(ctx context.Context, n int) ([]Note, error) - // RecentNotesFromSource — the newest n notes whose source starts with - // prefix. Notes Maven read rather than heard (rss:, crawl:) are excluded - // from recall, so this is the only way to reach them, and it keeps the feed - // answer from being crowded out of a fixed window by his own notes. - RecentNotesFromSource(ctx context.Context, prefix string, n int) ([]Note, error) - - // ProposeTool drafts an inert 'proposed' tool scaffold (maven-callable); - // returns whether a new proposal was written. EnableTool fills cmd + - // destructive and flips status to 'enabled'. DisableTool reverts an - // enabled tool back to proposed (it stays in the store, won't run). - // Enable/DisableTool gate at AuthStepUp (allowlist mutation, human-only); - // ProposeTool is maven-callable (no step-up — she has no passkey). - // LookupTool/ListTools read them. - // scope defaults to "homelab" when empty. - ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) - EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error - DisableTool(ctx context.Context, name string) error - DeleteTool(ctx context.Context, name string) error - LookupTool(ctx context.Context, name string) (Tool, error) - ListTools(ctx context.Context, status string) ([]Tool, error) - RevertFact(ctx context.Context, key string) (int64, error) - - // ListProposedRoutines returns proposed routines, newest first. - ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error) - // DismissProposedRoutine flips a proposed routine to 'dismissed'. - DismissProposedRoutine(ctx context.Context, id int64) error - // AcceptProposedRoutine flips a proposed routine to 'accepted'. The tick - // loop takes the schedule from there — no reminder is created (Vikunja #366). - AcceptProposedRoutine(ctx context.Context, id int64) error - - // CaptureTask records a task. See CaptureTaskReq — this is the single - // intake seam for the voice path, the web form and the future email - // extractor. Idempotent per live normalised text; the response says - // whether a row was actually created. - CaptureTask(ctx context.Context, req CaptureTaskReq) (CaptureTaskResp, error) - // ListTasks returns tasks in one status, newest first. "" is every row, - // "live" is candidate + open (outstanding work). - ListTasks(ctx context.Context, status string) ([]Task, error) - // 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 - - // TickTrace returns the most recent tick's rule trace. The daemon caches - // this after every tick; the store adapter returns an error (trace is not - // persisted — it's a daemon-level cache). - TickTrace(ctx context.Context) (TickTrace, error) - - // MorningStatus returns each configured morning routine's current - // checklist state (see internal/morning): active today/now, which items - // are done, which are still missing. The store adapter returns an error - // (morning routines are daemon-config, not persisted) — same shape as - // TickTrace. - MorningStatus(ctx context.Context) ([]MorningRoutineStatus, error) - - // MCPServers reports the configured MCP servers and their health - // (Vikunja #251). Read-only introspection for /tools — there is no - // "call this tool" method on purpose: an MCP tool runs through the same - // allowlist, confirm turn and act path as any other tool, and a second - // mutation path would be a second thing to get wrong. Empty when the - // mcp config block is absent, which is the default. - MCPServers(ctx context.Context) ([]MCPServerStatus, error) - - // DayPlan returns today's ordered plan — calendar events, pending - // reminders and any morning checklist still outstanding (see - // internal/morning.BuildPlan) — plus the spoken RU rendering of it. - // Read-only: asking for the plan never dispatches or schedules anything. - // The store adapter returns an error (the plan needs the daemon's routine - // config) — same shape as TickTrace and MorningStatus. - DayPlan(ctx context.Context) (DayPlan, error) - - // Chat routes a text utterance through the reactive handler's core path - // (router → dialogue → action → replier) and returns the reply text. - // No audio or stt/tts — for text channels (mavweb, telegram). - // - // conversation names the thread. A parked clarifying question is held per - // conversation, so an unanswered question on one reach cannot eat the next - // utterance from another (Vikunja #466). Empty means the unattributed text - // tap and is still one conversation of its own, separate from the mic. - Chat(ctx context.Context, conversation, text string) (string, error) - - // RecentEvents returns the daemon's unified intake journal, newest first - // (Vikunja #283) — one envelope per thing that arrived, whatever direction - // it came from: a relayed notification, a mail candidate, a feed item, a - // changed page, a spend, a presence probe. - // - // Read-only and daemon-cached, the same shape as TickTrace and DayPlan: - // the store adapter returns an error, because the journal is a bounded - // in-memory ring and not a table. Its contents are a window over intake, - // never the durable record — that is still the fact, note or task the - // intake path wrote. - RecentEvents(ctx context.Context, n int) ([]IntakeEvent, error) -} - // IntakeEvent — one entry of the unified intake journal on the wire. Mirrors // event.Event field for field; the ipc package does not import internal/event // so the wire shape stays independent of the in-process type. diff --git a/internal/ipc/coreapi.go b/internal/ipc/coreapi.go new file mode 100644 index 0000000..481c517 --- /dev/null +++ b/internal/ipc/coreapi.go @@ -0,0 +1,194 @@ +package ipc + +import ( + "context" + "time" +) + +// CoreAPI and the eight domain interfaces it composes (Vikunja #408). +// +// It used to be one flat block of forty methods, and the cost of that shape +// was paid three times over: every method added an arm to the dispatcher, a +// stub to the daemon's locked API, and a stub to every test double. Two of +// those three are already gone — the dispatcher is a table (methodTable in +// server.go), and UnimplementedCoreAPI took the padding out of the doubles and +// out of lockedAPI, which no longer exists. +// +// What was left is the interface itself, and this file is that half. CoreAPI +// is unchanged as a type: the same forty methods, in the same order, so the +// wire contract, the client proxy and the store adapter are all untouched. +// What it gains is a named seam per domain, so a caller that only reads facts +// can say FactAPI and a reader can see which cluster a method belongs to +// without counting lines. +// +// Add a method to the domain it belongs to, not to CoreAPI. + +// FactAPI — the fact store: write, read the current value, read history, and +// undo. Presence sits here because it is a hysteresis view over presence +// facts, not a store of its own. +type FactAPI interface { + WriteFact(ctx context.Context, req WriteFactReq) (int64, error) + LatestFact(ctx context.Context, key string) (Fact, error) + LatestFactBySource(ctx context.Context, key, source string) (Fact, error) + Since(ctx context.Context, key string, now time.Time) (time.Duration, error) + Presence(ctx context.Context) (Presence, error) + RecentFacts(ctx context.Context, n int) ([]Fact, error) + RecentActiveFactsByKind(ctx context.Context, kind string, n int) ([]Fact, error) + CalendarEvents(ctx context.Context, from, to time.Time) ([]Fact, error) + RevertFact(ctx context.Context, key string) (int64, error) +} + +// ReminderAPI — scheduled sends the owner asked for. +type ReminderAPI interface { + CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) + MarkReminder(ctx context.Context, id int64, status string) error + ListReminders(ctx context.Context, n int) ([]Reminder, error) +} + +// NudgeAPI — proactive sends Maven proposed, their outcomes, and the outbox +// they went out through. +type NudgeAPI interface { + RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error) + ResolveNudge(ctx context.Context, id int64, outcome string, ts time.Time) error + RecentOutcomes(ctx context.Context, rule string, n int) ([]string, error) + RecentNudges(ctx context.Context, n int) ([]Nudge, error) + // DeliveryAttempts reads the outbox, newest first. An empty status means + // every status (Vikunja #390). + DeliveryAttempts(ctx context.Context, status string, n int) ([]DeliveryAttempt, error) +} + +// NoteAPI — free text he captured, plus the embedded recall over it. +type NoteAPI interface { + WriteNote(ctx context.Context, ts time.Time, text string, embedding []float32, source string) (int64, error) + QueryNotes(ctx context.Context, embedding []float32, k int) ([]Note, error) + RecentNotes(ctx context.Context, n int) ([]Note, error) + // RecentNotesFromSource — the newest n notes whose source starts with + // prefix. Notes Maven read rather than heard (rss:, crawl:) are excluded + // from recall, so this is the only way to reach them, and it keeps the feed + // answer from being crowded out of a fixed window by his own notes. + RecentNotesFromSource(ctx context.Context, prefix string, n int) ([]Note, error) +} + +// ToolAPI — the capability allowlist and its read side. +// +// ProposeTool drafts an inert 'proposed' tool scaffold (maven-callable); +// returns whether a new proposal was written. EnableTool fills cmd + +// destructive and flips status to 'enabled'. DisableTool reverts an +// enabled tool back to proposed (it stays in the store, won't run). +// Enable/DisableTool gate at AuthStepUp (allowlist mutation, human-only); +// ProposeTool is maven-callable (no step-up — she has no passkey). +// LookupTool/ListTools read them. +// scope defaults to "homelab" when empty. +type ToolAPI interface { + ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) + EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error + DisableTool(ctx context.Context, name string) error + DeleteTool(ctx context.Context, name string) error + LookupTool(ctx context.Context, name string) (Tool, error) + ListTools(ctx context.Context, status string) ([]Tool, error) + + // MCPServers reports the configured MCP servers and their health + // (Vikunja #251). Read-only introspection for /tools — there is no + // "call this tool" method on purpose: an MCP tool runs through the same + // allowlist, confirm turn and act path as any other tool, and a second + // mutation path would be a second thing to get wrong. Empty when the + // mcp config block is absent, which is the default. + MCPServers(ctx context.Context) ([]MCPServerStatus, error) +} + +// RoutineAPI — the shapes of his day: routines Maven noticed and proposed, the +// morning checklist, and today's plan. +// +// MorningStatus and DayPlan are daemon-computed rather than stored, so the +// store adapter returns an error for both — the same shape as TickTrace. +type RoutineAPI interface { + // ListProposedRoutines returns proposed routines, newest first. + ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error) + // DismissProposedRoutine flips a proposed routine to 'dismissed'. + DismissProposedRoutine(ctx context.Context, id int64) error + // AcceptProposedRoutine flips a proposed routine to 'accepted'. The tick + // loop takes the schedule from there — no reminder is created (Vikunja #366). + AcceptProposedRoutine(ctx context.Context, id int64) error + + // MorningStatus returns each configured morning routine's current + // checklist state (see internal/morning): active today/now, which items + // are done, which are still missing. + MorningStatus(ctx context.Context) ([]MorningRoutineStatus, error) + + // DayPlan returns today's ordered plan — calendar events, pending + // reminders and any morning checklist still outstanding (see + // internal/morning.BuildPlan) — plus the spoken RU rendering of it. + // Read-only: asking for the plan never dispatches or schedules anything. + DayPlan(ctx context.Context) (DayPlan, error) +} + +// TaskAPI — outstanding work, whatever surface it arrived from. +type TaskAPI interface { + // CaptureTask records a task. See CaptureTaskReq — this is the single + // intake seam for the voice path, the web form and the future email + // extractor. Idempotent per live normalised text; the response says + // whether a row was actually created. + CaptureTask(ctx context.Context, req CaptureTaskReq) (CaptureTaskResp, error) + // ListTasks returns tasks in one status, newest first. "" is every row, + // "live" is candidate + open (outstanding work). + ListTasks(ctx context.Context, status string) ([]Task, error) + // 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 +} + +// SystemAPI — what the daemon knows about itself, plus the one method that +// runs a whole turn. +type SystemAPI interface { + // TickTrace returns the most recent tick's rule trace. The daemon caches + // this after every tick; the store adapter returns an error (trace is not + // persisted — it's a daemon-level cache). + TickTrace(ctx context.Context) (TickTrace, error) + + // RecentEcosystemTraces reads the ecosystem call log, which lives in its + // own table so machine-rate traces never crowd out human-rate facts. + RecentEcosystemTraces(ctx context.Context, n int) ([]EcosystemTrace, error) + + // RecentEvents returns the daemon's unified intake journal, newest first + // (Vikunja #283) — one envelope per thing that arrived, whatever direction + // it came from: a relayed notification, a mail candidate, a feed item, a + // changed page, a spend, a presence probe. + // + // Read-only and daemon-cached, the same shape as TickTrace and DayPlan: + // the store adapter returns an error, because the journal is a bounded + // in-memory ring and not a table. Its contents are a window over intake, + // never the durable record — that is still the fact, note or task the + // intake path wrote. + RecentEvents(ctx context.Context, n int) ([]IntakeEvent, error) + + // Chat routes a text utterance through the reactive handler's core path + // (router → dialogue → action → replier) and returns the reply text. + // No audio or stt/tts — for text channels (mavweb, telegram). + // + // conversation names the thread. A parked clarifying question is held per + // conversation, so an unanswered question on one reach cannot eat the next + // utterance from another (Vikunja #466). Empty means the unattributed text + // tap and is still one conversation of its own, separate from the mic. + Chat(ctx context.Context, conversation, text string) (string, error) +} + +// CoreAPI — what core exposes to modules. One Go interface, satisfied by: +// - the in-process store adapter (storeapi.go) — used by the daemon +// for modules that live in-process for now (router, delivery) and by tests, +// - the socket-backed server's dispatcher (which delegates to a CoreAPI), +// - the client proxy (client.go) — same interface, over the wire. +// +// So a module imports ipc, holds a CoreAPI, and is agnostic to whether it's +// been wired in-process (tests / daemon-embedded) or socketed (full topology). +// That swappability is the seam the auth layer inserts into without touching +// the module code. +type CoreAPI interface { + FactAPI + ReminderAPI + NudgeAPI + NoteAPI + ToolAPI + RoutineAPI + TaskAPI + SystemAPI +} -- 2.52.0