Design Philosophy: - Core is Lego Bricks — export primitives, reduce downstream LOC - Export rules: struct fields yes, mutexes no - Why core/go is minimal (stdlib-only, layers import downward) Known Issues (8): 1. Dual IPC naming (ACTION vs Action) 2. MustServiceFor uses panic (contradicts Result pattern) 3. Embed() legacy accessor (dead code) 4. Package-level vs Core-level logging (document boundary) 5. RegisterAction in wrong file (task.go vs ipc.go) 6. serviceRegistry unexported (should be Lego brick) 7. No c.Process() accessor (planned) 8. NewRuntime/NewWithFactories legacy (verify usage) Co-Authored-By: Virgil <virgil@lethean.io>
676 lines
18 KiB
Markdown
676 lines
18 KiB
Markdown
# CoreGO API Contract — RFC Specification
|
|
|
|
> `dappco.re/go/core` — Dependency injection, service lifecycle, and message-passing framework.
|
|
> This document is the authoritative API contract. An agent should be able to write a service
|
|
> that registers with Core from this document alone.
|
|
|
|
**Status:** Living document
|
|
**Module:** `dappco.re/go/core`
|
|
**Version:** v0.7.0+
|
|
|
|
---
|
|
|
|
## 1. Core — The Container
|
|
|
|
Core is the central application container. Everything registers with Core, communicates through Core, and has its lifecycle managed by Core.
|
|
|
|
### 1.1 Creation
|
|
|
|
```go
|
|
c := core.New(
|
|
core.WithOption("name", "my-app"),
|
|
core.WithService(mypackage.Register),
|
|
core.WithService(anotherpackage.Register),
|
|
core.WithServiceLock(),
|
|
)
|
|
c.Run()
|
|
```
|
|
|
|
`core.New()` returns `*Core` (not Result — Core is the one type that can't wrap its own creation error). Functional options are applied in order. `WithServiceLock()` prevents late service registration.
|
|
|
|
### 1.2 Lifecycle
|
|
|
|
```
|
|
New() → WithService factories called → LockApply()
|
|
Run() → ServiceStartup() → Cli.Run() → ServiceShutdown()
|
|
```
|
|
|
|
`Run()` is blocking. `ServiceStartup` calls `OnStartup(ctx)` on all services implementing `Startable`. `ServiceShutdown` calls `OnShutdown(ctx)` on all `Stoppable` services. Shutdown uses `context.Background()` — not the Core context (which is already cancelled).
|
|
|
|
### 1.3 Subsystem Accessors
|
|
|
|
Every subsystem is accessed via a method on Core:
|
|
|
|
```go
|
|
c.Options() // *Options — input configuration
|
|
c.App() // *App — application metadata (name, version)
|
|
c.Config() // *Config — runtime settings, feature flags
|
|
c.Data() // *Data — embedded assets mounted by packages
|
|
c.Drive() // *Drive — transport handles (API, MCP, SSH)
|
|
c.Fs() // *Fs — filesystem I/O (sandboxable)
|
|
c.Cli() // *Cli — CLI command framework
|
|
c.IPC() // *Ipc — message bus internals
|
|
c.I18n() // *I18n — internationalisation
|
|
c.Error() // *ErrorPanic — panic recovery
|
|
c.Log() // *ErrorLog — structured logging
|
|
c.Context() // context.Context — Core's lifecycle context
|
|
c.Env(key) // string — environment variable (cached at init)
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Primitive Types
|
|
|
|
### 2.1 Option
|
|
|
|
The atom. A single key-value pair.
|
|
|
|
```go
|
|
core.Option{Key: "name", Value: "brain"}
|
|
core.Option{Key: "port", Value: 8080}
|
|
core.Option{Key: "debug", Value: true}
|
|
```
|
|
|
|
### 2.2 Options
|
|
|
|
A collection of Option with typed accessors.
|
|
|
|
```go
|
|
opts := core.NewOptions(
|
|
core.Option{Key: "name", Value: "myapp"},
|
|
core.Option{Key: "port", Value: 8080},
|
|
core.Option{Key: "debug", Value: true},
|
|
)
|
|
|
|
opts.String("name") // "myapp"
|
|
opts.Int("port") // 8080
|
|
opts.Bool("debug") // true
|
|
opts.Has("name") // true
|
|
opts.Len() // 3
|
|
|
|
opts.Set("name", "new-name")
|
|
opts.Get("name") // Result{Value: "new-name", OK: true}
|
|
```
|
|
|
|
### 2.3 Result
|
|
|
|
Universal return type. Every Core operation returns Result.
|
|
|
|
```go
|
|
type Result struct {
|
|
Value any
|
|
OK bool
|
|
}
|
|
```
|
|
|
|
Usage patterns:
|
|
|
|
```go
|
|
// Check success
|
|
r := c.Config().Get("database.host")
|
|
if r.OK {
|
|
host := r.Value.(string)
|
|
}
|
|
|
|
// Service factory returns Result
|
|
func Register(c *core.Core) core.Result {
|
|
svc := &MyService{}
|
|
return core.Result{Value: svc, OK: true}
|
|
}
|
|
|
|
// Error as Result
|
|
return core.Result{Value: err, OK: false}
|
|
```
|
|
|
|
No generics on Result. Type-assert the Value when needed. This is deliberate — `Result` is universal across all subsystems without carrying type parameters.
|
|
|
|
### 2.4 Message, Query, Task
|
|
|
|
IPC type aliases — all are `any` at the type level, distinguished by usage:
|
|
|
|
```go
|
|
type Message any // broadcast via ACTION — fire and forget
|
|
type Query any // request/response via QUERY — returns first handler's result
|
|
type Task any // work unit via PERFORM — tracked with progress
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Service System
|
|
|
|
### 3.1 Registration
|
|
|
|
Services register via factory functions passed to `WithService`:
|
|
|
|
```go
|
|
core.New(
|
|
core.WithService(mypackage.Register),
|
|
)
|
|
```
|
|
|
|
The factory signature is `func(*Core) Result`. The returned `Result.Value` is the service instance.
|
|
|
|
### 3.2 Factory Pattern
|
|
|
|
```go
|
|
func Register(c *core.Core) core.Result {
|
|
svc := &MyService{
|
|
runtime: core.NewServiceRuntime(c, MyOptions{}),
|
|
}
|
|
return core.Result{Value: svc, OK: true}
|
|
}
|
|
```
|
|
|
|
`NewServiceRuntime[T]` gives the service access to Core and typed options:
|
|
|
|
```go
|
|
type MyService struct {
|
|
*core.ServiceRuntime[MyOptions]
|
|
}
|
|
|
|
// Access Core from within the service:
|
|
func (s *MyService) doSomething() {
|
|
c := s.Core()
|
|
cfg := s.Config().String("my.setting")
|
|
}
|
|
```
|
|
|
|
### 3.3 Auto-Discovery
|
|
|
|
`WithService` reflects on the returned instance to discover:
|
|
- **Package name** → service name (from reflect type path)
|
|
- **Startable interface** → `OnStartup(ctx) error` called during `ServiceStartup`
|
|
- **Stoppable interface** → `OnShutdown(ctx) error` called during `ServiceShutdown`
|
|
- **HandleIPCEvents method** → auto-registered as IPC handler
|
|
|
|
### 3.4 Retrieval
|
|
|
|
```go
|
|
// Type-safe retrieval
|
|
svc, ok := core.ServiceFor[*MyService](c, "mypackage")
|
|
if !ok {
|
|
// service not registered
|
|
}
|
|
|
|
// Must variant (panics if not found)
|
|
svc := core.MustServiceFor[*MyService](c, "mypackage")
|
|
|
|
// List all registered services
|
|
names := c.Services() // []string
|
|
```
|
|
|
|
### 3.5 Lifecycle Interfaces
|
|
|
|
```go
|
|
type Startable interface {
|
|
OnStartup(ctx context.Context) error
|
|
}
|
|
|
|
type Stoppable interface {
|
|
OnShutdown(ctx context.Context) error
|
|
}
|
|
```
|
|
|
|
Services implementing these are automatically called during `c.Run()`.
|
|
|
|
---
|
|
|
|
## 4. IPC — Message Passing
|
|
|
|
### 4.1 ACTION (broadcast)
|
|
|
|
Fire-and-forget broadcast to all registered handlers:
|
|
|
|
```go
|
|
// Send
|
|
c.ACTION(messages.AgentCompleted{
|
|
Agent: "codex", Repo: "go-io", Status: "completed",
|
|
})
|
|
|
|
// Register handler
|
|
c.RegisterAction(func(c *core.Core, msg core.Message) core.Result {
|
|
if ev, ok := msg.(messages.AgentCompleted); ok {
|
|
// handle completion
|
|
}
|
|
return core.Result{OK: true}
|
|
})
|
|
```
|
|
|
|
All handlers receive all messages. Type-switch to filter. Return `Result{OK: true}` always (errors are logged, not propagated).
|
|
|
|
### 4.2 QUERY (request/response)
|
|
|
|
First handler to return a non-empty result wins:
|
|
|
|
```go
|
|
// Send
|
|
result := c.QUERY(MyQuery{Name: "brain"})
|
|
if result.OK {
|
|
svc := result.Value
|
|
}
|
|
|
|
// Register handler
|
|
c.RegisterQuery(func(c *core.Core, q core.Query) core.Result {
|
|
if mq, ok := q.(MyQuery); ok {
|
|
return core.Result{Value: found, OK: true}
|
|
}
|
|
return core.Result{OK: false} // not my query
|
|
})
|
|
```
|
|
|
|
### 4.3 PERFORM (tracked task)
|
|
|
|
```go
|
|
// Execute with progress tracking
|
|
c.PERFORM(MyTask{Data: payload})
|
|
|
|
// Register task handler
|
|
c.RegisterTask(func(c *core.Core, t core.Task) core.Result {
|
|
// do work, report progress
|
|
c.Progress(taskID, 0.5, "halfway done", t)
|
|
return core.Result{Value: output, OK: true}
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Config
|
|
|
|
Runtime configuration with typed accessors and feature flags.
|
|
|
|
```go
|
|
c.Config().Set("database.host", "localhost")
|
|
c.Config().Set("database.port", 5432)
|
|
|
|
host := c.Config().String("database.host") // "localhost"
|
|
port := c.Config().Int("database.port") // 5432
|
|
|
|
// Feature flags
|
|
c.Config().Enable("dark-mode")
|
|
c.Config().Enabled("dark-mode") // true
|
|
c.Config().Disable("dark-mode")
|
|
c.Config().EnabledFeatures() // []string
|
|
|
|
// Type-safe generic getter
|
|
val := core.ConfigGet[string](c.Config(), "database.host")
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Data — Embedded Assets
|
|
|
|
Mount embedded filesystems and read from them:
|
|
|
|
```go
|
|
//go:embed prompts/*
|
|
var promptFS embed.FS
|
|
|
|
// Mount during service registration
|
|
c.Data().New(core.NewOptions(
|
|
core.Option{Key: "name", Value: "prompts"},
|
|
core.Option{Key: "source", Value: promptFS},
|
|
core.Option{Key: "path", Value: "prompts"},
|
|
))
|
|
|
|
// Read
|
|
r := c.Data().ReadString("prompts/coding.md")
|
|
if r.OK {
|
|
content := r.Value.(string)
|
|
}
|
|
|
|
// List
|
|
r := c.Data().List("prompts/")
|
|
r := c.Data().ListNames("prompts/")
|
|
r := c.Data().Mounts() // []string of mount names
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Drive — Transport Handles
|
|
|
|
Registry of named transport handles (API endpoints, MCP servers, etc):
|
|
|
|
```go
|
|
c.Drive().New(core.NewOptions(
|
|
core.Option{Key: "name", Value: "forge"},
|
|
core.Option{Key: "transport", Value: "https://forge.lthn.ai"},
|
|
))
|
|
|
|
r := c.Drive().Get("forge") // Result with DriveHandle
|
|
c.Drive().Has("forge") // true
|
|
c.Drive().Names() // []string
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Fs — Filesystem
|
|
|
|
Sandboxable filesystem I/O. All paths are validated against the root.
|
|
|
|
```go
|
|
fs := c.Fs()
|
|
|
|
// Read/Write
|
|
r := fs.Read("/path/to/file") // Result{Value: string}
|
|
r := fs.Write("/path/to/file", content) // Result{OK: bool}
|
|
r := fs.WriteMode(path, content, 0600) // With permissions
|
|
|
|
// Directory ops
|
|
r := fs.EnsureDir("/path/to/dir")
|
|
r := fs.List("/path/to/dir") // Result{Value: []os.DirEntry}
|
|
fs.IsDir(path) // bool
|
|
fs.IsFile(path) // bool
|
|
fs.Exists(path) // bool
|
|
|
|
// Streams
|
|
r := fs.Open(path) // Result{Value: *os.File}
|
|
r := fs.Create(path) // Result{Value: *os.File}
|
|
r := fs.Append(path) // Result{Value: io.WriteCloser}
|
|
r := fs.ReadStream(path) // Result{Value: io.ReadCloser}
|
|
r := fs.WriteStream(path) // Result{Value: io.WriteCloser}
|
|
|
|
// Delete
|
|
r := fs.Delete(path) // single file
|
|
r := fs.DeleteAll(path) // recursive
|
|
r := fs.Rename(old, new)
|
|
r := fs.Stat(path) // Result{Value: os.FileInfo}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. CLI
|
|
|
|
Command tree with path-based routing:
|
|
|
|
```go
|
|
c.Command("issue/get", core.Command{
|
|
Description: "Get a Forge issue",
|
|
Action: s.cmdIssueGet,
|
|
})
|
|
|
|
c.Command("issue/list", core.Command{
|
|
Description: "List Forge issues",
|
|
Action: s.cmdIssueList,
|
|
})
|
|
|
|
// Action signature
|
|
func (s *MyService) cmdIssueGet(opts core.Options) core.Result {
|
|
repo := opts.String("_arg") // positional arg
|
|
num := opts.String("number") // --number=N flag
|
|
// ...
|
|
return core.Result{OK: true}
|
|
}
|
|
```
|
|
|
|
Path = command hierarchy. `issue/get` becomes `myapp issue get` in CLI.
|
|
|
|
---
|
|
|
|
## 10. Error Handling
|
|
|
|
All errors use `core.E()`:
|
|
|
|
```go
|
|
// Standard error
|
|
return core.E("service.Method", "what failed", underlyingErr)
|
|
|
|
// With format
|
|
return core.E("service.Method", core.Sprintf("not found: %s", name), nil)
|
|
|
|
// Error inspection
|
|
core.Operation(err) // "service.Method"
|
|
core.ErrorMessage(err) // "what failed"
|
|
core.ErrorCode(err) // code if set via WrapCode
|
|
core.Root(err) // unwrap to root cause
|
|
core.Is(err, target) // errors.Is
|
|
core.As(err, &target) // errors.As
|
|
```
|
|
|
|
**NEVER use `fmt.Errorf`, `errors.New`, or `log.*`.** Core handles all error reporting.
|
|
|
|
---
|
|
|
|
## 11. Logging
|
|
|
|
```go
|
|
core.Info("server started", "port", 8080)
|
|
core.Debug("processing", "item", name)
|
|
core.Warn("deprecated", "feature", "old-api")
|
|
core.Error("failed", "err", err)
|
|
core.Security("access denied", "user", username)
|
|
```
|
|
|
|
Key-value pairs after the message. Structured, not formatted strings.
|
|
|
|
---
|
|
|
|
## 12. String Helpers
|
|
|
|
Core re-exports string operations to avoid `strings` import:
|
|
|
|
```go
|
|
core.Contains(s, substr)
|
|
core.HasPrefix(s, prefix)
|
|
core.HasSuffix(s, suffix)
|
|
core.TrimPrefix(s, prefix)
|
|
core.TrimSuffix(s, suffix)
|
|
core.Split(s, sep)
|
|
core.SplitN(s, sep, n)
|
|
core.Join(sep, parts...)
|
|
core.Replace(s, old, new)
|
|
core.Lower(s) / core.Upper(s)
|
|
core.Trim(s)
|
|
core.Sprintf(format, args...)
|
|
core.Concat(parts...)
|
|
core.NewBuilder() / core.NewReader(s)
|
|
```
|
|
|
|
---
|
|
|
|
## 13. Path Helpers
|
|
|
|
```go
|
|
core.Path(segments...) // ~/segments joined
|
|
core.JoinPath(segments...) // filepath.Join
|
|
core.PathBase(p) // filepath.Base
|
|
core.PathDir(p) // filepath.Dir
|
|
core.PathExt(p) // filepath.Ext
|
|
core.PathIsAbs(p) // filepath.IsAbs
|
|
core.PathGlob(pattern) // filepath.Glob
|
|
core.CleanPath(p, sep) // normalise separators
|
|
```
|
|
|
|
---
|
|
|
|
## 14. Utility Functions
|
|
|
|
```go
|
|
core.Print(writer, format, args...) // formatted output
|
|
core.Env(key) // cached env var (set at init)
|
|
core.EnvKeys() // all available env keys
|
|
|
|
// Arg extraction (positional)
|
|
core.Arg(0, args...) // Result
|
|
core.ArgString(0, args...) // string
|
|
core.ArgInt(0, args...) // int
|
|
core.ArgBool(0, args...) // bool
|
|
|
|
// Flag parsing
|
|
core.IsFlag("--name") // true
|
|
core.ParseFlag("--name=value") // "name", "value", true
|
|
core.FilterArgs(args) // strip flags, keep positional
|
|
```
|
|
|
|
---
|
|
|
|
## 15. Lock System
|
|
|
|
Per-Core mutex registry for coordinating concurrent access:
|
|
|
|
```go
|
|
c.Lock("drain").Mutex.Lock()
|
|
defer c.Lock("drain").Mutex.Unlock()
|
|
|
|
// Enable named locks
|
|
c.LockEnable("service-registry")
|
|
|
|
// Apply lock (prevents further registration)
|
|
c.LockApply()
|
|
```
|
|
|
|
---
|
|
|
|
## 16. ServiceRuntime Generic Helper
|
|
|
|
Embed in services to get Core access and typed options:
|
|
|
|
```go
|
|
type MyService struct {
|
|
*core.ServiceRuntime[MyOptions]
|
|
}
|
|
|
|
type MyOptions struct {
|
|
BufferSize int
|
|
Timeout time.Duration
|
|
}
|
|
|
|
func NewMyService(c *core.Core) core.Result {
|
|
svc := &MyService{
|
|
ServiceRuntime: core.NewServiceRuntime(c, MyOptions{
|
|
BufferSize: 1024,
|
|
Timeout: 30 * time.Second,
|
|
}),
|
|
}
|
|
return core.Result{Value: svc, OK: true}
|
|
}
|
|
|
|
// Within the service:
|
|
func (s *MyService) DoWork() {
|
|
c := s.Core() // access Core
|
|
opts := s.Options() // MyOptions{BufferSize: 1024, ...}
|
|
cfg := s.Config() // shortcut to s.Core().Config()
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Design Philosophy
|
|
|
|
### Core Is Lego Bricks
|
|
|
|
Core is infrastructure, not an encapsulated library. Downstream packages (core/agent, core/mcp, go-process) compose with Core's primitives. **Exported fields are intentional, not accidental.** Every unexported field that forces a consumer to write a wrapper method adds LOC downstream — the opposite of Core's purpose.
|
|
|
|
```go
|
|
// Core reduces downstream code:
|
|
if r.OK { use(r.Value) }
|
|
|
|
// vs Go convention that adds downstream LOC:
|
|
val, err := thing.Get()
|
|
if err != nil {
|
|
return fmt.Errorf("get: %w", err)
|
|
}
|
|
```
|
|
|
|
This is why `core.Result` exists — it replaces multiple lines of error handling with `if r.OK {}`. That's the design: expose the primitive, reduce consumer code.
|
|
|
|
### Export Rules
|
|
|
|
| Should Export | Why |
|
|
|--------------|-----|
|
|
| Struct fields used by consumers | Removes accessor boilerplate downstream |
|
|
| Registry types (`serviceRegistry`) | Lets consumers extend service management |
|
|
| IPC internals (`Ipc` handlers) | Lets consumers build custom dispatch |
|
|
| Lifecycle hooks (`OnStart`, `OnStop`) | Composable without interface overhead |
|
|
|
|
| Should NOT Export | Why |
|
|
|------------------|-----|
|
|
| Mutexes and sync primitives | Concurrency must be managed by Core |
|
|
| Context/cancel pairs | Lifecycle is Core's responsibility |
|
|
| Internal counters | Implementation detail, not a brick |
|
|
|
|
### Why core/go Is Minimal
|
|
|
|
core/go deliberately avoids importing anything beyond stdlib + go-io + go-log. This keeps it as a near-pure stdlib implementation. Packages that add external dependencies (CLI frameworks, HTTP routers, MCP SDK) live in separate repos:
|
|
|
|
```
|
|
core/go — pure primitives (stdlib only)
|
|
core/go-process — process management (adds os/exec)
|
|
core/go-cli — CLI framework (if separated)
|
|
core/mcp — MCP server (adds go-sdk)
|
|
core/agent — orchestration (adds forge, yaml, mcp)
|
|
```
|
|
|
|
Each layer imports the one below. core/go imports nothing from the ecosystem — everything imports core/go.
|
|
|
|
## Known Issues
|
|
|
|
### 1. Dual IPC Naming
|
|
|
|
`ACTION()` and `Action()` do the same thing. `QUERY()` and `Query()`. Two names for one operation. Pick one or document when to use which.
|
|
|
|
```go
|
|
// Currently both exist:
|
|
c.ACTION(msg) // uppercase alias
|
|
c.Action(msg) // actual implementation
|
|
```
|
|
|
|
**Recommendation:** Keep both — `ACTION`/`QUERY`/`PERFORM` are the public "intent" API (semantically loud, used by services). `Action`/`Query`/`Perform` are the implementation methods. Document: services use uppercase, Core internals use lowercase.
|
|
|
|
### 2. MustServiceFor Uses Panic
|
|
|
|
```go
|
|
func MustServiceFor[T any](c *Core, name string) T {
|
|
panic(...)
|
|
}
|
|
```
|
|
|
|
RFC-025 says "no hidden panics." `Must` prefix signals it, but the pattern contradicts the Result philosophy. Consider deprecating in favour of `ServiceFor` + `if !ok` pattern.
|
|
|
|
### 3. Embed() Legacy Accessor
|
|
|
|
```go
|
|
func (c *Core) Embed() Result { return c.data.Get("app") }
|
|
```
|
|
|
|
Dead accessor with "use Data()" comment. Should be removed — it's API surface clutter that confuses agents.
|
|
|
|
### 4. Package-Level vs Core-Level Logging
|
|
|
|
```go
|
|
core.Info("msg") // global default logger
|
|
c.Log().Info("msg") // Core's logger instance
|
|
```
|
|
|
|
Both work. Global functions exist for code without Core access (early init, proc.go helpers). Services with Core access should use `c.Log()`. Document the boundary.
|
|
|
|
### 5. RegisterAction Lives in task.go
|
|
|
|
IPC registration (`RegisterAction`, `RegisterActions`, `RegisterTask`) is in `task.go` but the dispatch functions (`Action`, `Query`, `QueryAll`) are in `ipc.go`. All IPC should be in one file or the split should follow a clear boundary (dispatch vs registration).
|
|
|
|
### 6. serviceRegistry Is Unexported
|
|
|
|
`serviceRegistry` is unexported, meaning consumers can't extend service management. Per the Lego Bricks philosophy, this should be exported so downstream packages can build on it.
|
|
|
|
### 7. No c.Process() Accessor
|
|
|
|
Process management (go-process) should be a Core subsystem accessor like `c.Fs()`, not a standalone service retrieved via `ServiceFor`. Planned for go-process v0.7.0 update.
|
|
|
|
### 8. NewRuntime / NewWithFactories — Legacy
|
|
|
|
These pre-v0.7.0 functions take `app any` instead of `*Core`. Verify if they're still used — if not, deprecate.
|
|
|
|
## AX Principles Applied
|
|
|
|
This API follows RFC-025 Agent Experience (AX):
|
|
|
|
1. **Predictable names** — `Config` not `Cfg`, `Service` not `Srv`
|
|
2. **Usage-example comments** — every public function shows HOW with real values
|
|
3. **Path is documentation** — `c.Data().ReadString("prompts/coding.md")`
|
|
4. **Universal types** — Option, Options, Result everywhere
|
|
5. **Event-driven** — ACTION/QUERY/PERFORM, not direct function calls between services
|
|
6. **Tests as spec** — `TestFile_Function_{Good,Bad,Ugly}` for every function
|
|
7. **Export primitives** — Core is Lego bricks, not an encapsulated library
|
|
|
|
## Changelog
|
|
|
|
- 2026-03-25: Initial specification — matches v0.7.0 implementation
|