diff --git a/docs/getting-started.md b/docs/getting-started.md index d2d8166..0663905 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -69,20 +69,15 @@ c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { return core.Result{} }) -c.RegisterTask(func(_ *core.Core, t core.Task) core.Result { - switch task := t.(type) { - case createWorkspaceTask: - path := "/tmp/agent-workbench/" + task.Name - return core.Result{Value: path, OK: true} - } - return core.Result{} +c.Action("workspace.create", func(_ context.Context, opts core.Options) core.Result { + name := opts.String("name") + path := "/tmp/agent-workbench/" + name + return core.Result{Value: path, OK: true} }) c.Command("workspace/create", core.Command{ Action: func(opts core.Options) core.Result { - return c.PERFORM(createWorkspaceTask{ - Name: opts.String("name"), - }) + return c.Action("workspace.create").Run(context.Background(), opts) }, }) ``` @@ -170,20 +165,15 @@ func main() { return core.Result{} }) - c.RegisterTask(func(_ *core.Core, t core.Task) core.Result { - switch task := t.(type) { - case createWorkspaceTask: - path := c.Config().String("workspace.root") + "/" + task.Name - return core.Result{Value: path, OK: true} - } - return core.Result{} + c.Action("workspace.create", func(_ context.Context, opts core.Options) core.Result { + name := opts.String("name") + path := c.Config().String("workspace.root") + "/" + name + return core.Result{Value: path, OK: true} }) c.Command("workspace/create", core.Command{ Action: func(opts core.Options) core.Result { - return c.PERFORM(createWorkspaceTask{ - Name: opts.String("name"), - }) + return c.Action("workspace.create").Run(context.Background(), opts) }, }) diff --git a/docs/index.md b/docs/index.md index 0ec8647..93e203c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,108 +5,56 @@ description: AX-first documentation for the CoreGO framework. # CoreGO -CoreGO is the foundation layer for the Core ecosystem. It gives you one container, one command tree, one message bus, and a small set of shared primitives that repeat across the whole framework. +CoreGO is the foundation layer for the Core ecosystem. Module: `dappco.re/go/core`. -The current module path is `dappco.re/go/core`. +## What CoreGO Provides -## AX View - -CoreGO already follows the main AX ideas from RFC-025: - -- predictable names such as `Core`, `Service`, `Command`, `Options`, `Result`, `Message` -- path-shaped command registration such as `deploy/to/homelab` -- one repeated input shape (`Options`) and one repeated return shape (`Result`) -- comments and examples that show real usage instead of restating the type signature - -## What CoreGO Owns - -| Surface | Purpose | -|---------|---------| -| `Core` | Central container and access point | -| `Service` | Managed lifecycle component | -| `Command` | Path-based command tree node | -| `ACTION`, `QUERY`, `PERFORM` | Decoupled communication between components | -| `Data`, `Drive`, `Fs`, `Config`, `I18n`, `Cli` | Built-in subsystems for common runtime work | -| `E`, `Wrap`, `ErrorLog`, `ErrorPanic` | Structured failures and panic recovery | +| Primitive | Purpose | +|-----------|---------| +| `Core` | Central container — everything registers here | +| `Service` | Lifecycle-managed component (Startable/Stoppable return Result) | +| `Action` | Named callable with panic recovery + entitlement | +| `Task` | Composed sequence of Actions | +| `Registry[T]` | Thread-safe named collection (universal brick) | +| `Command` | Path-based CLI command tree | +| `Process` | Managed execution (Action sugar over go-process) | +| `API` | Remote streams (protocol handlers + Drive) | +| `Entitlement` | Permission gate (default permissive, consumer replaces) | +| `ACTION`, `QUERY` | Anonymous broadcast + request/response | +| `Data`, `Drive`, `Fs`, `Config`, `I18n` | Built-in subsystems | ## Quick Example ```go package main -import ( - "context" - "fmt" - - "dappco.re/go/core" -) - -type flushCacheTask struct { - Name string -} +import "dappco.re/go/core" func main() { - c := core.New(core.Options{ - {Key: "name", Value: "agent-workbench"}, - }) - - c.Service("cache", core.Service{ - OnStart: func() core.Result { - core.Info("cache ready", "app", c.App().Name) - return core.Result{OK: true} - }, - OnStop: func() core.Result { - core.Info("cache stopped", "app", c.App().Name) - return core.Result{OK: true} - }, - }) - - c.RegisterTask(func(_ *core.Core, task core.Task) core.Result { - switch task.(type) { - case flushCacheTask: - return core.Result{Value: "cache flushed", OK: true} - } - return core.Result{} - }) - - c.Command("cache/flush", core.Command{ - Action: func(opts core.Options) core.Result { - return c.PERFORM(flushCacheTask{Name: opts.String("name")}) - }, - }) - - if !c.ServiceStartup(context.Background(), nil).OK { - panic("startup failed") - } - - r := c.Cli().Run("cache", "flush", "--name=session-store") - fmt.Println(r.Value) - - _ = c.ServiceShutdown(context.Background()) + c := core.New( + core.WithOption("name", "agent-workbench"), + core.WithService(cache.Register), + core.WithServiceLock(), + ) + c.Run() } ``` -## Documentation Paths +## API Specification + +The full contract is `docs/RFC.md` (21 sections, 1476 lines). An agent should be able to write a service from RFC.md alone. + +## Documentation | Path | Covers | |------|--------| -| [getting-started.md](getting-started.md) | First runnable CoreGO app | -| [primitives.md](primitives.md) | `Options`, `Result`, `Service`, `Message`, `Query`, `Task` | -| [services.md](services.md) | Service registry, service locks, runtime helpers | -| [commands.md](commands.md) | Path-based commands and CLI execution | -| [messaging.md](messaging.md) | `ACTION`, `QUERY`, `QUERYALL`, `PERFORM`, `PerformAsync` | -| [lifecycle.md](lifecycle.md) | Startup, shutdown, context, background task draining | -| [configuration.md](configuration.md) | Constructor options, config state, feature flags | -| [subsystems.md](subsystems.md) | `App`, `Data`, `Drive`, `Fs`, `I18n`, `Cli` | -| [errors.md](errors.md) | Structured errors, logging helpers, panic recovery | -| [testing.md](testing.md) | Test naming and framework-level testing patterns | -| [pkg/core.md](pkg/core.md) | Package-level reference summary | -| [pkg/log.md](pkg/log.md) | Logging reference for the root package | -| [pkg/PACKAGE_STANDARDS.md](pkg/PACKAGE_STANDARDS.md) | AX package-authoring guidance | - -## Good Reading Order - -1. Start with [getting-started.md](getting-started.md). -2. Learn the repeated shapes in [primitives.md](primitives.md). -3. Pick the integration path you need next: [services.md](services.md), [commands.md](commands.md), or [messaging.md](messaging.md). -4. Use [subsystems.md](subsystems.md), [errors.md](errors.md), and [testing.md](testing.md) as reference pages while building. +| [RFC.md](RFC.md) | Authoritative API contract (21 sections) | +| [primitives.md](primitives.md) | Option, Result, Action, Task, Registry, Entitlement | +| [services.md](services.md) | Service registry, ServiceRuntime, service locks | +| [commands.md](commands.md) | Path-based commands, Managed field | +| [messaging.md](messaging.md) | ACTION, QUERY, named Actions, PerformAsync | +| [lifecycle.md](lifecycle.md) | RunE, ServiceStartup, ServiceShutdown | +| [subsystems.md](subsystems.md) | App, Data, Drive, Fs, Config, I18n | +| [errors.md](errors.md) | core.E(), structured errors, panic recovery | +| [testing.md](testing.md) | AX-7 TestFile_Function_{Good,Bad,Ugly} | +| [configuration.md](configuration.md) | WithOption, WithService, WithServiceLock | diff --git a/docs/messaging.md b/docs/messaging.md index 688893a..785a0c4 100644 --- a/docs/messaging.md +++ b/docs/messaging.md @@ -1,171 +1,127 @@ --- title: Messaging -description: ACTION, QUERY, QUERYALL, PERFORM, and async task flow. +description: ACTION, QUERY, QUERYALL, named Actions, and async dispatch. --- # Messaging -CoreGO uses one message bus for broadcasts, lookups, and work dispatch. +CoreGO has two messaging layers: anonymous broadcast (ACTION/QUERY) and named Actions. -## Message Types +## Anonymous Broadcast -```go -type Message any -type Query any -type Task any -``` +### `ACTION` -Your own structs define the protocol. - -```go -type repositoryIndexed struct { - Name string -} - -type repositoryCountQuery struct{} - -type syncRepositoryTask struct { - Name string -} -``` - -## `ACTION` - -`ACTION` is a broadcast. +Fire-and-forget broadcast to all registered handlers. Each handler is wrapped in panic recovery. Handler return values are ignored — all handlers fire regardless. ```go c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { - switch m := msg.(type) { - case repositoryIndexed: - core.Info("repository indexed", "name", m.Name) - return core.Result{OK: true} - } - return core.Result{OK: true} + if ev, ok := msg.(repositoryIndexed); ok { + core.Info("indexed", "name", ev.Name) + } + return core.Result{OK: true} }) -r := c.ACTION(repositoryIndexed{Name: "core-go"}) +c.ACTION(repositoryIndexed{Name: "core-go"}) ``` -### Behavior +### `QUERY` -- all registered action handlers are called in their current registration order -- if a handler returns `OK:false`, dispatch stops and that `Result` is returned -- if no handler fails, `ACTION` returns `Result{OK:true}` - -## `QUERY` - -`QUERY` is first-match request-response. +First handler to return `OK:true` wins. ```go c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { - switch q.(type) { - case repositoryCountQuery: - return core.Result{Value: 42, OK: true} - } - return core.Result{} + if _, ok := q.(repositoryCountQuery); ok { + return core.Result{Value: 42, OK: true} + } + return core.Result{} }) r := c.QUERY(repositoryCountQuery{}) ``` -### Behavior +### `QUERYALL` -- handlers run until one returns `OK:true` -- the first successful result wins -- if nothing handles the query, CoreGO returns an empty `Result` - -## `QUERYALL` - -`QUERYALL` collects every successful non-nil response. +Collects every successful non-nil response. ```go r := c.QUERYALL(repositoryCountQuery{}) results := r.Value.([]any) ``` -### Behavior +## Named Actions -- every query handler is called -- only `OK:true` results with non-nil `Value` are collected -- the call itself returns `OK:true` even when the result list is empty +Named Actions are the typed, inspectable replacement for anonymous dispatch. See Section 18 of `RFC.md`. -## `PERFORM` - -`PERFORM` dispatches a task to the first handler that accepts it. +### Register and Invoke ```go -c.RegisterTask(func(_ *core.Core, t core.Task) core.Result { - switch task := t.(type) { - case syncRepositoryTask: - return core.Result{Value: "synced " + task.Name, OK: true} - } - return core.Result{} +// Register during OnStartup +c.Action("repo.sync", func(ctx context.Context, opts core.Options) core.Result { + name := opts.String("name") + return core.Result{Value: "synced " + name, OK: true} }) -r := c.PERFORM(syncRepositoryTask{Name: "core-go"}) +// Invoke by name +r := c.Action("repo.sync").Run(ctx, core.NewOptions( + core.Option{Key: "name", Value: "core-go"}, +)) ``` -### Behavior - -- handlers run until one returns `OK:true` -- the first successful result wins -- if nothing handles the task, CoreGO returns an empty `Result` - -## `PerformAsync` - -`PerformAsync` runs a task in a background goroutine and returns a generated task identifier. +### Capability Check ```go -r := c.PerformAsync(syncRepositoryTask{Name: "core-go"}) -taskID := r.Value.(string) +if c.Action("process.run").Exists() { + // go-process is registered +} + +c.Actions() // []string of all registered action names ``` -### Generated Events +### Permission Gate -Async execution emits three action messages: +Every `Action.Run()` checks `c.Entitled(action.Name)` before executing. See Section 21 of `RFC.md`. -| Message | When | -|---------|------| -| `ActionTaskStarted` | just before background execution begins | -| `ActionTaskProgress` | whenever `Progress` is called | -| `ActionTaskCompleted` | after the task finishes or panics | +## Task Composition -Example listener: +A Task is a named sequence of Actions: + +```go +c.Task("deploy", core.Task{ + Steps: []core.Step{ + {Action: "go.build"}, + {Action: "go.test"}, + {Action: "docker.push"}, + {Action: "notify.slack", Async: true}, + }, +}) + +r := c.Task("deploy").Run(ctx, c, opts) +``` + +Sequential steps stop on first failure. `Async: true` steps fire without blocking. `Input: "previous"` pipes output. + +## Background Execution + +```go +r := c.PerformAsync("repo.sync", opts) +taskID := r.Value.(string) + +c.Progress(taskID, 0.5, "indexing commits", "repo.sync") +``` + +Broadcasts `ActionTaskStarted`, `ActionTaskProgress`, `ActionTaskCompleted` as ACTION messages. + +### Completion Listener ```go c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { - switch m := msg.(type) { - case core.ActionTaskCompleted: - core.Info("task completed", "task", m.TaskIdentifier, "err", m.Error) - } - return core.Result{OK: true} + if ev, ok := msg.(core.ActionTaskCompleted); ok { + core.Info("done", "task", ev.TaskIdentifier, "ok", ev.Result.OK) + } + return core.Result{OK: true} }) ``` -## Progress Updates +## Shutdown -```go -c.Progress(taskID, 0.5, "indexing commits", syncRepositoryTask{Name: "core-go"}) -``` - -That broadcasts `ActionTaskProgress`. - -## `TaskWithIdentifier` - -Tasks that implement `TaskWithIdentifier` receive the generated ID before dispatch. - -```go -type trackedTask struct { - ID string - Name string -} - -func (t *trackedTask) SetTaskIdentifier(id string) { t.ID = id } -func (t *trackedTask) GetTaskIdentifier() string { return t.ID } -``` - -## Shutdown Interaction - -When shutdown has started, `PerformAsync` returns an empty `Result` instead of scheduling more work. - -This is why `ServiceShutdown` can safely drain the outstanding background tasks before stopping services. +When shutdown has started, `PerformAsync` returns an empty `Result`. `ServiceShutdown` drains outstanding background work before stopping services. diff --git a/docs/primitives.md b/docs/primitives.md index 43701f2..0719311 100644 --- a/docs/primitives.md +++ b/docs/primitives.md @@ -5,165 +5,172 @@ description: The repeated shapes that make CoreGO easy to navigate. # Core Primitives -CoreGO is easiest to use when you read it as a small vocabulary repeated everywhere. Most of the framework is built from the same handful of types. +CoreGO is built from a small vocabulary repeated everywhere. ## Primitive Map | Type | Used For | |------|----------| -| `Options` | Input values and lightweight metadata | +| `Option` / `Options` | Input values and metadata | | `Result` | Output values and success state | | `Service` | Lifecycle-managed components | -| `Message` | Broadcast events | -| `Query` | Request-response lookups | -| `Task` | Side-effecting work items | +| `Action` | Named callable with panic recovery + entitlement | +| `Task` | Composed sequence of Actions | +| `Registry[T]` | Thread-safe named collection | +| `Entitlement` | Permission check result | +| `Message` | Broadcast events (ACTION) | +| `Query` | Request-response lookups (QUERY) | ## `Option` and `Options` `Option` is one key-value pair. `Options` is an ordered slice of them. ```go -opts := core.Options{ - {Key: "name", Value: "brain"}, - {Key: "path", Value: "prompts"}, - {Key: "debug", Value: true}, -} -``` +opts := core.NewOptions( + core.Option{Key: "name", Value: "brain"}, + core.Option{Key: "path", Value: "prompts"}, + core.Option{Key: "debug", Value: true}, +) -Use the helpers to read values: - -```go name := opts.String("name") -path := opts.String("path") debug := opts.Bool("debug") -hasPath := opts.Has("path") -raw := opts.Get("name") +raw := opts.Get("name") // Result{Value, OK} +opts.Has("path") // true +opts.Len() // 3 ``` -### Important Details - -- `Get` returns the first matching key. -- `String`, `Int`, and `Bool` do not convert between types. -- Missing keys return zero values. -- CLI flags with values are stored as strings, so `--port=8080` should be read with `opts.String("port")`, not `opts.Int("port")`. - ## `Result` -`Result` is the universal return shape. +Universal return shape. Every Core operation returns Result. ```go -r := core.Result{Value: "ready", OK: true} +type Result struct { + Value any + OK bool +} +r := c.Config().Get("host") if r.OK { - fmt.Println(r.Value) + host := r.Value.(string) } ``` -It has two jobs: - -- carry a value when work succeeds -- carry either an error or an empty state when work does not succeed - -### `Result.Result(...)` - -The `Result()` method adapts plain Go values and `(value, error)` pairs into a `core.Result`. +The `Result()` method adapts Go `(value, error)` pairs: ```go -r1 := core.Result{}.Result("hello") -r2 := core.Result{}.Result(file, err) +r := core.Result{}.Result(file, err) ``` -This is how several built-in helpers bridge standard-library calls. - ## `Service` -`Service` is the managed lifecycle DTO stored in the registry. +Managed lifecycle component stored in the `ServiceRegistry`. ```go -svc := core.Service{ - Name: "cache", - Options: core.Options{ - {Key: "backend", Value: "memory"}, - }, - OnStart: func() core.Result { - return core.Result{OK: true} - }, - OnStop: func() core.Result { - return core.Result{OK: true} - }, - OnReload: func() core.Result { - return core.Result{OK: true} - }, +core.Service{ + OnStart: func() core.Result { return core.Result{OK: true} }, + OnStop: func() core.Result { return core.Result{OK: true} }, } ``` -### Important Details - -- `OnStart` and `OnStop` are used by the framework lifecycle. -- `OnReload` is stored on the service DTO, but CoreGO does not currently call it automatically. -- The registry stores `*core.Service`, not arbitrary typed service instances. - -## `Message`, `Query`, and `Task` - -These are simple aliases to `any`. +Or via `Startable`/`Stoppable` interfaces (preferred for named services): ```go -type Message any -type Query any -type Task any +type Startable interface { OnStartup(ctx context.Context) Result } +type Stoppable interface { OnShutdown(ctx context.Context) Result } ``` -That means your own structs become the protocol: +## `Action` + +Named callable — the atomic unit of work. Registered by name, invoked by name. ```go -type deployStarted struct { - Environment string -} +type ActionHandler func(context.Context, Options) Result -type workspaceCountQuery struct{} - -type syncRepositoryTask struct { - Name string +type Action struct { + Name string + Handler ActionHandler + Description string + Schema Options } ``` -## `TaskWithIdentifier` +`Action.Run()` includes panic recovery and entitlement checking. -Long-running tasks can opt into task identifiers. +## `Task` + +Composed sequence of Actions: ```go -type indexedTask struct { - ID string +type Task struct { + Name string + Description string + Steps []Step } -func (t *indexedTask) SetTaskIdentifier(id string) { t.ID = id } -func (t *indexedTask) GetTaskIdentifier() string { return t.ID } +type Step struct { + Action string + With Options + Async bool + Input string // "previous" = output of last step +} ``` -If a task implements `TaskWithIdentifier`, `PerformAsync` injects the generated `task-N` identifier before dispatch. +## `Registry[T]` + +Thread-safe named collection with insertion order and 3 lock modes: + +```go +r := core.NewRegistry[*MyService]() +r.Set("brain", svc) +r.Get("brain") // Result +r.Has("brain") // bool +r.Names() // []string (insertion order) +r.Each(func(name string, svc *MyService) { ... }) +r.Lock() // fully frozen +r.Seal() // no new keys, updates OK +``` + +## `Entitlement` + +Permission check result: + +```go +type Entitlement struct { + Allowed bool + Unlimited bool + Limit int + Used int + Remaining int + Reason string +} + +e := c.Entitled("social.accounts", 3) +e.NearLimit(0.8) // true if > 80% used +e.UsagePercent() // 75.0 +``` + +## `Message` and `Query` + +IPC type aliases for the anonymous broadcast system: + +```go +type Message any // broadcast via ACTION +type Query any // request/response via QUERY +``` + +For typed, named dispatch use `c.Action("name").Run(ctx, opts)`. ## `ServiceRuntime[T]` -`ServiceRuntime[T]` is the small helper for packages that want to keep a Core reference and a typed options struct together. +Composition helper for services that need Core access and typed options: ```go -type agentServiceOptions struct { - WorkspacePath string +type MyService struct { + *core.ServiceRuntime[MyOptions] } -type agentService struct { - *core.ServiceRuntime[agentServiceOptions] -} - -runtime := core.NewServiceRuntime(c, agentServiceOptions{ - WorkspacePath: "/srv/agent-workspaces", -}) +runtime := core.NewServiceRuntime(c, MyOptions{BufferSize: 1024}) +runtime.Core() // *Core +runtime.Options() // MyOptions +runtime.Config() // shortcut to Core().Config() ``` - -It exposes: - -- `Core()` -- `Options()` -- `Config()` - -This helper does not register anything by itself. It is a composition aid for package authors. diff --git a/docs/testing.md b/docs/testing.md index 656634a..3e0c7d4 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -78,14 +78,12 @@ assert.Equal(t, "pong", c.QUERY("ping").Value) ``` ```go -c.RegisterTask(func(_ *core.Core, t core.Task) core.Result { - if t == "compute" { - return core.Result{Value: 42, OK: true} - } - return core.Result{} +c.Action("compute", func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: 42, OK: true} }) -assert.Equal(t, 42, c.PERFORM("compute").Value) +r := c.Action("compute").Run(context.Background(), core.NewOptions()) +assert.Equal(t, 42, r.Value) ``` ## Test Async Work