13 KiB
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 document alone.
Purpose and authority
Applications and integrations construct captured command declarations and install them through the same core API. Installation validates each batch before registry mutation. Integrations supply complete standard families and focused conveniences while retaining ownership of their provider semantics.
This document records the selected direction following the registration alternatives. It replaces that comparison's recommendation of Option A with a refinement of Option C: capture declarations and any construction errors eagerly, then report errors at installation. The comparison remains background, not the selected API proposal.
ADR 0001 owns explicit registration, typed inputs, and command ownership. ADR 0010 owns optional module boundaries and convenient integration registration. The module roadmap tracks extraction work. Reconcile the namespace lifetime decision below with the relevant ADR and architecture-policy wording before implementation; this proposal does not silently rewrite those records.
Design for the best consumer API without preserving current implementation signatures or machinery. Existing code can supply evidence and reusable work, but does not constrain the design. This is future API design, not documentation of implemented behavior.
Selected consumer model
A consumer can use only the core or add any supported combination of integrations.
Every command reaches the application through core New or Register, regardless
of whether an application or integration declared it. There is no mandatory
integration interface, application-wide generic input type, or separate
integration installation lifecycle.
The core remains independent of integration modules and their upstream libraries.
Integrations may depend on one another as documented. Such module dependencies
do not install commands or acquire providers. The selected modules remain
CommandKit, CommandKit/integrations/ckconfig, and
CommandKit/integrations/ckprompt, under the existing repository module prefix.
Each application command retains its own typed input and domain handler.
Integration suites retain their own typed context and provider declarations.
Both produce the same opaque core Command handle. Consumers need not construct
individual standard handlers, duplicate integration adapters, or expose backend
objects to assemble an application.
Declaration and installation
One declaration model
A Command is a captured declaration, not proof that the declaration is valid
or installed. Ordinary command, group, scope, and suite adapter construction
does not require a separate error check at each call site.
The intended shape is illustrated below. Names of existing concepts aid discussion; the complete signatures and fields still need the example pass.
// 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:
// In each initial integration package; Suite belongs to that integration:
func Command[C any](suite Suite[C]) 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.
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.
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.
New and Register report retained declaration errors and validate the batch,
including bindings, root placement, and collisions, before registry mutation.
A failed New yields no usable application. A failed Register leaves the
existing registry intact. No rollback guarantee spans separate registration calls.
Declaration construction, installation, help, and version do not invoke provider acquisition or invocation input/context factories. They do not validate files, credentials, or application configuration. Invalid wiring fails at installation; an upstream error returned by a valid provider remains an invocation result.
An originally nil loader remains invalid wiring even if an adapter would wrap it in a non-nil function. Integration declarations retain enough information to detect this. An intentionally absent capability is distinct from an invalid adapter. Errors identify the affected command family and relevant scope, binding, or capability and preserve supported wrapped error identity; exact prose is not selected here.
Integration composition and conveniences
Configuration scopes are composed before installation into one configuration suite. Application and engine scopes can share that suite. Named definition inspection remains independently selectable: installing an engine configuration scope does not automatically install definition commands, or vice versa.
Keep provider declarations integration-owned. Configuration supports ConfigKit for now; the core does not acquire a universal configuration or suite interface. The PromptKit integration can use the ConfigKit integration's selection and scope contracts through its selected dependency.
Provide the focused ckconfig.AppCommand(load) convenience for a context-free
application configuration loader. It declares the standard app scope, with a
default description, through the same declaration model as the full suite. It
does not require an unused context argument or Suite[struct{}] at the consumer
call site. A loader still receives context.Context and configuration selection.
Use the full suite when adding scopes, selectors, custom state, or other metadata.
Do not initially expose integration-specific Register or RegisterApp
functions alongside declaration factories. Core registration is the installation
path for all consumers. Additional conveniences should address demonstrated
consumer needs rather than mirror every underlying constructor automatically.
Namespaces and empty declarations
Help and version are reserved by core construction. Optional families claim their complete root namespace when installed. Before installation, a domain group may occupy that name; subsequent integration installation then fails with a collision. The reverse order also rejects a collision. Neither order replaces or merges an existing group.
Standard-family declarations carry enforceable root-only placement metadata.
Installing them beneath an ordinary group is an assembly error. Use the same
Command type with 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.
The selected empty-suite behavior is:
| Declaration | Installation result |
|---|---|
| Configuration suite with no scopes | Declaration error |
| Named configuration scopes with no inspection operations | Install scope discovery with the actual empty capability sets |
| Configuration scopes with supplied operations | Install discovery and supported operations |
| Otherwise valid definition suite with no capabilities | Successful no-op; no namespace or help entry |
| Adapter with a nil loader | Declaration error, even when selected capabilities otherwise appear empty |
Use an explicit commandkit.NoCommand() declaration for intentional absence.
Core installation skips it. A zero-value Command remains invalid. Integration
factories validate or retain wiring errors before representing an empty family
as a no-op; absence must not swallow errors. A no-op does not reserve a namespace.
Capture, reuse, and invocation inputs
Capture scope metadata, capability lists, selector applicability, bindings, and callback references when declarations are constructed. Copy the declaration collections needed for those semantics. Later edits to the source suite or its metadata slices do not change a returned command declaration.
Declarations may be installed in multiple applications. Registries maintain independent installed state, and invocations have independent parsed state. Application callback closures and objects they reference remain application-owned and may deliberately be shared. These choices do not promise arbitrary deep copies or concurrent dispatch.
Use the same typed, presence-aware binding primitives for application commands and integration inputs. Configuration selectors additionally declare scope applicability; definition selectors need no configuration-specific wrapper. Explicit empty, false, and zero values remain distinguishable from omission.
An omitted 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.
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.
Next example and review pass
The next step is to complete this proposal's declarations and examples, then review them before implementation. Focus on the following work:
- 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.
- Show context-free application configuration through
AppCommand, including the loader signature and absence of placeholder state or forwarding wrappers. - 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.
- 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.
- 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.
- 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.
- 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.
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.