# Unified command registration API Status: finalized API design; implementation is pending. The declarations, consumer examples, and design checks below define the selected target API, including the additions and clarifications selected in the finalized backend design. 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 defines the selected registration API: capture declarations and any construction errors eagerly, then report errors at installation. The completed alternatives comparison is retained in Git history. [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. [ADR 0011](../adr/0011-captured-declarations-and-installation.md) records the captured-declaration model, optional-family namespace lifetime, and separated input/shared-state lifetimes. The [architecture policy](../policy/architecture.md) reflects those boundaries. The [module roadmap](modules.md) tracks extraction work. The finalized [backend design](registration-backend.md) owns internal structure and shared implementation mechanics. Its resolved composition choices are incorporated below; this API design remains a target rather than implemented behavior. 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 implementation. ```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 selected 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 selected `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 deferred preparation lifecycle. `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 application/suite input/state factories. Built-in help may prepare its own private typed input through the common compiler, without initializing the target command or invoking its validation/execution callbacks. These paths 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 selected `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 retain adapter-construction errors before representing an empty family as a no-op. Validate bindings needed by generated commands; unused selector bindings are ignored when they contribute to no generated inspection command. Integration-owned scope identifiers, applicability references, and enabled-provider wiring still undergo validation. Ignoring unused bindings does not erase retained adapter errors. This is the selected [unused-binding resolution](registration-backend.md#resolved-unused-binding-validation-oq-1). A no-op does not reserve a namespace, and `NoCommand()` needs no validation-bearing overload or separate public declaration validator. ## 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 `NewState` 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 selected `Suite[I, S]`: `NewInput` initializes each selected scope's input, while `NewState` 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 spells out those calls without requiring arbitrary deep copies or prohibiting pointer inputs or shared pointer state. ## Selected 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 Children []Command 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. ### Runnable parents and captured children `CommandSpec.Children` combines a typed handler with child declarations. Capture its child-handle slice and apply the same immediate composition, collision, and installation placement checks used by `Group`. A runnable parent follows its handler and operand requirements on bare invocation; help describes the parent and children under [ADR 0006](../adr/0006-command-grammar.md). `Group` remains the convenience for a non-runnable parent. Neither form permits mutation or child append operations after capture. The [backend design](registration-backend.md#immutable-declaration-records) uses the same captured representation and installation path for both. ### 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. } // Non-nil sentinel errors; compare using errors.Is, including through wrapping. var ( ErrFlagInput error // Missing required selected flag value. ErrFlagWiring error // Invalid binding selection or destination. ) 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. Copying a binding handle or capturing it in a command preserves its identity. Constructing another flag with identical metadata creates a distinct binding; matching names, parsers, or setters do not make it interchangeable with the original. Reuse the original handles when selecting values to apply. The same handle can be captured by separate declarations without sharing invocation values. 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 match `ErrFlagInput` through `errors.Is` and produce usage outcomes. An absent binding or nil destination matches `ErrFlagWiring` and produces an operational failure, not an operator usage error. Each error produced by `ApplyTo` identifies one of these categories and includes relevant flag context without requiring callers to parse its message. The core's `CheckInput` error handling recognizes `ErrFlagWiring` through wrapping and routes it to the operational path. `ErrFlagInput` and ordinary input-validation errors use the usage path. Integrations may therefore return an `ApplyTo` error from their input-validation path, adding family/scope context by wrapping it, without misclassifying wiring defects. Both failures stop before shared-state initialization or acquisition. Metadata wiring defects detectable during assembly remain retained installation errors; these categories do not defer those checks to execution. 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. ## Selected 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 NewState func() S Selectors []Selector[I] CheckInput func(scope string, selection Selection, input 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 WithScopeError[I, S any](scope Scope[I, S], err 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 scope identifiers are declaration errors. Bindings used by generated commands undergo core validation, including standard-flag collisions; unused selector bindings follow the selected absence/unused-binding rule above. 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` receives the registered name of each requested scope alongside its selection and initialized, filtered input before acquisition. It can distinguish a selector omitted for an applicable scope from one intentionally not delivered to another scope, and apply domain rules accordingly without loading configuration. The definition suite retains its scope-free validation signature; it has no configuration scope to supply. `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. ### Retained scope errors `ckconfig.WithScopeError` returns a scope retaining a non-nil construction error alongside earlier causes, leaving the original scope unchanged. A nil error preserves the scope. Retained causes preserve supported `errors.Is`/`errors.As` identity across scope copies and narrowing of `Operations`. The family factory collects those issues into its command declaration before capability selection can discard them; no acquisition callback runs to discover a construction error. `ckprompt.ConfigScope` uses this public operation to retain original loader errors without accessing private `ckconfig` state. It otherwise declares the ordinary scope fields and capabilities. The [scope-error resolution](registration-backend.md#resolved-scope-error-handoff-oq-2) records the rationale and rejected alternative; the operation adds no new scope installation lifecycle. ## Selected 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 NewState 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 suite with both callbacks nil contributes no commands and ignores unused selector bindings. It returns a no-op retaining any adapter-construction errors; those errors still fail installation. 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 Combining selector storage and a cache in one context type obscures their distinct lifetimes and can cause scope-specific inputs to alias. Separate input values could share a cache through references, but would leave input isolation and state lifetime implicit. The selected `Suite[I, S]` makes the two responsibilities explicit without requiring arbitrary application graphs to be cloned: - `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 selected separation supports unified construction and independently owned suites. ### Factory timing The execution rows describe the normal path after applicable conversion and preflight checks succeed and execution reaches the indicated stage. They do not require callbacks to run after a failure. An input-application or validation failure can stop processing before later scopes are initialized; shared state and acquisition are reached only after all requested input checks succeed. The no-call rows remain unconditional for CommandKit-owned paths. | Path | `NewInput` | Suite `NewState` | 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 | The factory counts describe application inputs and suite input/state. Built-in help may prepare its own private input through the common compiler; it still leaves the target application's and suites' factories, validation/execution callbacks, and providers uninvoked. Input conversion and scope/capability preflight use metadata without acquisition. All supplied occurrences on an installed command undergo conversion, including selectors inapplicable to the requested scope and values supplied with help. Scope filtering affects value application and required-value checks, not conversion; an earlier malformed singleton occurrence remains an error even if a later one is valid. 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/state callbacks do not run for unselected scopes or unrelated families. This explicitly separates the former mixed-context role: `NewState` names the shared acquisition-state factory, distinct from `context.Context`; it runs once when execution reaches that stage. `NewInput` initializes scope storage independently. No shared-state 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 `NewState`. 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 selected 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{} } newState := 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, NewState: newState, CheckInput: func(scope string, selection ckconfig.Selection, input *InspectionInput) error { // Example application policy; it does not apply to engine inspection. if scope == "app" && input.Campaign.Set && !input.Limit.Set { return fmt.Errorf("application campaign inspection requires --limit") } return nil }, 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.NewState = newState 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. `newState` 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. The example's scope-aware validation permits `example config show --scope promptkit --campaign spring` without `--limit`. The same campaign selection for `--scope app` requires an explicitly supplied limit, including zero. An aggregate request missing that limit fails input validation before shared-state creation or either acquisition. This is the example application's policy, not a new CommandKit selector requirement. `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 target behavior, 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 | | Definition suite with no callbacks and unused selector bindings | Explicit absence, unused bindings ignored; retained adapter errors still fail | | `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, NewState: newState, 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 When its preflight and input checks succeed, the composed example's selected aggregate inspection calls `newInput` twice and `newState` 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 `NewState`, 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. ### Scope validation, flag identity, and error categories | Situation | Expected result | | --- | --- | | Campaign selected for the example's app scope, with no limit | Scope-aware `CheckInput` returns a usage error; no shared state or acquisition | | Same campaign selected for only the engine scope | Validation receives scope `promptkit`; the app-only limit rule does not apply | | A required applicable flag was not supplied | `ApplyTo` returns an error matching `ErrFlagInput`; usage outcome | | A required flag applies only to an unselected scope | Its requirement is not enforced for the selected scope | | A copied original binding is selected from its captured values | Identity matches; values apply to the initialized input | | A newly constructed lookalike binding is selected instead | It is absent from that captured set; error matches `ErrFlagWiring` | | The destination pointer passed to `ApplyTo` is nil | Error matches `ErrFlagWiring` | | An `ApplyTo` error is wrapped with family/scope context | `errors.Is` still identifies the category; wiring failures remain operational | | Input validation fails before later scopes are initialized | Later input factories need not run; `NewState` and acquisitions do not run | The flag-required checks here describe binding metadata, while the campaign/limit relationship is the example's application validation rule. Neither requires examining error-message text. `ErrFlagInput` and `ErrFlagWiring` classify errors reported by flag application; they do not replace captured installation errors or upstream provider errors. ## 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. The documentation alignment is complete: | Owner | Recorded decision | | --- | --- | | [ADR 0011](../adr/0011-captured-declarations-and-installation.md) | Captured declarations, atomic installation batches, installed-family ownership, root-only placement, separate input/shared-state lifetimes, and scope-aware configuration input validation | | [ADR 0001 status](../adr/0001-command-dispatch-and-inputs.md#status) | Remains accepted, with refinements recorded in ADR 0011 and its decision text preserved | | [ADR 0010](../adr/0010-optional-integration-modules.md) | Core/integration dependency boundaries remain in effect; ADR 0011 refines registration and supersedes the earlier prototype migration-guide requirement | | [Architecture policy](../policy/architecture.md) | Uniform installed-family ownership, optional integration facades, and declaration/input boundaries | | [Module roadmap](modules.md) | Final API and backend design selected; extraction and release adoption still pending | | [Backend design](registration-backend.md) | Shared internal pipeline, runnable children, scope-error attachment, unused-binding validation, and both composition-question resolutions | The [staged implementation plan](implementation.md) translates this finalized API, the finalized [backend design](registration-backend.md), and the module roadmap into ordered implementation assignments. Both composition questions are resolved; the plan covers eager capture, error retention, neutral bindings, scope isolation, maintained examples, and release adoption. The documentation alignment neither implements these changes nor authorizes implementation by itself.