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:
Snider 2026-02-03 07:13:00 +00:00
parent cbd8ea87df
commit c8124b7a88
5 changed files with 0 additions and 2863 deletions

View file

@ -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`

View file

@ -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.

View file

@ -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

View file

@ -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