# Unified command registration proposal Status: direction agreed; declarations, consumer examples, and design checks are prepared for final review. Supporting API names and input/state separation below are proposed details of that direction. No implementation is authorized by this document alone. ## Purpose and authority Applications and integrations construct captured command declarations and install them through the same core API. Installation validates each batch before registry mutation. Integrations supply complete standard families and focused conveniences while retaining ownership of their provider semantics. This document records the selected direction following the [registration alternatives](registration-alternatives.md). It replaces that comparison's recommendation of Option A with a refinement of Option C: capture declarations and any construction errors eagerly, then report errors at installation. The comparison remains background, not the selected API proposal. [ADR 0001](../adr/0001-command-dispatch-and-inputs.md) owns explicit registration, typed inputs, and command ownership. [ADR 0010](../adr/0010-optional-integration-modules.md) owns optional module boundaries and convenient integration registration. The [module roadmap](modules.md) tracks extraction work. Reconcile the namespace lifetime decision below with the relevant ADR and architecture-policy wording before implementation; this proposal does not silently rewrite those records. Design for the best consumer API without preserving current implementation signatures or machinery. Existing code can supply evidence and reusable work, but does not constrain the design. This is future API design, not documentation of implemented behavior. ## Selected consumer model A consumer can use only the core or add any supported combination of integrations. Every command reaches the application through core `New` or `Register`, regardless of whether an application or integration declared it. There is no mandatory integration interface, application-wide generic input type, or separate integration installation lifecycle. The core remains independent of integration modules and their upstream libraries. Integrations may depend on one another as documented. Such module dependencies do not install commands or acquire providers. The selected modules remain `CommandKit`, `CommandKit/integrations/ckconfig`, and `CommandKit/integrations/ckprompt`, under the existing repository module prefix. Each application command retains its own typed input and domain handler. Integration suites retain their own typed context and provider declarations. Both produce the same opaque core `Command` handle. Consumers need not construct individual standard handlers, duplicate integration adapters, or expose backend objects to assemble an application. ## Declaration and installation ### One declaration model A `Command` is a captured declaration, not proof that the declaration is valid or installed. Ordinary command, group, scope, and suite adapter construction does not require a separate error check at each call site. The uniform construction shape is below. The complete declarations and examples later in this document provide the supporting fields and semantics for review. ```go // Core package: func NewCommand[I any](spec CommandSpec[I]) Command func Group(name, summary string, children ...Command) Command func New(identity Identity, commands ...Command) (*App, error) func (app *App) Register(commands ...Command) error ``` The initial integrations expose complete-family factories using their own suite types: ```go // In each initial integration package; Suite belongs to that integration: func Command[I, S any](suite Suite[I, S]) commandkit.Command ``` Here `I` is the suite's CLI input and `S` is its shared invocation state. This proposed separation resolves the pointer-isolation issue without a clone hook or an application-wide type. Future integrations can accept declarations suited to their resources while returning the same core `Command`; the core does not prescribe a universal suite. Application-owned command factories follow the same declaration pattern. Application helpers that genuinely perform other fallible work may still return errors; the design does not suppress those errors or require all application functions to be infallible. ### Eager capture, errors reported at installation Factories capture declaration metadata and callback references and assemble neutral command descriptions immediately. Validation that can already be done may occur then, but its errors remain attached to the declaration for installation to report. Internally fallible assembly can capture an error without returning it to the consumer at every construction step. Use the proposed `WithError(command, err)` core operation to retain integration construction errors in opaque declarations. Its complete signature and no-op interaction are specified below. Do not substitute an untyped integration payload, root-private access, or a general preparation callback. The proposed `DeferredGroup` lifecycle from the comparison is not selected. `New` and `Register` report retained declaration errors and validate the batch, including bindings, root placement, and collisions, before registry mutation. A failed `New` yields no usable application. A failed `Register` leaves the existing registry intact. No rollback guarantee spans separate registration calls. Declaration construction, installation, help, and version do not invoke provider acquisition or invocation input/context factories. They do not validate files, credentials, or application configuration. Invalid wiring fails at installation; an upstream error returned by a valid provider remains an invocation result. An originally nil loader remains invalid wiring even if an adapter would wrap it in a non-nil function. Integration declarations retain enough information to detect this. An intentionally absent capability is distinct from an invalid adapter. Errors identify the affected command family and relevant scope, binding, or capability and preserve supported wrapped error identity; exact prose is not selected here. ## Integration composition and conveniences Configuration scopes are composed before installation into one configuration suite. Application and engine scopes can share that suite. Named definition inspection remains independently selectable: installing an engine configuration scope does not automatically install definition commands, or vice versa. Keep provider declarations integration-owned. Configuration supports ConfigKit for now; the core does not acquire a universal configuration or suite interface. The PromptKit integration can use the ConfigKit integration's selection and scope contracts through its selected dependency. Provide the focused `ckconfig.AppCommand(load)` convenience for a context-free application configuration loader. It declares the standard `app` scope, with a default description, through the same declaration model as the full suite. It does not require an unused context argument or `Suite[struct{}]` at the consumer call site. A loader still receives `context.Context` and configuration selection. Use the full suite when adding scopes, selectors, custom state, or other metadata. Do not initially expose integration-specific `Register` or `RegisterApp` functions alongside declaration factories. Core registration is the installation path for all consumers. Additional conveniences should address demonstrated consumer needs rather than mirror every underlying constructor automatically. ## Namespaces and empty declarations Help and version are reserved by core construction. Optional families claim their complete root namespace when installed. Before installation, a domain group may occupy that name; subsequent integration installation then fails with a collision. The reverse order also rejects a collision. Neither order replaces or merges an existing group. Standard-family declarations carry enforceable root-only placement metadata. Installing them beneath an ordinary group is an assembly error. Use the same `Command` type with the proposed `RootGroup` constructor specified below. It represents placement, not authentication of the declaring package. Installed standard families cannot be extended with private application operations or have their handlers replaced. The selected empty-suite behavior is: | Declaration | Installation result | | --- | --- | | Configuration suite with no scopes | Declaration error | | Named configuration scopes with no inspection operations | Install scope discovery with the actual empty capability sets | | Configuration scopes with supplied operations | Install discovery and supported operations | | Otherwise valid definition suite with no capabilities | Successful no-op; no namespace or help entry | | Adapter with a nil loader | Declaration error, even when selected capabilities otherwise appear empty | Use an explicit `commandkit.NoCommand()` declaration for intentional absence. Core installation skips it. A zero-value `Command` remains invalid. Integration factories validate or retain wiring errors before representing an empty family as a no-op; absence must not swallow errors. A no-op does not reserve a namespace. ## Capture, reuse, and invocation inputs Capture scope metadata, capability lists, selector applicability, bindings, and callback references when declarations are constructed. Copy the declaration collections needed for those semantics. Later edits to the source suite or its metadata slices do not change a returned command declaration. Declarations may be installed in multiple applications. Registries maintain independent installed state, and invocations have independent parsed state. Application callback closures and objects they reference remain application-owned and may deliberately be shared. These choices do not promise arbitrary deep copies or concurrent dispatch. Use the same typed, presence-aware binding primitives for application commands and integration inputs. Configuration selectors additionally declare scope applicability; definition selectors need no configuration-specific wrapper. Explicit empty, false, and zero values remain distinguishable from omission. An omitted `NewInput` or `NewContext` supplies the zero value of its respective type. For pointer types that is nil; do not silently allocate arbitrary pointer state or prohibit a loader that intentionally accepts nil. Applications supply a factory when initialized state is needed. A supplied factory returning nil retains its application-owned meaning. Scope-specific parsed values stay separate from shared acquisition state through the proposed `Suite[I, S]`: `NewInput` initializes each selected scope's input, while `NewContext` initializes `S` once for the selected inspection invocation. Providers receive both explicitly. Pointer inputs are independently initialized, not shallow copies of a single pointer. The detailed lifetime table below makes this change from the comparison's mixed context model explicit; it does not require arbitrary deep copies or prohibit pointer inputs or shared pointer state. ## Proposed core declarations The following declarations specify the public registration and binding surface used by these examples. Opaque types retain unexported representation; these are API declarations, not an implementation of their bodies. `context` and `io` are standard-library packages. Outcome semantics remain governed by [ADR 0005](../adr/0005-invocation-outcomes.md); this pass changes neither status mapping nor diagnostic delivery policy. ```go package commandkit import ( "context" "io" ) type App struct { // Unexported representation. } type Command struct { // Unexported representation. } type Flag[I any] struct { // Unexported representation. } type Argument[I any] struct { // Unexported representation. } type Identity struct { Name string Version string Revision string } type Streams struct { Out io.Writer Err io.Writer } type Invocation struct { Context context.Context Streams Streams } type Supplied[T any] struct { Value T Set bool } type FlagMeta struct { Name string Short string Usage string Required bool } type ArgumentMeta struct { Name string Usage string } type CommandSpec[I any] struct { Name string Summary string Description string NewInput func() I Flags []Flag[I] Arguments []Argument[I] CheckInput func(I) error Run func(Invocation, I) Outcome } func NewCommand[I any](spec CommandSpec[I]) Command func Group(name, summary string, children ...Command) Command func RootGroup(name, summary string, children ...Command) Command func WithError(command Command, err error) Command func NoCommand() Command func New(identity Identity, commands ...Command) (*App, error) func (app *App) Register(commands ...Command) error func (app *App) Execute(ctx context.Context, args []string, streams Streams) Outcome func ValueFlag[I, V any]( meta FlagMeta, defaultValue *V, parse func(string) (V, error), set func(*I, Supplied[V]), ) Flag[I] func BoolFlag[I any]( meta FlagMeta, defaultValue bool, set func(*I, Supplied[bool]), ) Flag[I] func RepeatFlag[I, V any]( meta FlagMeta, defaults []V, parse func(string) (V, error), set func(*I, Supplied[[]V]), ) Flag[I] func ValueArgument[I, V any]( meta ArgumentMeta, required bool, parse func(string) (V, error), set func(*I, Supplied[V]), ) Argument[I] func RepeatArgument[I, V any]( meta ArgumentMeta, min, max int, parse func(string) (V, error), set func(*I, Supplied[[]V]), ) Argument[I] type Severity string const ( SeverityWarning Severity = "warning" SeverityError Severity = "error" ) type Diagnostic struct { Severity Severity Write func(io.Writer) error } // Neutral value/diagnostic envelope; not a universal provider interface. type Acquired[T any] struct { Value T Diagnostics []Diagnostic } type OutcomeKind string const ( OutcomeSuccess OutcomeKind = "success" OutcomeUsage OutcomeKind = "usage" OutcomeOperational OutcomeKind = "operational" OutcomeDomain OutcomeKind = "domain" ) type DeliveryState string const ( DeliveryNone DeliveryState = "none" DeliveryComplete DeliveryState = "complete" DeliveryIncomplete DeliveryState = "incomplete" ) type Outcome struct { Kind OutcomeKind Status int Domain string Cause error DeliveryError error DiagnosticDelivery DeliveryState ReportIncomplete bool Diagnostics []Diagnostic } func Success() Outcome func Failure(cause error) Outcome ``` `NewCommand` records invalid bindings or invalid execution wiring as declaration errors; it does not require a construction error check. An ordinary group may have no execution handler. Opaque declarations expose no operation to mutate an installed tree, extract its handlers, or append children after capture. Collections in `CommandSpec`, groups, and defaults are captured as appropriate; runtime closure state and diagnostic writer effects remain caller-owned. ### Retained errors, placement, and no-ops `WithError` returns a new captured declaration retaining a non-nil error alongside any errors already attached to the declaration or its children. A nil error does not add an error. Installation reports retained errors with command context and preserves their supported `errors.Is`/`errors.As` identity. It need not prescribe an exact aggregate-error type or error text. Wrapping does not turn an invalid zero declaration into a valid command or erase a root-only marker. `RootGroup` has the same eager declaration behavior as `Group`, but carries root-only placement metadata. Core installation rejects it anywhere below the root, including inside a wrapper declared with ordinary `Group`. No integration name allowlist or declaring-package authentication is needed. `NoCommand` is an explicit absence marker. Core checks attached errors before skipping it, so `WithError(NoCommand(), err)` fails rather than hiding `err`. An unadorned no-op is skipped at the root or among children and contributes no help entry or reserved namespace. A zero-value `Command` is not this marker and fails installation. A root-only group with no children remains a real group, not an absence marker; the integrations select their own empty-suite semantics. These operations compose without private-state access or delayed preparation. An integration can construct an ordinary neutral tree, mark its root, and attach its own validation error: ```go // Schematic integration-owned assembly: every function called here runs eagerly. func familyDeclaration(children []commandkit.Command, wiringErr error) commandkit.Command { group := commandkit.RootGroup("config", "Inspect configuration.", children...) return commandkit.WithError(group, wiringErr) } ``` The actual integration factory validates scope/provider metadata and eagerly constructs all supported children. Core constructors retain their own binding errors. There is no exported preparation callback, invocation-time declaration validation, or root helper that understands `ckconfig.Suite`. ### Typed flag composition needed by optional modules Scopes need to apply the same parsed selector to independently initialized inputs. The integration cannot inspect a core `Flag`'s private parser or setter. Provide one narrow typed composition mechanism, rather than exporting those internals: ```go // In commandkit: type FlagValues[I any] struct { // Unexported representation. } func CaptureFlags[Outer, I any]( flags []Flag[I], target func(*Outer) *FlagValues[I], ) []Flag[Outer] func (values FlagValues[I]) ApplyTo(target *I, selected ...Flag[I]) error ``` `CaptureFlags` eagerly captures bindings and supplies ordinary outer-command flag declarations. During an execution invocation those declarations record converted values, defaults, and presence in the targeted `FlagValues`; they do not initialize `I` or acquire providers. Nil target callbacks and invalid inner bindings remain declaration errors. The core's ordinary flag validation still catches duplicate names and conflicts with standard flags after composition. `ApplyTo` delivers only the selected bindings into an already initialized input. The integration selects those bindings from its captured scope-applicability metadata. Binding handles carry sufficient identity to match their captured values without string-based retrieval by handlers. Selecting a binding absent from the captured set or providing a nil destination pointer is an application/ integration wiring error. The zero `FlagValues` with no selected bindings is valid. Execution-required checks for captured inner flags are deferred until `ApplyTo` selects the applicable subset; a required app-only selector must not become mandatory for a PromptKit-only scope request. Missing required selected values are input-validation errors, routed through the standard command's ordinary `CheckInput`/usage path before shared-state initialization or acquisition. Help uses metadata without applying values or requiring execution inputs. Core-owned repeatable-value slices are independent per application of the values; custom parsed objects and accessor effects retain application ownership. This API does not deep-copy arbitrary application values or expose backend state. It is a reusable typed input-composition contract for integrations, not a second parser, a raw-argv handler API, or an untyped integration payload. For example, the private configuration command input can contain `FlagValues[I]` beside standard selection/scope fields. The integration declares its selector flags through `CaptureFlags`, resolves selected scopes in its execution input-validation path, and calls `ApplyTo` for each scope. Definition inspection applies the whole captured flag set once. This explains the public seam; private input structs and the exact assembly algorithm remain implementation choices. ## Proposed configuration integration declarations The `ckconfig` package owns the following contracts. Only the four implemented inspection operations appear here; comparison remains deferred. `config scopes` is supplied by suite assembly, not an acquisition operation. ```go package ckconfig import ( "context" "io" commandkit "gitea.maximumdirect.net/eric/CommandKit" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) type Selection struct { Config commandkit.Supplied[string] Overlay commandkit.Supplied[string] } type Operation string const ( Validate Operation = "validate" Show Operation = "show" Sources Operation = "sources" Explain Operation = "explain" ) type Request struct { Operation Operation Selection Selection } type EffectiveWriter interface { WriteEffective(io.Writer) error } type SourceReader interface { Sources() []configkit.SourceInfo } type ExplanationReader interface { Explain(configkit.Path) (configkit.Explanation, bool) WriteExplanation(io.Writer, configkit.Path) error } type View struct { Effective EffectiveWriter Sources SourceReader Explanation ExplanationReader } type Scope[I, S any] struct { Name string Summary string Operations []Operation Acquire func(context.Context, Request, I, S) (commandkit.Acquired[View], error) // Adapter construction issues are retained in unexported representation. } type Selector[I any] struct { Binding commandkit.Flag[I] Scopes []string } type Suite[I, S any] struct { NewInput func() I NewContext func() S Selectors []Selector[I] CheckInput func(Selection, I) error Scopes []Scope[I, S] } func FromConfigKit[I, S, T any]( name, summary string, load func(context.Context, Selection, I, S) (*configkit.Result[T], error), ) Scope[I, S] func Command[I, S any](suite Suite[I, S]) commandkit.Command func AppCommand[T any]( load func(context.Context, Selection) (*configkit.Result[T], error), ) commandkit.Command ``` The generic types are local to the suite, not to `App`. `I` contains typed CLI values; `S` contains invocation state such as a cache. Neither belongs to the core configuration model because the core has no such model. A scope with no operations can omit `Acquire` and still be discoverable. A scope declaring operations needs valid acquisition wiring. Providers supply the relevant native view facets and validation diagnostics at invocation time, preserving current selected reports and classifications rather than implementing a second configuration engine. An empty selector scope list applies to all registered scopes; a nonempty list identifies its applicable registered scopes. Unknown identifiers and reserved standard-flag collisions are declaration errors. Scope identifiers, capability selection, defaults, and aggregate availability follow [ADR 0009](../adr/0009-configuration-scopes.md). Non-applicable selectors are not applied to a scope's input; this adds no new unused-selector rejection policy. `CheckInput` validates the selection and initialized input for each requested scope before acquisition. It may inspect presence without loading configuration. `FromConfigKit` declares the operations supported by the selected ConfigKit adapter and records the original loader, including an invalid nil loader. Consumers may explicitly narrow `Operations` before calling `Command`; a constructor issue is not erased by clearing the operation list. The suite factory captures those lists and its generated neutral command tree immediately. `AppCommand` uses an internal state-free suite with an `app` scope and a plain application-configuration description. It neither exposes placeholder type arguments nor demands a forwarding wrapper from the caller. A nil loader becomes a retained `config`/`app` declaration error. The full suite is used for composition; there is no incremental scope append operation after installation. ## Proposed PromptKit integration declarations The `ckprompt` package uses `ckconfig` selections and scope contracts through its selected module dependency. Definition selectors are plain typed flags; they do not use a configuration-scope wrapper. Named prompt and profile inspection remain the implemented subset; this pass adds no catalog operations. ```go package ckprompt import ( "context" commandkit "gitea.maximumdirect.net/eric/CommandKit" "gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig" promptkit "gitea.maximumdirect.net/eric/promptkit" ) type PromptRequest struct { Selection ckconfig.Selection ID string Version commandkit.Supplied[string] } type ProfileRequest struct { Selection ckconfig.Selection ID string } type Suite[I, S any] struct { NewInput func() I NewContext func() S Selectors []commandkit.Flag[I] CheckInput func(ckconfig.Selection, I) error InspectPrompt func(context.Context, PromptRequest, I, S) (commandkit.Acquired[*promptkit.PromptInspection], error) InspectProfile func(context.Context, ProfileRequest, I, S) (commandkit.Acquired[*promptkit.ProfileInspection], error) // Adapter construction issues are retained in unexported representation. } func ConfigScope[I, S any]( load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error), ) ckconfig.Scope[I, S] func FromEngine[I, S any]( load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error), ) Suite[I, S] func Command[I, S any](suite Suite[I, S]) commandkit.Command ``` `ConfigScope` declares the supported configuration facets for the `promptkit` scope; `FromEngine` declares supported named inspection callbacks. Both leave acquisition untouched until invocation and retain nil-loader errors. Each adapter maps the owning library's public diagnostics into core-owned `Diagnostic` values. Definition providers preserve diagnostics even when a named lookup fails. Consumers may leave either definition callback nil to omit that capability. A valid suite with both nil is an explicit no-op after eager declaration validation. A nil loader supplied to `FromEngine` is invalid wiring, not missing capability; setting its callbacks nil does not erase the retained error. For prompt lookup, the adapter passes the requested version value to PromptKit, with omission using its existing empty/default selection. The presence field remains available to a custom inspection callback; this does not redefine upstream version semantics. ## Input isolation and invocation lifetimes ### Why inputs and shared state have separate types The comparison used one context type holding both selector fields and a cache. A shared pointer to that object cannot isolate selector fields between scopes. Cloning arbitrary application graphs would add a promise CommandKit cannot make. The proposed `Suite[I, S]` makes the actual two responsibilities explicit: - `I`: per-scope/per-command CLI input, initialized and populated independently. - `S`: shared acquisition state for one selected inspection invocation. There is no public clone hook and no reflection-based allocation of pointer inputs. Two type parameters cost some declaration syntax in composed consumers, but make sharing deliberate and lifetimes predictable. `AppCommand` hides both for the simple case. This is a supporting API detail proposed for final review, not a change to unified construction or the independently owned suite principle. ### Factory timing | Path | `NewInput` | Suite `NewContext` | Acquisition | | --- | --- | --- | --- | | Construct declarations / `New` / `Register` | Not called | Not called | Not called | | Help, version, `config scopes` | Not called | Not called | Not called | | Application command execution | Once for the selected command | No suite factory | Caller handler | | Configuration inspection of one scope | Once for that scope | Once for the inspection invocation | Requested scope | | Configuration inspection of several scopes | Once per selected scope | Once, shared by selected scopes | Requested scopes, in selected operation order | | Named definition inspection | Once for the selected command | Once for that invocation | Selected inspection callback | Input conversion and scope/capability preflight use metadata without acquisition. For configuration execution, initialize and populate each requested scope input, then run its `CheckInput`; initialize the shared state before acquisition after input checks succeed. The input/context callbacks do not run for unselected scopes or unrelated families. This explicitly separates the former mixed-context role: `NewContext` remains once per invocation, and the new `NewInput` initializes scope storage independently. No existing callback is silently called once per scope. A missing factory supplies the zero value of its type. A nil pointer input/state is permitted when the receiving binding callbacks and providers support it. Applications requiring allocated pointer input supply `NewInput`; those requiring a cache or other initialized state supply `NewContext`. Factories that deliberately return shared mutable input objects retain caller-owned effects; CommandKit does not promise to repair those aliases. For scope-independent CLI storage, return fresh input storage as demonstrated below. Pointer shared state is supported and is shared deliberately, not copied. Typed captured flag values are applied only to applicable inputs. Repeatable collections owned by CommandKit are independent when materialized into different inputs. Arbitrary objects supplied by factories, custom parsers, or closures are not deeply cloned. Providers can retain their input pointer according to their callback contract without a later scope overwriting it through a shared template. Subsequent invocations initialize new input/state; registration does not retain an invocation cache. No concurrent-dispatch guarantee follows from these rules. ## Complete consumer registration examples These examples target the proposed APIs, not today's prototype. Each block is a complete consumer assembly function with imports and input declarations. Domain loaders and domain execution are supplied as typed function parameters where appropriate: their bodies are application behavior, not hidden registration helpers. No constructor error checks or provider acquisition are concealed in those parameters. The modules and declarations must be implemented before these blocks can compile against CommandKit itself. ### 1. Core only: typed commands and a composable group ```go package coreexample import ( "fmt" "strconv" commandkit "gitea.maximumdirect.net/eric/CommandKit" ) type GreetInput struct { Count commandkit.Supplied[int] Name commandkit.Supplied[string] Enabled commandkit.Supplied[bool] } func App() (*commandkit.App, error) { countDefault := 1 nameDefault := "reader" greet := commandkit.NewCommand(commandkit.CommandSpec[GreetInput]{ Name: "greet", Summary: "Write a greeting.", Flags: []commandkit.Flag[GreetInput]{ commandkit.ValueFlag( commandkit.FlagMeta{Name: "count", Usage: "Number of greetings."}, &countDefault, strconv.Atoi, func(input *GreetInput, value commandkit.Supplied[int]) { input.Count = value }, ), commandkit.ValueFlag( commandkit.FlagMeta{Name: "name", Usage: "Greeting recipient."}, &nameDefault, func(raw string) (string, error) { return raw, nil }, func(input *GreetInput, value commandkit.Supplied[string]) { input.Name = value }, ), commandkit.BoolFlag( commandkit.FlagMeta{Name: "enabled", Usage: "Enable greeting output."}, true, func(input *GreetInput, value commandkit.Supplied[bool]) { input.Enabled = value }, ), }, CheckInput: func(input GreetInput) error { if input.Count.Value < 0 { return fmt.Errorf("count must be nonnegative") } return nil }, Run: func(invocation commandkit.Invocation, input GreetInput) commandkit.Outcome { if !input.Enabled.Value { return commandkit.Success() } for i := 0; i < input.Count.Value; i++ { if _, err := fmt.Fprintf(invocation.Streams.Out, "Hello, %s!\n", input.Name.Value); err != nil { return commandkit.Failure(err) } } return commandkit.Success() }, }) text := commandkit.Group("text", "Text utilities.", greet) return commandkit.New(commandkit.Identity{Name: "Example", Version: "dev"}, text) } ``` `NewInput` is omitted, so ordinary command execution starts with a zero-valued `GreetInput` and applies defaults/presence through bindings. Explicit `--count=0`, `--name=`, and `--enabled=false` remain supplied. Invalid command or binding metadata is reported by the single final `New` call, not by `greet` or `text`. Negative count is instead a usage result from invocation-time input validation. No integration package or upstream configuration library is imported. For a caller that wants to execute the assembled app, the common invocation is: ```go outcome := app.Execute(ctx, args, commandkit.Streams{Out: stdout, Err: stderr}) // The caller owns streams, cancellation, and final process exit/status mapping. ``` ### 2. Context-free application configuration ```go package configexample import ( "context" commandkit "gitea.maximumdirect.net/eric/CommandKit" "gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) type AppConfig struct { Limit int `yaml:"limit"` } func App( load func(context.Context, ckconfig.Selection) (*configkit.Result[AppConfig], error), ) (*commandkit.App, error) { return commandkit.New( commandkit.Identity{Name: "Example", Version: "dev"}, ckconfig.AppCommand(load), ) } ``` The parameter is the complete loader signature. It can be an application's normal ConfigKit acquisition function directly; no unused context argument, dummy suite, or forwarding wrapper is required. `load` is not called during construction, installation, help, or version. An explicitly empty configuration/overlay value is available to the loader through `Selection` with `Set == true`. ### 3. Composed application: pointer inputs, shared cache, custom command ```go package composedexample import ( "context" "fmt" "os" "strconv" commandkit "gitea.maximumdirect.net/eric/CommandKit" "gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig" "gitea.maximumdirect.net/eric/CommandKit/integrations/ckprompt" configkit "gitea.maximumdirect.net/eric/ConfigKit" promptkit "gitea.maximumdirect.net/eric/promptkit" ) type AppConfig struct { Limit int `yaml:"limit"` } type InspectionInput struct { Campaign commandkit.Supplied[string] Limit commandkit.Supplied[int] } // Application-owned cache. Loaders may use Source for shared file reads. // Source bytes are treated as read-only by this example's loaders. type RunCache struct { files map[string][]byte } func (cache *RunCache) Source(ctx context.Context, path string) ([]byte, error) { if err := ctx.Err(); err != nil { return nil, err } if contents, ok := cache.files[path]; ok { return contents, nil } contents, err := os.ReadFile(path) if err != nil { return nil, err } cache.files[path] = contents return contents, nil } type RestoreInput struct { ID commandkit.Supplied[string] } func App( loadApp func(context.Context, ckconfig.Selection, *InspectionInput, *RunCache) (*configkit.Result[AppConfig], error), loadEngine func(context.Context, ckconfig.Selection, *InspectionInput, *RunCache) (*promptkit.Engine, error), restore func(context.Context, string) commandkit.Outcome, ) (*commandkit.App, error) { parseString := func(raw string) (string, error) { return raw, nil } campaignFlag := commandkit.ValueFlag[*InspectionInput, string]( commandkit.FlagMeta{Name: "campaign", Usage: "Select campaign inputs."}, nil, parseString, func(input **InspectionInput, value commandkit.Supplied[string]) { (*input).Campaign = value }, ) limitFlag := commandkit.ValueFlag[*InspectionInput, int]( commandkit.FlagMeta{Name: "limit", Usage: "Override the application item limit."}, nil, strconv.Atoi, func(input **InspectionInput, value commandkit.Supplied[int]) { (*input).Limit = value }, ) newInput := func() *InspectionInput { return &InspectionInput{} } newContext := func() *RunCache { return &RunCache{files: make(map[string][]byte)} } appScope := ckconfig.FromConfigKit("app", "Application settings.", loadApp) engineScope := ckprompt.ConfigScope(loadEngine) configCommand := ckconfig.Command(ckconfig.Suite[*InspectionInput, *RunCache]{ NewInput: newInput, NewContext: newContext, Selectors: []ckconfig.Selector[*InspectionInput]{ {Binding: campaignFlag, Scopes: []string{"app", "promptkit"}}, {Binding: limitFlag, Scopes: []string{"app"}}, }, Scopes: []ckconfig.Scope[*InspectionInput, *RunCache]{appScope, engineScope}, }) definitions := ckprompt.FromEngine(loadEngine) definitions.NewInput = newInput definitions.NewContext = newContext definitions.Selectors = []commandkit.Flag[*InspectionInput]{campaignFlag} definitions.InspectPrompt = nil // Install profiles only; no prompt command. definitionCommand := ckprompt.Command(definitions) restoreCommand := commandkit.NewCommand(commandkit.CommandSpec[RestoreInput]{ Name: "restore", Summary: "Restore a session.", Arguments: []commandkit.Argument[RestoreInput]{ commandkit.ValueArgument( commandkit.ArgumentMeta{Name: "ID", Usage: "Session identifier."}, true, parseString, func(input *RestoreInput, value commandkit.Supplied[string]) { input.ID = value }, ), }, Run: func(invocation commandkit.Invocation, input RestoreInput) commandkit.Outcome { return restore(invocation.Context, input.ID.Value) }, }) session := commandkit.Group("session", "Manage sessions.", restoreCommand) // Domain callback validation also uses captured errors, without a helper error. if restore == nil { session = commandkit.WithError(session, fmt.Errorf("session restore: handler is nil")) } return commandkit.New( commandkit.Identity{Name: "Example", Version: "dev"}, configCommand, definitionCommand, session, ) } ``` Domain loaders are passed directly to the adapters, so a nil original loader cannot be hidden by a forwarding closure. Each signature shows both the isolated pointer input and shared cache. Loader bodies choose ConfigKit schemas and override application rules; CommandKit does not prescribe those application internals. For `example config show --scope all --campaign spring --limit=0`: 1. Capability preflight selects the `app` and `promptkit` scopes. 2. `newInput` produces two distinct pointers; both receive `Campaign.Set == true` and `Campaign.Value == "spring"`. 3. Only the app input receives `Limit.Set == true` and `Limit.Value == 0`. The engine input retains its independently initialized, unsupplied limit. 4. `newContext` produces one `*RunCache`, passed to both acquisitions. If both loaders request the same path through `Source`, its bytes are read once within that invocation. Sharing is application-owned, not a global CommandKit cache. 5. An app loader applies its limit override only when `input.Limit.Set`; omission preserves its ordinary inherited configuration, while explicit zero overrides it. `--campaign=` likewise remains an explicitly empty campaign selection. 6. Native reports are presented under the existing `config show` preparation and delivery contract. No subsequent scope mutates the first input's selector fields. For `example promptkit show profile briefing --campaign spring`, definition input and state are initialized once for that invocation. Configuration scopes are not acquired. The definition suite does not declare `--limit`, so that flag is not accepted on the profile command. `session restore` invokes only the domain handler. Help and scope discovery create neither cache nor pointer input. `restore` validation illustrates application wiring errors, not a requirement that CommandKit inspect arbitrary closure internals. The application's declaration factory records a known nil dependency because it wrapped that callback itself. ### Independent installation and supported subsets Using the declarations assembled above, each of these is a separate assembly choice, not another installation API: ```go // Configuration including engine scope, without named definition inspection: app, err := commandkit.New(identity, configCommand, session) // Named profile inspection, without any config family: app, err := commandkit.New(identity, definitionCommand, session) // Add a complete optional family later through the same atomic batch API: err := app.Register(definitionCommand) ``` These are separate illustrative snippets; they are not successive redeclarations in one function. Importing `ckprompt` without passing one of its declarations does not install either surface. A metadata-only configuration scope can be declared with a name/summary, an empty `Operations` list, and no `Acquire`; it remains visible under `config scopes`. A full adapter's capabilities can also be narrowed: ```go appScope := ckconfig.FromConfigKit("app", "Application settings.", loadApp) appScope.Operations = []ckconfig.Operation{ckconfig.Validate, ckconfig.Show} ``` The suite factory captures this selection and installs only supplied operations plus discovery. It does not invent comparison or catalog providers. ## Focused design checks and expected outcomes These checks specify observable design results for review, not a requirement to add an exhaustive new test matrix. They use the declarations above; `identity` has a valid name. Declaration errors are returned Go errors from installation, not invocation status codes. Provider failures and ordinary input errors retain the invocation model. ### Retained errors are eager evidence, not delayed preparation ```go sentinel := errors.New("configuration declaration is incomplete") declaration := commandkit.WithError(commandkit.NoCommand(), sentinel) _, err := commandkit.New(identity, declaration) // err != nil; errors.Is(err, sentinel) is true despite the absence marker. ``` This snippet uses standard-library `errors`. `WithError` needs no callback and invokes no constructor later. A group's retained child errors and placement metadata survive copying, wrapping, and installation in another app. Nil error wrapping preserves the underlying declaration, including invalid zero commands. ### Invalid wiring versus missing capability ```go var missingLoader func(context.Context, ckconfig.Selection) (*configkit.Result[AppConfig], error) _, err := commandkit.New(identity, ckconfig.AppCommand(missingLoader)) // Installation fails with config/app loader context; no loader runs. emptyDefinitions := ckprompt.Command(ckprompt.Suite[struct{}, struct{}]{}) app, err := commandkit.New(identity, emptyDefinitions) // Success; no promptkit family appears in help. ``` An engine adapter created from a typed nil engine loader fails installation even if its inspection callbacks are later cleared. In contrast, a manually composed suite with one non-nil inspection provider and the other absent installs only the supplied capability. Metadata validation does not require every family operation. A valid provider that later returns a load/lookup error reports operational status 1 with supported public error identity and its captured diagnostics. ### Collisions, placement, and per-call atomicity ```go existing := commandkit.Group("config", "Application-owned configuration tools.") app, err := commandkit.New(identity, existing) // Success while no standard config family is installed under the selected rule. err = app.Register(ckconfig.AppCommand(load)) // Collision; the original domain group remains unchanged. wrapper := commandkit.Group("tools", "Utilities.", ckconfig.AppCommand(load)) _, err = commandkit.New(identity, wrapper) // Root-only placement error, not a new tools/config standard command path. ``` Installing the standard family first and then attempting a domain `config` group also fails. Neither direction merges children or replaces handlers. Installing `Group("help", ...)` or `Group("version", ...)` always collides with built-ins. For `app.Register(goodNewCommand, conflictingCommand)`, neither command is added when the batch fails; commands installed by earlier successful calls remain. A failed `New` returns no usable app. This is registry atomicity, not rollback of arbitrary effects inside application callbacks. ### Empty declarations | Input | Expected installation result | | --- | --- | | `ckconfig.Command(ckconfig.Suite[struct{}, struct{}]{})` | Error: no configuration scopes | | One named scope with empty operations and nil acquisition | Success; `config scopes` describes it, no inspection verbs supplied by that scope | | Valid definition suite with no callbacks | Explicit absence, no namespace claimed | | `NoCommand()` beside ordinary children | Skipped, no placeholder help item | | `WithError(NoCommand(), sentinel)` | Error retaining sentinel identity | | Zero `Command{}` at the root or as a child | Declaration error | | `RootGroup("config", ..., child)` under an ordinary parent | Placement error | An empty definition no-op nested under `Group("tools", ...)` remains absence, not a misplaced real family. A real empty `RootGroup` is still subject to placement and collision checks. Configuration and definition factories choose the relevant empty-family behavior before producing these declarations. ### Capture and reuse ```go scope := ckconfig.FromConfigKit("app", "Application settings.", loadApp) suite := ckconfig.Suite[*InspectionInput, *RunCache]{ NewInput: newInput, NewContext: newContext, Selectors: []ckconfig.Selector[*InspectionInput]{ {Binding: campaignFlag, Scopes: []string{"app"}}, }, Scopes: []ckconfig.Scope[*InspectionInput, *RunCache]{scope}, } declaration := ckconfig.Command(suite) suite.Scopes[0].Summary = "Changed source metadata" suite.Scopes[0].Operations = []ckconfig.Operation{ckconfig.Validate} suite.Selectors[0].Scopes[0] = "another-scope" first, firstErr := commandkit.New(identity, declaration) second, secondErr := commandkit.New(identity, declaration) // Both installations use the original captured scope, operation, and selector metadata. ``` The variables here have the types from the composed example. Original captured operations remain available, the summary remains unchanged, and the selector still applies to `app`; the later source edit is not revalidated as though it were the returned command. Registry and invocation state are independent in `first` and `second`. `firstErr` and `secondErr` are nil for otherwise valid wiring. Callback references are shared intentionally, so mutable objects captured inside those callbacks remain application-owned. Reusing a declaration does not authorize concurrent execution or clone a callback's object graph. ### Initialization and scope isolation The composed example's selected aggregate inspection calls `newInput` twice and `newContext` once. It delivers distinct `*InspectionInput` addresses but the same `*RunCache` to the two providers. Retaining the app input in a provider does not expose it to later engine-selector writes. Selecting one scope calls each factory once; selecting profile inspection initializes only its own invocation. Without input selectors, a suite may deliberately use pointer `I` with no `NewInput` and a loader accepting nil. With selectors whose accessors dereference that pointer, it supplies the input factory, just as a pointer-shaped application command does. Without `NewContext`, pointer `S` is nil; providers that require a cache supply the state factory. A fresh parsed-value store does not imply that arbitrary application factories returning aliases are repaired or deep-copied. ## Namespace reconciliation and implementation handoff The user-selected lifetime is precise: help/version are always reserved; configuration and PromptKit standard roots are owned only when their real family is installed. An absent/no-op family reserves nothing. A domain group using an uninstalled integration name is permitted, but blocks later installation of the same root with a collision. Root-only placement is independent of that lifetime. This clarifies an ambiguity rather than assuming that earlier broad reservation wording already specifies the same lifetime. The durable documents need deliberate alignment before implementation: | Owner | Required alignment after final proposal review | | --- | --- | | ADR 0001 | Clarify the reservation lifetime through the ADR change process if its accepted wording is interpreted as unconditional reservation | | ADR 0010 | Retain core/integration dependency and convenient-composition boundaries; exact factories now return captured declarations | | Architecture policy | State installed-family ownership uniformly for config and promptkit; align its public facade rules with ADR 0010 | | Module roadmap | Replace the open registration alternative with this selected uniform model and reviewed supporting signatures | | Staged module implementation plan | Specify eager capture, error retention, neutral bindings, scope isolation, examples, and release adoption using the reviewed API | A new or superseding ADR can record the lifetime clarification where needed; accepted decision text is not revised silently. This authoring pass updates only this proposal, leaving policy/ADR reconciliation and the staged plan for the explicit post-review handoff requested above. It does not claim those documents are already aligned or authorize implementation. ## Review checklist The requested declaration/example work is now present: 1. Core-only typed command/group construction with a single installation boundary. 2. Context-free `AppCommand` with the complete loader signature and no dummy state. 3. Visible composed suite assembly, application command, and independent capability selection. 4. Concrete `WithError`, `RootGroup`, and `NoCommand` contracts, with no delayed preparation or private-state access. 5. Typed selector composition, distinct pointer inputs, explicit presence, and one shared invocation cache with documented factory timing. 6. Focused expected outcomes for invalid wiring, collisions, placement, emptiness, capture, and reuse. 7. An explicit durable-document reconciliation and post-review implementation-plan handoff, without prematurely editing accepted records or implementing code. Final review should particularly consider the proposed `Suite[I, S]` separation and the narrow `CaptureFlags`/`FlagValues` composition seam. These are now concrete supporting API choices rather than unresolved isolation mechanics. The uniform declaration model is selected; these names and details await the user's final review before implementation.