From 0b5c1422ed8a5dbc0c5ae1e354bdcfaa63b18cfa Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 10 Oct 2026 15:21:16 +0000 Subject: [PATCH] Complete the registration proposal declarations and examples --- docs/roadmap/registration-proposal.md | 1058 +++++++++++++++++++++++-- 1 file changed, 1000 insertions(+), 58 deletions(-) diff --git a/docs/roadmap/registration-proposal.md b/docs/roadmap/registration-proposal.md index 086f123..cce19bf 100644 --- a/docs/roadmap/registration-proposal.md +++ b/docs/roadmap/registration-proposal.md @@ -1,7 +1,8 @@ # Unified command registration proposal -Status: direction agreed; exact API declarations and complete consumer examples -await the next authoring and review pass. No implementation is authorized by this +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 @@ -58,8 +59,8 @@ 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 intended shape is illustrated below. Names of existing concepts aid discussion; -the complete signatures and fields still need the example pass. +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: @@ -75,11 +76,14 @@ types: ```go // In each initial integration package; Suite belongs to that integration: -func Command[C any](suite Suite[C]) commandkit.Command +func Command[I, S any](suite Suite[I, S]) commandkit.Command ``` -Future integrations can accept declarations suited to their resources while -returning the same core `Command`; the core does not prescribe a universal suite. +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 @@ -94,11 +98,11 @@ may occur then, but its errors remain attached to the declaration for installati to report. Internally fallible assembly can capture an error without returning it to the consumer at every construction step. -The core needs a small supported public mechanism for integrations to carry -construction errors in opaque declarations. Its exact signature remains to be -specified. 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. +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. @@ -151,10 +155,10 @@ 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 a narrow placement declaration mechanism, whose final shape -remains to be specified. It represents placement, not authentication of the -declaring package. Installed standard families cannot be extended with private -application operations or have their handlers replaced. +`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: @@ -189,50 +193,988 @@ 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 invocation input/context factory supplies the zero value of its 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. +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 must stay separate from shared acquisition state. -The value-shaped input with a shared cache pointer in the comparison illustrates -one suitable arrangement. Shallow-copying a shared pointer input and mutating its -selector fields is not sufficient to isolate scope values. The next example pass -must specify this interaction without inventing deep-copy guarantees, banning -pointer contexts, or silently changing factory lifetimes. +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. -## Next example and review pass +## Proposed core declarations -The next step is to complete this proposal's declarations and examples, then -review them before implementation. Focus on the following work: +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. -1. Show a complete core-only registration example with a typed application - command and group. Ordinary declaration construction should follow the same - error-reporting pattern as integration factories, rather than hiding an older - pattern in a fallible helper. -2. Show context-free application configuration through `AppCommand`, including - the loader signature and absence of placeholder state or forwarding wrappers. -3. Show full configuration-scope and definition-suite assembly beside application - commands, with all registration-related setup visible. Demonstrate independent - capability selection without importing an integration into the core. -4. Specify the supported mechanisms for retained declaration errors and root-only - placement, plus their interaction with ordinary groups and explicit no-ops. - Show how integration-owned validation feeds core installation without a public - preparation callback or private-state access. -5. Complete the typed selector/override example, including input initialization, - scope filtering, explicit presence, and shared cache lifetime. Explain how - pointer-shaped inputs preserve scope isolation, and align input factory - semantics with application commands. -6. Include focused examples or expected outcomes for invalid wiring, collisions, - nesting a root-only group, empty suites, declaration reuse, and edits to source - metadata after construction. These are design checks, not a mandate for a new - exhaustive test matrix. -7. Reconcile the selected namespace lifetime with the ADR and architecture policy, - and update the module implementation plan once this proposal's examples and - remaining API details have passed review. +```go +package commandkit -The uniform declaration-and-installation pattern is decided. Exact supporting -API names and the unresolved input-isolation mechanics above remain work for the -next pass. This step adds no code, new inspection operations, or release-cadence -decision. +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.