diff --git a/docs/go/index.md b/docs/go/index.md index 40c70ef..56f2b9f 100644 --- a/docs/go/index.md +++ b/docs/go/index.md @@ -1,95 +1,242 @@ --- title: Core Go Framework -description: Dependency injection and service lifecycle framework for Go. +description: Dependency injection, service lifecycle, and permission framework for Go. --- # Core Go Framework -Core (`forge.lthn.ai/core/go`) is a dependency injection and service lifecycle framework for Go. It provides a typed service registry, lifecycle hooks, and a message-passing bus for decoupled communication between services. +Core (`dappco.re/go/core`) is a dependency injection, service lifecycle, and permission framework for Go. It provides a typed service registry, lifecycle hooks, a message-passing bus, named actions with task composition, and an entitlement primitive for permission gating. -This is the foundation layer of the ecosystem. It has no CLI, no GUI, and minimal dependencies. +This is the foundation layer of the ecosystem. It has no CLI, no GUI, and minimal dependencies (stdlib + go-io + go-log). ## Installation ```bash -go get forge.lthn.ai/core/go +go get dappco.re/go/core ``` Requires Go 1.26 or later. -## What It Does - -Core solves three problems that every non-trivial Go application eventually faces: - -1. **Service wiring** -- how do you register, retrieve, and type-check services without import cycles? -2. **Lifecycle management** -- how do you start and stop services in the right order? -3. **Decoupled communication** -- how do services talk to each other without knowing each other's types? - -## Packages - -| Package | Purpose | -|---------|---------| -| [`pkg/core`](services.md) | DI container, service registry, lifecycle, message bus | -| `pkg/log` | Structured logger service with Core integration | - ## Quick Example ```go package main -import ( - "context" - "fmt" - - "forge.lthn.ai/core/go/pkg/core" - "forge.lthn.ai/core/go/pkg/log" -) +import "dappco.re/go/core" func main() { - c, err := core.New( - core.WithName("log", log.NewService(log.Options{Level: log.LevelInfo})), - core.WithServiceLock(), // Prevent late registration + c := core.New( + core.WithOption("name", "my-app"), + core.WithService(mypackage.Register), + core.WithService(monitor.Register), + core.WithServiceLock(), ) - if err != nil { - panic(err) - } - - // Start all services - if err := c.ServiceStartup(context.Background(), nil); err != nil { - panic(err) - } - - // Type-safe retrieval - logger, err := core.ServiceFor[*log.Service](c, "log") - if err != nil { - panic(err) - } - fmt.Println("Log level:", logger.Level()) - - // Shut down (reverse order) - _ = c.ServiceShutdown(context.Background()) + c.Run() } ``` +`core.New()` returns `*Core`. Services register via factory functions. `Run()` calls `ServiceStartup`, runs the CLI, then `ServiceShutdown`. For error handling use `RunE()` which returns `error` instead of calling `os.Exit`. + +## Service Registration + +```go +func Register(c *core.Core) core.Result { + svc := &MyService{ + ServiceRuntime: core.NewServiceRuntime(c, MyOptions{}), + } + return core.Result{Value: svc, OK: true} +} + +// In main: +core.New(core.WithService(mypackage.Register)) +``` + +Services implement `Startable` and/or `Stoppable` for lifecycle hooks: + +```go +type Startable interface { + OnStartup(ctx context.Context) Result +} + +type Stoppable interface { + OnShutdown(ctx context.Context) Result +} +``` + +## Subsystem Accessors + +```go +c.Options() // *Options — input configuration +c.App() // *App — application identity +c.Config() // *Config — runtime settings, feature flags +c.Data() // *Data — embedded assets (Registry[*Embed]) +c.Drive() // *Drive — transport handles (Registry[*DriveHandle]) +c.Fs() // *Fs — filesystem I/O (sandboxable) +c.Cli() // *Cli — CLI command framework +c.IPC() // *Ipc — message bus internals +c.Log() // *ErrorLog — structured logging +c.Error() // *ErrorPanic — panic recovery +c.I18n() // *I18n — internationalisation +c.Process() // *Process — managed execution (Action sugar) +c.Context() // context.Context — Core's lifecycle context +c.Env(key) // string — environment variable (cached at init) +``` + +## Primitive Types + +```go +// Option — the atom +core.Option{Key: "name", Value: "brain"} + +// Options — universal input +opts := core.NewOptions( + core.Option{Key: "name", Value: "myapp"}, + core.Option{Key: "port", Value: 8080}, +) +opts.String("name") // "myapp" +opts.Int("port") // 8080 + +// Result — universal output +core.Result{Value: svc, OK: true} +``` + +## IPC — Message Passing + +```go +// Broadcast (fire-and-forget) +c.ACTION(messages.AgentCompleted{Agent: "codex", Status: "completed"}) + +// Query (first responder) +r := c.QUERY(MyQuery{Name: "brain"}) + +// Register handler +c.RegisterAction(func(c *core.Core, msg core.Message) core.Result { + if ev, ok := msg.(messages.AgentCompleted); ok { /* handle */ } + return core.Result{OK: true} +}) +``` + +## Named Actions + +```go +// Register +c.Action("git.log", func(ctx context.Context, opts core.Options) core.Result { + dir := opts.String("dir") + return c.Process().RunIn(ctx, dir, "git", "log", "--oneline") +}) + +// Invoke +r := c.Action("git.log").Run(ctx, core.NewOptions( + core.Option{Key: "dir", Value: "/repo"}, +)) + +// Check capability +if c.Action("process.run").Exists() { /* can run commands */ } +``` + +## Task Composition + +```go +c.Task("deploy", core.TaskDef{ + Steps: []core.Step{ + {Action: "go.build"}, + {Action: "go.test"}, + {Action: "docker.push"}, + {Action: "ansible.deploy", Async: true}, + }, +}) + +r := c.Task("deploy").Run(ctx, c, opts) +``` + +Sequential steps stop on failure. Async steps fire without blocking. `Input: "previous"` pipes the last step's output to the next. + +## Process Primitive + +```go +// Run a command (delegates to Action "process.run") +r := c.Process().Run(ctx, "git", "log", "--oneline") +r := c.Process().RunIn(ctx, "/repo", "go", "test", "./...") + +// Permission by registration: +// No go-process registered → c.Process().Run() returns Result{OK: false} +// go-process registered → executes the command +``` + +## Registry[T] + +Thread-safe named collection — the universal brick for all registries. + +```go +r := core.NewRegistry[*MyService]() +r.Set("brain", brainSvc) +r.Get("brain") // Result{brainSvc, true} +r.Has("brain") // true +r.Names() // insertion order +r.List("process.*") // glob match +r.Each(func(name string, svc *MyService) { ... }) +r.Lock() // fully frozen +r.Seal() // no new keys, updates OK + +// Cross-cutting queries +c.RegistryOf("services").Names() +c.RegistryOf("actions").List("process.*") +``` + +## Commands + +```go +c.Command("deploy/to/homelab", core.Command{ + Action: handler, + Managed: "process.daemon", // go-process provides lifecycle +}) +``` + +Path = hierarchy. `deploy/to/homelab` becomes `myapp deploy to homelab` in CLI. + +## Utilities + +```go +core.ID() // "id-1-a3f2b1" — unique identifier +core.ValidateName("brain") // Result{OK: true} +core.SanitisePath("../../x") // "x" +core.E("op", "msg", err) // structured error + +fs.WriteAtomic(path, data) // write-to-temp-then-rename +fs.NewUnrestricted() // full filesystem access +fs.Root() // sandbox root path +``` + +## Error Handling + +All errors use `core.E()`: + +```go +return core.E("service.Method", "what failed", underlyingErr) +``` + +Never use `fmt.Errorf`, `errors.New`, or `log.*`. Core handles all error reporting. + ## Documentation | Page | Covers | |------|--------| -| [Getting Started](getting-started.md) | Creating a Core app, registering your first service | -| [Services](services.md) | Service registration, `ServiceRuntime`, factory pattern | -| [Lifecycle](lifecycle.md) | `Startable`/`Stoppable` interfaces, startup/shutdown order | -| [Messaging](messaging.md) | ACTION, QUERY, PERFORM -- the message bus | -| [Configuration](configuration.md) | `WithService`, `WithName`, `WithAssets`, `WithServiceLock` options | -| [Testing](testing.md) | Test naming conventions, test helpers, fuzz testing | -| [Errors](errors.md) | `E()` helper, `Error` struct, unwrapping | +| [Getting Started](getting-started.md) | Installing the Core CLI, first build | +| [Configuration](configuration.md) | Config files, environment variables | +| [Workflows](workflows.md) | Common task sequences | +| [Packages](packages/index.md) | Ecosystem package reference | + +## API Specification + +The full API contract lives in [`docs/RFC.md`](https://forge.lthn.ai/core/go/src/branch/dev/docs/RFC.md) — 3,800+ lines covering all 21 sections, 108 findings, and implementation plans. ## Dependencies Core is deliberately minimal: -- `forge.lthn.ai/core/go-io` -- abstract storage (local, S3, SFTP, WebDAV) -- `forge.lthn.ai/core/go-log` -- structured logging -- `github.com/stretchr/testify` -- test assertions (test-only) +- `dappco.re/go/core/io` — abstract storage (local, S3, SFTP, WebDAV) +- `dappco.re/go/core/log` — structured logging +- `github.com/stretchr/testify` — test assertions (test-only) ## Licence