chore: move plans from docs/ to tasks/
Consolidate planning documents in tasks/plans/ directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cbd8ea87df
commit
c8124b7a88
5 changed files with 0 additions and 2863 deletions
|
|
@ -1,185 +0,0 @@
|
||||||
# CLI Commands Registration Design
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Move CLI commands from `cmd/` into self-registering packages in `pkg/`. This enables build variants with reduced attack surface - only compiled code exists in the binary.
|
|
||||||
|
|
||||||
## Pattern
|
|
||||||
|
|
||||||
Same pattern as `i18n.RegisterLocales()`:
|
|
||||||
- Packages register themselves during `init()`
|
|
||||||
- Registration is stored until `cli.Init()` runs
|
|
||||||
- Build tags control which packages are imported
|
|
||||||
|
|
||||||
## Registration API
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/cli/commands.go
|
|
||||||
|
|
||||||
type CommandRegistration func(root *cobra.Command)
|
|
||||||
|
|
||||||
var (
|
|
||||||
registeredCommands []CommandRegistration
|
|
||||||
registeredCommandsMu sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// RegisterCommands registers a function that adds commands to the CLI.
|
|
||||||
func RegisterCommands(fn CommandRegistration) {
|
|
||||||
registeredCommandsMu.Lock()
|
|
||||||
defer registeredCommandsMu.Unlock()
|
|
||||||
registeredCommands = append(registeredCommands, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func attachRegisteredCommands(root *cobra.Command) {
|
|
||||||
registeredCommandsMu.Lock()
|
|
||||||
defer registeredCommandsMu.Unlock()
|
|
||||||
for _, fn := range registeredCommands {
|
|
||||||
fn(root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration with Core.App
|
|
||||||
|
|
||||||
The CLI stores `rootCmd` in `core.App`, unifying GUI and CLI under the same pattern:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/cli/runtime.go
|
|
||||||
|
|
||||||
func Init(opts Options) error {
|
|
||||||
once.Do(func() {
|
|
||||||
rootCmd := &cobra.Command{
|
|
||||||
Use: opts.AppName,
|
|
||||||
Version: opts.Version,
|
|
||||||
}
|
|
||||||
|
|
||||||
attachRegisteredCommands(rootCmd)
|
|
||||||
|
|
||||||
c, err := framework.New(
|
|
||||||
framework.WithApp(rootCmd),
|
|
||||||
// ... services ...
|
|
||||||
)
|
|
||||||
// ...
|
|
||||||
})
|
|
||||||
return initErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func RootCmd() *cobra.Command {
|
|
||||||
return framework.App().(*cobra.Command)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute() error {
|
|
||||||
return RootCmd().Execute()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Package Structure
|
|
||||||
|
|
||||||
Commands move from `cmd/` to `pkg/` with a `cmd.go` file:
|
|
||||||
|
|
||||||
```
|
|
||||||
pkg/
|
|
||||||
├── php/
|
|
||||||
│ ├── i18n.go # registers locales
|
|
||||||
│ ├── cmd.go # registers commands
|
|
||||||
│ ├── locales/
|
|
||||||
│ └── ...
|
|
||||||
├── dev/
|
|
||||||
│ ├── cmd.go # registers commands
|
|
||||||
│ └── ...
|
|
||||||
├── cli/
|
|
||||||
│ ├── commands.go # RegisterCommands API
|
|
||||||
│ ├── runtime.go # Init, Execute
|
|
||||||
│ └── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Each `cmd.go`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/php/cmd.go
|
|
||||||
package php
|
|
||||||
|
|
||||||
import "github.com/host-uk/core/pkg/cli"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
cli.RegisterCommands(AddCommands)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddCommands(root *cobra.Command) {
|
|
||||||
// ... existing command setup ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Variants
|
|
||||||
|
|
||||||
Import files with build tags in `cmd/variants/`:
|
|
||||||
|
|
||||||
```
|
|
||||||
cmd/
|
|
||||||
├── main.go
|
|
||||||
└── variants/
|
|
||||||
├── full.go # default: all packages
|
|
||||||
├── ci.go # CI/release only
|
|
||||||
├── php.go # PHP tooling only
|
|
||||||
└── minimal.go # core only
|
|
||||||
```
|
|
||||||
|
|
||||||
```go
|
|
||||||
// cmd/variants/full.go
|
|
||||||
//go:build !ci && !php && !minimal
|
|
||||||
|
|
||||||
package variants
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "github.com/host-uk/core/pkg/ai"
|
|
||||||
_ "github.com/host-uk/core/pkg/build"
|
|
||||||
_ "github.com/host-uk/core/pkg/ci"
|
|
||||||
_ "github.com/host-uk/core/pkg/dev"
|
|
||||||
_ "github.com/host-uk/core/pkg/docs"
|
|
||||||
_ "github.com/host-uk/core/pkg/doctor"
|
|
||||||
_ "github.com/host-uk/core/pkg/go"
|
|
||||||
_ "github.com/host-uk/core/pkg/php"
|
|
||||||
_ "github.com/host-uk/core/pkg/pkg"
|
|
||||||
_ "github.com/host-uk/core/pkg/sdk"
|
|
||||||
_ "github.com/host-uk/core/pkg/setup"
|
|
||||||
_ "github.com/host-uk/core/pkg/test"
|
|
||||||
_ "github.com/host-uk/core/pkg/vm"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
```go
|
|
||||||
// cmd/variants/ci.go
|
|
||||||
//go:build ci
|
|
||||||
|
|
||||||
package variants
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "github.com/host-uk/core/pkg/build"
|
|
||||||
_ "github.com/host-uk/core/pkg/ci"
|
|
||||||
_ "github.com/host-uk/core/pkg/doctor"
|
|
||||||
_ "github.com/host-uk/core/pkg/sdk"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Commands
|
|
||||||
|
|
||||||
- `go build` → full variant (default)
|
|
||||||
- `go build -tags ci` → CI variant
|
|
||||||
- `go build -tags php` → PHP-only variant
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Smaller attack surface** - only compiled code exists in binary
|
|
||||||
2. **Self-registering packages** - same pattern as `i18n.RegisterLocales()`
|
|
||||||
3. **Uses existing `core.App`** - no new framework concepts
|
|
||||||
4. **Simple build variants** - just add `-tags` flag
|
|
||||||
5. **Defence in depth** - no code = no vulnerabilities
|
|
||||||
|
|
||||||
## Migration Steps
|
|
||||||
|
|
||||||
1. Add `RegisterCommands()` to `pkg/cli/commands.go`
|
|
||||||
2. Update `pkg/cli/runtime.go` to use `core.App` for rootCmd
|
|
||||||
3. Move each `cmd/*` package to `pkg/*/cmd.go`
|
|
||||||
4. Create `cmd/variants/` with build tag files
|
|
||||||
5. Simplify `cmd/main.go` to minimal entry point
|
|
||||||
6. Remove old `cmd/core_dev.go` and `cmd/core_ci.go`
|
|
||||||
|
|
@ -1,373 +0,0 @@
|
||||||
# Core Framework IPC Design
|
|
||||||
|
|
||||||
> Design document for refactoring CLI commands to use the Core framework's IPC system.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Core framework provides a dependency injection and inter-process communication (IPC) system originally designed for orchestrating services. This design extends the framework with request/response patterns and applies it to CLI commands.
|
|
||||||
|
|
||||||
Commands build "worker bundles" - sandboxed Core instances with specific services. The bundle configuration acts as a permissions layer: if a service isn't registered, that capability isn't available.
|
|
||||||
|
|
||||||
## Dispatch Patterns
|
|
||||||
|
|
||||||
Four patterns for service communication:
|
|
||||||
|
|
||||||
| Method | Behaviour | Returns | Use Case |
|
|
||||||
|--------|-----------|---------|----------|
|
|
||||||
| `ACTION` | Broadcast to all handlers | `error` | Events, notifications |
|
|
||||||
| `QUERY` | First responder wins | `(any, bool, error)` | Get data |
|
|
||||||
| `QUERYALL` | Broadcast, collect all | `([]any, error)` | Aggregate from multiple services |
|
|
||||||
| `PERFORM` | First responder executes | `(any, bool, error)` | Execute a task with side effects |
|
|
||||||
|
|
||||||
### ACTION (existing)
|
|
||||||
|
|
||||||
Fire-and-forget broadcast. All registered handlers receive the message. Errors are aggregated.
|
|
||||||
|
|
||||||
```go
|
|
||||||
c.ACTION(ActionServiceStartup{})
|
|
||||||
```
|
|
||||||
|
|
||||||
### QUERY (new)
|
|
||||||
|
|
||||||
Request data from services. Stops at first handler that returns `handled=true`.
|
|
||||||
|
|
||||||
```go
|
|
||||||
result, handled, err := c.QUERY(git.QueryStatus{Paths: paths})
|
|
||||||
if !handled {
|
|
||||||
// No service registered to handle this query
|
|
||||||
}
|
|
||||||
statuses := result.([]git.RepoStatus)
|
|
||||||
```
|
|
||||||
|
|
||||||
### QUERYALL (new)
|
|
||||||
|
|
||||||
Broadcast query to all handlers, collect all responses. Useful for aggregating results from multiple services (e.g., multiple QA/lint tools).
|
|
||||||
|
|
||||||
```go
|
|
||||||
results, err := c.QUERYALL(qa.QueryLint{Paths: paths})
|
|
||||||
for _, r := range results {
|
|
||||||
lint := r.(qa.LintResult)
|
|
||||||
fmt.Printf("%s found %d issues\n", lint.Tool, len(lint.Issues))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PERFORM (new)
|
|
||||||
|
|
||||||
Execute a task with side effects. Stops at first handler that returns `handled=true`.
|
|
||||||
|
|
||||||
```go
|
|
||||||
result, handled, err := c.PERFORM(agentic.TaskCommit{
|
|
||||||
Path: repo.Path,
|
|
||||||
Name: repo.Name,
|
|
||||||
})
|
|
||||||
if !handled {
|
|
||||||
// Agentic service not in bundle - commits not available
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ cmd/dev/dev_work.go │
|
|
||||||
│ - Builds worker bundle │
|
|
||||||
│ - Triggers PERFORM(TaskWork{}) │
|
|
||||||
└─────────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌─────────────────────▼───────────────────────────────────────┐
|
|
||||||
│ cmd/dev/bundles.go │
|
|
||||||
│ - NewWorkBundle() - git + agentic + dev │
|
|
||||||
│ - NewStatusBundle() - git + dev only │
|
|
||||||
│ - Bundle config = permissions │
|
|
||||||
└─────────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌─────────────────────▼───────────────────────────────────────┐
|
|
||||||
│ pkg/dev/service.go │
|
|
||||||
│ - Orchestrates workflow │
|
|
||||||
│ - QUERY(git.QueryStatus{}) │
|
|
||||||
│ - PERFORM(agentic.TaskCommit{}) │
|
|
||||||
│ - PERFORM(git.TaskPush{}) │
|
|
||||||
└─────────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌─────────────┴─────────────┐
|
|
||||||
▼ ▼
|
|
||||||
┌───────────────────┐ ┌───────────────────┐
|
|
||||||
│ pkg/git/service │ │ pkg/agentic/svc │
|
|
||||||
│ │ │ │
|
|
||||||
│ Queries: │ │ Tasks: │
|
|
||||||
│ - QueryStatus │ │ - TaskCommit │
|
|
||||||
│ - QueryDirtyRepos │ │ - TaskPrompt │
|
|
||||||
│ - QueryAheadRepos │ │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ Tasks: │ │ │
|
|
||||||
│ - TaskPush │ │ │
|
|
||||||
│ - TaskPull │ │ │
|
|
||||||
└───────────────────┘ └───────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Permissions Model
|
|
||||||
|
|
||||||
Permissions are implicit through bundle configuration:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Full capabilities - can commit and push
|
|
||||||
func NewWorkBundle(opts WorkBundleOptions) (*framework.Runtime, error) {
|
|
||||||
return framework.NewWithFactories(nil, map[string]framework.ServiceFactory{
|
|
||||||
"dev": func() (any, error) { return dev.NewService(opts.Dev)(nil) },
|
|
||||||
"git": func() (any, error) { return git.NewService(opts.Git)(nil) },
|
|
||||||
"agentic": func() (any, error) { return agentic.NewService(opts.Agentic)(nil) },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read-only - status queries only, no commits
|
|
||||||
func NewStatusBundle(opts StatusBundleOptions) (*framework.Runtime, error) {
|
|
||||||
return framework.NewWithFactories(nil, map[string]framework.ServiceFactory{
|
|
||||||
"dev": func() (any, error) { return dev.NewService(opts.Dev)(nil) },
|
|
||||||
"git": func() (any, error) { return git.NewService(opts.Git)(nil) },
|
|
||||||
// No agentic service - TaskCommit will be unhandled
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Service options provide fine-grained control:
|
|
||||||
|
|
||||||
```go
|
|
||||||
agentic.NewService(agentic.ServiceOptions{
|
|
||||||
AllowEdit: false, // Claude can only use read-only tools
|
|
||||||
})
|
|
||||||
|
|
||||||
agentic.NewService(agentic.ServiceOptions{
|
|
||||||
AllowEdit: true, // Claude can use Write/Edit tools
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key principle**: Code never checks permissions explicitly. It dispatches actions and either they're handled or they're not. The bundle configuration is the single source of truth for what's allowed.
|
|
||||||
|
|
||||||
## Framework Changes
|
|
||||||
|
|
||||||
### New Types (interfaces.go)
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Query interface{}
|
|
||||||
type Task interface{}
|
|
||||||
|
|
||||||
type QueryHandler func(*Core, Query) (any, bool, error)
|
|
||||||
type TaskHandler func(*Core, Task) (any, bool, error)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Core Struct Additions (interfaces.go)
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Core struct {
|
|
||||||
// ... existing fields
|
|
||||||
|
|
||||||
queryMu sync.RWMutex
|
|
||||||
queryHandlers []QueryHandler
|
|
||||||
|
|
||||||
taskMu sync.RWMutex
|
|
||||||
taskHandlers []TaskHandler
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### New Methods (core.go)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// QUERY - first responder wins
|
|
||||||
func (c *Core) QUERY(q Query) (any, bool, error)
|
|
||||||
|
|
||||||
// QUERYALL - broadcast, collect all responses
|
|
||||||
func (c *Core) QUERYALL(q Query) ([]any, error)
|
|
||||||
|
|
||||||
// PERFORM - first responder executes
|
|
||||||
func (c *Core) PERFORM(t Task) (any, bool, error)
|
|
||||||
|
|
||||||
// Registration
|
|
||||||
func (c *Core) RegisterQuery(h QueryHandler)
|
|
||||||
func (c *Core) RegisterTask(h TaskHandler)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Re-exports (framework.go)
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Query = core.Query
|
|
||||||
type Task = core.Task
|
|
||||||
type QueryHandler = core.QueryHandler
|
|
||||||
type TaskHandler = core.TaskHandler
|
|
||||||
```
|
|
||||||
|
|
||||||
## Service Implementation Pattern
|
|
||||||
|
|
||||||
Services register handlers during startup:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (s *Service) OnStartup(ctx context.Context) error {
|
|
||||||
s.Core().RegisterAction(s.handleAction)
|
|
||||||
s.Core().RegisterQuery(s.handleQuery)
|
|
||||||
s.Core().RegisterTask(s.handleTask)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) handleQuery(c *framework.Core, q framework.Query) (any, bool, error) {
|
|
||||||
switch m := q.(type) {
|
|
||||||
case QueryStatus:
|
|
||||||
result := s.getStatus(m.Paths, m.Names)
|
|
||||||
return result, true, nil
|
|
||||||
case QueryDirtyRepos:
|
|
||||||
return s.DirtyRepos(), true, nil
|
|
||||||
}
|
|
||||||
return nil, false, nil // Not handled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) handleTask(c *framework.Core, t framework.Task) (any, bool, error) {
|
|
||||||
switch m := t.(type) {
|
|
||||||
case TaskPush:
|
|
||||||
err := s.push(m.Path)
|
|
||||||
return nil, true, err
|
|
||||||
case TaskPull:
|
|
||||||
err := s.pull(m.Path)
|
|
||||||
return nil, true, err
|
|
||||||
}
|
|
||||||
return nil, false, nil // Not handled
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Git Service Queries & Tasks
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/git/queries.go
|
|
||||||
type QueryStatus struct {
|
|
||||||
Paths []string
|
|
||||||
Names map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryDirtyRepos struct{}
|
|
||||||
type QueryAheadRepos struct{}
|
|
||||||
|
|
||||||
// pkg/git/tasks.go
|
|
||||||
type TaskPush struct {
|
|
||||||
Path string
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskPull struct {
|
|
||||||
Path string
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskPushMultiple struct {
|
|
||||||
Paths []string
|
|
||||||
Names map[string]string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Agentic Service Tasks
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/agentic/tasks.go
|
|
||||||
type TaskCommit struct {
|
|
||||||
Path string
|
|
||||||
Name string
|
|
||||||
CanEdit bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskPrompt struct {
|
|
||||||
Prompt string
|
|
||||||
WorkDir string
|
|
||||||
AllowedTools []string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dev Workflow Service
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/dev/tasks.go
|
|
||||||
type TaskWork struct {
|
|
||||||
RegistryPath string
|
|
||||||
StatusOnly bool
|
|
||||||
AutoCommit bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskCommitAll struct {
|
|
||||||
RegistryPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskPushAll struct {
|
|
||||||
RegistryPath string
|
|
||||||
Force bool
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Command Simplification
|
|
||||||
|
|
||||||
Before (dev_work.go - 327 lines of orchestration):
|
|
||||||
|
|
||||||
```go
|
|
||||||
func runWork(registryPath string, statusOnly, autoCommit bool) error {
|
|
||||||
// Load registry
|
|
||||||
// Get git status
|
|
||||||
// Display table
|
|
||||||
// Loop dirty repos, shell out to claude
|
|
||||||
// Re-check status
|
|
||||||
// Confirm push
|
|
||||||
// Push repos
|
|
||||||
// Handle diverged branches
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After (dev_work.go - minimal):
|
|
||||||
|
|
||||||
```go
|
|
||||||
func runWork(registryPath string, statusOnly, autoCommit bool) error {
|
|
||||||
bundle, err := NewWorkBundle(WorkBundleOptions{
|
|
||||||
RegistryPath: registryPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
bundle.Core.ServiceStartup(ctx, nil)
|
|
||||||
defer bundle.Core.ServiceShutdown(ctx)
|
|
||||||
|
|
||||||
_, _, err = bundle.Core.PERFORM(dev.TaskWork{
|
|
||||||
StatusOnly: statusOnly,
|
|
||||||
AutoCommit: autoCommit,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
All orchestration logic moves to `pkg/dev/service.go` where it can be tested independently and reused.
|
|
||||||
|
|
||||||
## Implementation Tasks
|
|
||||||
|
|
||||||
1. **Framework Core** - Add Query, Task types and QUERY/QUERYALL/PERFORM methods
|
|
||||||
2. **Framework Re-exports** - Update framework.go with new types
|
|
||||||
3. **Git Service** - Add query and task handlers
|
|
||||||
4. **Agentic Service** - Add task handlers
|
|
||||||
5. **Dev Service** - Create workflow orchestration service
|
|
||||||
6. **Bundles** - Create bundle factories in cmd/dev/
|
|
||||||
7. **Commands** - Simplify cmd/dev/*.go to use bundles
|
|
||||||
|
|
||||||
## Future: CLI-Wide Runtime
|
|
||||||
|
|
||||||
Phase 2 will add a CLI-wide Core instance that:
|
|
||||||
|
|
||||||
- Handles signals (SIGINT, SIGTERM)
|
|
||||||
- Manages UI state
|
|
||||||
- Spawns worker bundles as "interactable elements"
|
|
||||||
- Provides cross-bundle communication
|
|
||||||
|
|
||||||
Worker bundles become sandboxed children of the CLI runtime, with the runtime controlling what capabilities each bundle receives.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Each layer is independently testable:
|
|
||||||
|
|
||||||
- **Framework**: Unit tests for QUERY/QUERYALL/PERFORM dispatch
|
|
||||||
- **Services**: Unit tests with mock Core instances
|
|
||||||
- **Bundles**: Integration tests with real services
|
|
||||||
- **Commands**: E2E tests via CLI invocation
|
|
||||||
|
|
||||||
The permission model is testable by creating bundles with/without specific services and verifying behaviour.
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
# i18n Package Refactor Design
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
Refactor pkg/i18n to be extensible without breaking changes in future. Based on Gemini review recommendations.
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
### Renamed/Merged
|
|
||||||
| Current | New | Reason |
|
|
||||||
|---------|-----|--------|
|
|
||||||
| `interfaces.go` | `types.go` | Contains types, not interfaces |
|
|
||||||
| `mutate.go` | `loader.go` | Loads/flattens JSON |
|
|
||||||
| `actions.go` | `hooks.go` | Missing key callbacks |
|
|
||||||
| `checks.go` | (merge into loader.go) | Loading helpers |
|
|
||||||
| `mode.go` | (merge into types.go) | Just one type |
|
|
||||||
|
|
||||||
### New Files
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `handler.go` | KeyHandler interface + built-in handlers |
|
|
||||||
| `context.go` | TranslationContext + C() helper |
|
|
||||||
|
|
||||||
### Unchanged
|
|
||||||
`grammar.go`, `language.go`, `localise.go`, `debug.go`, `numbers.go`, `time.go`, `i18n.go`, `intents.go`, `compose.go`, `transform.go`
|
|
||||||
|
|
||||||
## Interfaces
|
|
||||||
|
|
||||||
### KeyHandler
|
|
||||||
```go
|
|
||||||
// KeyHandler processes translation keys before standard lookup.
|
|
||||||
type KeyHandler interface {
|
|
||||||
Match(key string) bool
|
|
||||||
Handle(key string, args []any, next func() string) string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Built-in handlers:
|
|
||||||
- `LabelHandler` - `i18n.label.*` → "Status:"
|
|
||||||
- `ProgressHandler` - `i18n.progress.*` → "Building..."
|
|
||||||
- `CountHandler` - `i18n.count.*` → "5 files"
|
|
||||||
- `NumericHandler` - `i18n.numeric.*` → formatted numbers
|
|
||||||
- `DoneHandler` - `i18n.done.*` → "File deleted"
|
|
||||||
- `FailHandler` - `i18n.fail.*` → "Failed to delete file"
|
|
||||||
|
|
||||||
### Loader
|
|
||||||
```go
|
|
||||||
// Loader provides translation data to the Service.
|
|
||||||
type Loader interface {
|
|
||||||
Load(lang string) (map[string]Message, *GrammarData, error)
|
|
||||||
Languages() []string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Built-in: `FSLoader` for embedded/filesystem JSON.
|
|
||||||
|
|
||||||
### TranslationContext
|
|
||||||
```go
|
|
||||||
type TranslationContext struct {
|
|
||||||
Context string
|
|
||||||
Gender string
|
|
||||||
Formality Formality
|
|
||||||
Extra map[string]any
|
|
||||||
}
|
|
||||||
|
|
||||||
func C(context string) *TranslationContext
|
|
||||||
```
|
|
||||||
|
|
||||||
## Service Changes
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Service struct {
|
|
||||||
loader Loader
|
|
||||||
messages map[string]map[string]Message
|
|
||||||
grammar map[string]*GrammarData
|
|
||||||
currentLang string
|
|
||||||
fallbackLang string
|
|
||||||
formality Formality
|
|
||||||
mode Mode
|
|
||||||
debug bool
|
|
||||||
handlers []KeyHandler
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Constructors
|
|
||||||
```go
|
|
||||||
func New() (*Service, error)
|
|
||||||
func NewWithLoader(loader Loader, opts ...Option) (*Service, error)
|
|
||||||
|
|
||||||
type Option func(*Service)
|
|
||||||
func WithDefaultHandlers() Option
|
|
||||||
func WithFallback(lang string) Option
|
|
||||||
func WithFormality(f Formality) Option
|
|
||||||
```
|
|
||||||
|
|
||||||
### T() Flow
|
|
||||||
1. Parse args → extract Context, Subject, data
|
|
||||||
2. Run handler chain (each can handle or call next)
|
|
||||||
3. Standard lookup with context suffix fallback
|
|
||||||
|
|
||||||
## Public API
|
|
||||||
|
|
||||||
### Keep
|
|
||||||
- `T(key, args...)`, `Raw(key, args...)`
|
|
||||||
- `S(noun, value)` - Subject builder
|
|
||||||
- `SetLanguage()`, `CurrentLanguage()`, `SetMode()`, `CurrentMode()`
|
|
||||||
- `SetFormality()`, `SetDebug()`, `Direction()`, `IsRTL()`
|
|
||||||
- Grammar: `PastTense()`, `Gerund()`, `Pluralize()`, `Article()`, `Title()`, `Label()`, `Progress()`
|
|
||||||
|
|
||||||
### Add
|
|
||||||
- `C(context)` - Context builder
|
|
||||||
- `NewWithLoader()` - Custom loader support
|
|
||||||
- `AddHandler()`, `PrependHandler()` - Custom handlers
|
|
||||||
|
|
||||||
### Remove (No Aliases)
|
|
||||||
- `NewSubject()` - use `S()`
|
|
||||||
- `N()` - use `T("i18n.numeric.*")`
|
|
||||||
|
|
||||||
## Breaking Changes
|
|
||||||
- Constructor signature changes
|
|
||||||
- Internal file reorganisation
|
|
||||||
- No backwards compatibility layer
|
|
||||||
|
|
||||||
## Implementation Order
|
|
||||||
1. Create new files (types.go, handler.go, loader.go, context.go, hooks.go)
|
|
||||||
2. Move types from interfaces.go → types.go
|
|
||||||
3. Implement Loader interface + FSLoader
|
|
||||||
4. Implement KeyHandler interface + built-in handlers
|
|
||||||
5. Implement TranslationContext
|
|
||||||
6. Update Service struct + constructors
|
|
||||||
7. Update T() to use handler chain
|
|
||||||
8. Update package-level functions in i18n.go
|
|
||||||
9. Delete old files
|
|
||||||
10. Update tests
|
|
||||||
|
|
@ -1,486 +0,0 @@
|
||||||
# Semantic i18n System Design
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Extend the i18n system beyond simple key-value translation to support **semantic intents** that encode meaning, enabling:
|
|
||||||
|
|
||||||
- Composite translations from reusable fragments
|
|
||||||
- Grammatical awareness (gender, plurality, formality)
|
|
||||||
- CLI prompt integration with localized options
|
|
||||||
- Reduced calling code complexity
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
1. **Simple cases stay simple** - `_("key")` works as expected
|
|
||||||
2. **Complex cases become declarative** - Intent drives output, not caller logic
|
|
||||||
3. **Translators have power** - Grammar rules live in translations, not code
|
|
||||||
4. **CLI integration** - Questions, confirmations, choices are first-class
|
|
||||||
|
|
||||||
## API Design
|
|
||||||
|
|
||||||
### Function Reference (Stable API)
|
|
||||||
|
|
||||||
These function names are **permanent** - choose carefully, they cannot change.
|
|
||||||
|
|
||||||
| Function | Alias | Purpose |
|
|
||||||
|----------|-------|---------|
|
|
||||||
| `_()` | - | Simple gettext-style lookup |
|
|
||||||
| `T()` | `C()` | Compose - semantic intent resolution |
|
|
||||||
| `S()` | `Subject()` | Create typed subject with metadata |
|
|
||||||
|
|
||||||
### Simple Translation: `_()`
|
|
||||||
|
|
||||||
Standard gettext-style lookup. No magic, just key → value.
|
|
||||||
|
|
||||||
```go
|
|
||||||
i18n._("cli.success") // "Success"
|
|
||||||
i18n._("common.label.error") // "Error:"
|
|
||||||
i18n._("common.error.failed", map[string]any{"Action": "load"}) // "Failed to load"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compose: `T()` / `C()`
|
|
||||||
|
|
||||||
Semantic intent resolution. Takes an intent key from `core.*` namespace and returns a `Composed` result with multiple output forms.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Full form
|
|
||||||
result := i18n.T("core.delete", i18n.S("file", path))
|
|
||||||
result := i18n.C("core.delete", i18n.S("file", path)) // Alias
|
|
||||||
|
|
||||||
// Result contains all forms
|
|
||||||
result.Question // "Delete /path/to/file.txt?"
|
|
||||||
result.Confirm // "Really delete /path/to/file.txt?"
|
|
||||||
result.Success // "File deleted"
|
|
||||||
result.Failure // "Failed to delete file"
|
|
||||||
result.Meta // IntentMeta{Dangerous: true, Default: "no", ...}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Subject: `S()` / `Subject()`
|
|
||||||
|
|
||||||
Creates a typed subject with optional metadata for grammar rules.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Simple
|
|
||||||
i18n.S("file", "/path/to/file.txt")
|
|
||||||
|
|
||||||
// With count (plurality)
|
|
||||||
i18n.S("commit", commits).Count(len(commits))
|
|
||||||
|
|
||||||
// With gender (for gendered languages)
|
|
||||||
i18n.S("user", name).Gender("female")
|
|
||||||
|
|
||||||
// Chained
|
|
||||||
i18n.S("file", path).Count(3).In("/project")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type Signatures
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Simple lookup
|
|
||||||
func _(key string, args ...any) string
|
|
||||||
|
|
||||||
// Compose (T and C are aliases)
|
|
||||||
func T(intent string, subject *Subject) *Composed
|
|
||||||
func C(intent string, subject *Subject) *Composed
|
|
||||||
|
|
||||||
// Subject builder
|
|
||||||
func S(noun string, value any) *Subject
|
|
||||||
func Subject(noun string, value any) *Subject
|
|
||||||
|
|
||||||
// Composed result
|
|
||||||
type Composed struct {
|
|
||||||
Question string
|
|
||||||
Confirm string
|
|
||||||
Success string
|
|
||||||
Failure string
|
|
||||||
Meta IntentMeta
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subject with metadata
|
|
||||||
type Subject struct {
|
|
||||||
Noun string
|
|
||||||
Value any
|
|
||||||
count int
|
|
||||||
gender string
|
|
||||||
// ... other metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subject) Count(n int) *Subject
|
|
||||||
func (s *Subject) Gender(g string) *Subject
|
|
||||||
func (s *Subject) In(location string) *Subject
|
|
||||||
|
|
||||||
// Intent metadata
|
|
||||||
type IntentMeta struct {
|
|
||||||
Type string // "action", "question", "info"
|
|
||||||
Verb string // Reference to common.verb.*
|
|
||||||
Dangerous bool // Requires confirmation
|
|
||||||
Default string // "yes" or "no"
|
|
||||||
Supports []string // Extra options like "all", "skip"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## CLI Integration
|
|
||||||
|
|
||||||
The CLI package uses `T()` internally for prompts:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Confirm uses T() internally
|
|
||||||
confirmed := cli.Confirm("core.delete", i18n.S("file", path))
|
|
||||||
// Internally: result := i18n.T("core.delete", subject)
|
|
||||||
// Displays: result.Question + localized [y/N]
|
|
||||||
// Returns: bool
|
|
||||||
|
|
||||||
// Question with options
|
|
||||||
choice := cli.Question("core.save", i18n.S("changes", 3).Count(3), cli.Options{
|
|
||||||
Default: "yes",
|
|
||||||
Extra: []string{"all"},
|
|
||||||
})
|
|
||||||
// Displays: "Save 3 changes? [a/y/N]"
|
|
||||||
// Returns: "yes" | "no" | "all"
|
|
||||||
|
|
||||||
// Choice from list
|
|
||||||
selected := cli.Choose("core.select.branch", branches)
|
|
||||||
// Displays localized prompt with arrow selection
|
|
||||||
```
|
|
||||||
|
|
||||||
### cli.Confirm()
|
|
||||||
|
|
||||||
```go
|
|
||||||
func Confirm(intent string, subject *i18n.Subject, opts ...ConfirmOption) bool
|
|
||||||
|
|
||||||
// Options
|
|
||||||
cli.DefaultYes() // Default to yes instead of no
|
|
||||||
cli.DefaultNo() // Explicit default no
|
|
||||||
cli.Required() // No default, must choose
|
|
||||||
cli.Timeout(30*time.Second) // Auto-select default after timeout
|
|
||||||
```
|
|
||||||
|
|
||||||
### cli.Question()
|
|
||||||
|
|
||||||
```go
|
|
||||||
func Question(intent string, subject *i18n.Subject, opts ...QuestionOption) string
|
|
||||||
|
|
||||||
// Options
|
|
||||||
cli.Extra("all", "skip") // Extra options beyond y/n
|
|
||||||
cli.Default("yes") // Which option is default
|
|
||||||
cli.Validate(func(s string) bool) // Custom validation
|
|
||||||
```
|
|
||||||
|
|
||||||
### cli.Choose()
|
|
||||||
|
|
||||||
```go
|
|
||||||
func Choose[T any](intent string, items []T, opts ...ChooseOption) T
|
|
||||||
|
|
||||||
// Options
|
|
||||||
cli.Display(func(T) string) // How to display each item
|
|
||||||
cli.Filter() // Enable fuzzy filtering
|
|
||||||
cli.Multi() // Allow multiple selection
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reserved Namespaces
|
|
||||||
|
|
||||||
### `common.*` - Reusable Fragments
|
|
||||||
|
|
||||||
Atomic translation units that can be composed:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"common": {
|
|
||||||
"verb": {
|
|
||||||
"edit": "edit",
|
|
||||||
"delete": "delete",
|
|
||||||
"create": "create",
|
|
||||||
"save": "save",
|
|
||||||
"update": "update",
|
|
||||||
"commit": "commit"
|
|
||||||
},
|
|
||||||
"noun": {
|
|
||||||
"file": { "one": "file", "other": "files" },
|
|
||||||
"commit": { "one": "commit", "other": "commits" },
|
|
||||||
"change": { "one": "change", "other": "changes" }
|
|
||||||
},
|
|
||||||
"article": {
|
|
||||||
"the": "the",
|
|
||||||
"a": { "one": "a", "vowel": "an" }
|
|
||||||
},
|
|
||||||
"prompt": {
|
|
||||||
"yes": "y",
|
|
||||||
"no": "n",
|
|
||||||
"all": "a",
|
|
||||||
"skip": "s",
|
|
||||||
"quit": "q"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `core.*` - Semantic Intents
|
|
||||||
|
|
||||||
Intents encode meaning and behavior:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"core": {
|
|
||||||
"edit": {
|
|
||||||
"_meta": {
|
|
||||||
"type": "action",
|
|
||||||
"verb": "common.verb.edit",
|
|
||||||
"dangerous": false
|
|
||||||
},
|
|
||||||
"question": "Should I {{.Verb}} {{.Subject}}?",
|
|
||||||
"confirm": "{{.Verb | title}} {{.Subject}}?",
|
|
||||||
"success": "{{.Subject | title}} {{.Verb | past}}",
|
|
||||||
"failure": "Failed to {{.Verb}} {{.Subject}}"
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"_meta": {
|
|
||||||
"type": "action",
|
|
||||||
"verb": "common.verb.delete",
|
|
||||||
"dangerous": true,
|
|
||||||
"default": "no"
|
|
||||||
},
|
|
||||||
"question": "Delete {{.Subject}}? This cannot be undone.",
|
|
||||||
"confirm": "Really delete {{.Subject}}?",
|
|
||||||
"success": "{{.Subject | title}} deleted",
|
|
||||||
"failure": "Failed to delete {{.Subject}}"
|
|
||||||
},
|
|
||||||
"save": {
|
|
||||||
"_meta": {
|
|
||||||
"type": "action",
|
|
||||||
"verb": "common.verb.save",
|
|
||||||
"supports": ["all", "skip"]
|
|
||||||
},
|
|
||||||
"question": "Save {{.Subject}}?",
|
|
||||||
"success": "{{.Subject | title}} saved"
|
|
||||||
},
|
|
||||||
"commit": {
|
|
||||||
"_meta": {
|
|
||||||
"type": "action",
|
|
||||||
"verb": "common.verb.commit",
|
|
||||||
"dangerous": false
|
|
||||||
},
|
|
||||||
"question": "Commit {{.Subject}}?",
|
|
||||||
"success": "{{.Subject | title}} committed",
|
|
||||||
"failure": "Failed to commit {{.Subject}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Template Functions
|
|
||||||
|
|
||||||
Available in translation templates:
|
|
||||||
|
|
||||||
| Function | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `title` | Title case | `{{.Name \| title}}` → "Hello World" |
|
|
||||||
| `lower` | Lower case | `{{.Name \| lower}}` → "hello world" |
|
|
||||||
| `upper` | Upper case | `{{.Name \| upper}}` → "HELLO WORLD" |
|
|
||||||
| `past` | Past tense verb | `{{.Verb \| past}}` → "edited" |
|
|
||||||
| `plural` | Pluralize noun | `{{.Noun \| plural .Count}}` → "files" |
|
|
||||||
| `article` | Add article | `{{.Noun \| article}}` → "a file" |
|
|
||||||
| `quote` | Wrap in quotes | `{{.Path \| quote}}` → `"/path/to/file"` |
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Phase 1: Foundation
|
|
||||||
1. Define `Composed` and `Subject` types
|
|
||||||
2. Add `S()` / `Subject()` builder
|
|
||||||
3. Add `T()` / `C()` with intent resolution
|
|
||||||
4. Parse `_meta` from JSON
|
|
||||||
5. Add template functions (title, lower, past, etc.)
|
|
||||||
|
|
||||||
### Phase 2: CLI Integration
|
|
||||||
1. Implement `cli.Confirm()` using intents
|
|
||||||
2. Implement `cli.Question()` with options
|
|
||||||
3. Implement `cli.Choose()` for lists
|
|
||||||
4. Localize prompt characters [y/N] → [j/N] etc.
|
|
||||||
|
|
||||||
### Phase 3: Grammar Engine
|
|
||||||
1. Verb conjugation (past tense, etc.)
|
|
||||||
2. Noun plurality with irregular forms
|
|
||||||
3. Article selection (a/an, gender)
|
|
||||||
4. Language-specific rules
|
|
||||||
|
|
||||||
### Phase 4: Extended Languages
|
|
||||||
1. Gender agreement (French, German, etc.)
|
|
||||||
2. Formality levels (Japanese, Korean, etc.)
|
|
||||||
3. Right-to-left support
|
|
||||||
4. Plural forms beyond one/other (Russian, Arabic, etc.)
|
|
||||||
|
|
||||||
## Example: Full Flow
|
|
||||||
|
|
||||||
```go
|
|
||||||
// In cmd/dev/dev_commit.go
|
|
||||||
path := "/Users/dev/project"
|
|
||||||
files := []string{"main.go", "config.yaml"}
|
|
||||||
|
|
||||||
// Old way (hardcoded English, manual prompt handling)
|
|
||||||
fmt.Printf("Commit %d files in %s? [y/N] ", len(files), path)
|
|
||||||
var response string
|
|
||||||
fmt.Scanln(&response)
|
|
||||||
if response != "y" && response != "Y" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// New way (semantic, localized, integrated)
|
|
||||||
if !cli.Confirm("core.commit", i18n.S("file", path).Count(len(files))) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For German user, displays:
|
|
||||||
// "2 Dateien in /Users/dev/project committen? [j/N]"
|
|
||||||
// (note: "j" for "ja" instead of "y" for "yes")
|
|
||||||
```
|
|
||||||
|
|
||||||
## JSON Schema
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"common": {
|
|
||||||
"description": "Reusable translation fragments",
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"core": {
|
|
||||||
"description": "Semantic intents with metadata",
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"_meta": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": { "enum": ["action", "question", "info"] },
|
|
||||||
"verb": { "type": "string" },
|
|
||||||
"dangerous": { "type": "boolean" },
|
|
||||||
"default": { "enum": ["yes", "no"] },
|
|
||||||
"supports": { "type": "array", "items": { "type": "string" } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"question": { "type": "string" },
|
|
||||||
"confirm": { "type": "string" },
|
|
||||||
"success": { "type": "string" },
|
|
||||||
"failure": { "type": "string" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Grammar Fundamentals
|
|
||||||
|
|
||||||
Parts of speech we need to handle:
|
|
||||||
|
|
||||||
| Part | Role | Example | Transforms |
|
|
||||||
|------|------|---------|------------|
|
|
||||||
| **Verb** | Action | delete, save, commit | tense (past/present), mood (imperative) |
|
|
||||||
| **Noun** | Subject/Object | file, commit, user | plurality, gender, case |
|
|
||||||
| **Article** | Determiner | a/an, the | vowel-awareness, gender agreement |
|
|
||||||
| **Adjective** | Describes noun | modified, new, deleted | gender/number agreement |
|
|
||||||
| **Preposition** | Relation | in, from, to | - |
|
|
||||||
|
|
||||||
### Verb Conjugation
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"common": {
|
|
||||||
"verb": {
|
|
||||||
"delete": {
|
|
||||||
"base": "delete",
|
|
||||||
"past": "deleted",
|
|
||||||
"gerund": "deleting",
|
|
||||||
"imperative": "delete"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For most English verbs, derive automatically:
|
|
||||||
- `past`: base + "ed" (or irregular lookup)
|
|
||||||
- `gerund`: base + "ing"
|
|
||||||
|
|
||||||
### Noun Handling
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"common": {
|
|
||||||
"noun": {
|
|
||||||
"file": {
|
|
||||||
"one": "file",
|
|
||||||
"other": "files",
|
|
||||||
"gender": "neuter"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Article Selection
|
|
||||||
|
|
||||||
English: a/an based on next word's sound (not letter)
|
|
||||||
- "a file", "an item", "a user", "an hour"
|
|
||||||
|
|
||||||
Other languages: gender agreement (der/die/das, le/la, etc.)
|
|
||||||
|
|
||||||
## DX Improvements
|
|
||||||
|
|
||||||
### 1. Compile-Time Validation
|
|
||||||
- `go generate` checks all `T("core.X")` calls have matching JSON keys
|
|
||||||
- Warns on missing `_meta` fields
|
|
||||||
- Type-checks template variables
|
|
||||||
|
|
||||||
### 2. IDE Support
|
|
||||||
- JSON schema for autocomplete in translation files
|
|
||||||
- Go constants generated from JSON keys: `i18n.CoreDelete` instead of `"core.delete"`
|
|
||||||
|
|
||||||
### 3. Fallback Chain
|
|
||||||
```
|
|
||||||
T("core.delete", subject)
|
|
||||||
→ try core.delete.question
|
|
||||||
→ try core.delete (plain string)
|
|
||||||
→ try common.action.delete
|
|
||||||
→ return "Delete {{.Subject}}?" (hardcoded fallback)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Debug Mode
|
|
||||||
```go
|
|
||||||
i18n.Debug(true) // Shows: [core.delete] Delete file.txt?
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Short Subject Syntax
|
|
||||||
```go
|
|
||||||
// Instead of:
|
|
||||||
i18n.T("core.delete", i18n.S("file", path))
|
|
||||||
|
|
||||||
// Allow:
|
|
||||||
i18n.T("core.delete", path) // Infers subject type from intent's expected noun
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Fluent Chaining
|
|
||||||
```go
|
|
||||||
i18n.T("core.delete").
|
|
||||||
Subject("file", path).
|
|
||||||
Count(3).
|
|
||||||
Question() // Returns just the question string
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes for Future Implementation
|
|
||||||
|
|
||||||
- Use `github.com/gertd/go-pluralize` for English plurality
|
|
||||||
- Consider `github.com/nicksnyder/go-i18n` patterns for CLDR plural rules
|
|
||||||
- Store compiled templates in sync.Map for caching
|
|
||||||
- `_meta` parsing happens once at load time, not per-call
|
|
||||||
- CLI prompt chars from `common.prompt.*` - allows `[j/N]` for German
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
1. **Verb conjugation library** - Use existing Go library or build custom?
|
|
||||||
2. **Gender detection** - How to infer gender for subjects in gendered languages?
|
|
||||||
3. **Fallback behavior** - What happens when intent metadata is missing?
|
|
||||||
4. **Caching** - Should compiled templates be cached?
|
|
||||||
5. **Validation** - How to validate intent definitions at build time?
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue