Files
commandkit/docs/adr/0011-captured-declarations-and-installation.md

11 KiB

ADR 0011: Capture command declarations and install optional families explicitly

Status

Accepted. Refines ADR 0001 for declaration construction, installation, optional-family namespace reservation, and integration input/state lifetimes. Partially supersedes ADR 0010 only for its consumer-migration consequence during the unreleased prototype transition, and refines its convenient composition principle.

Date

2026-10-10

Context

Optional integration modules need to assemble complete standard families through public core contracts. Applications also declare their own commands and groups. Different construction and installation paths would add repeated error checks and registration plumbing, weakening CommandKit's infrastructure-reduction goal. The current implementation is an unreleased prototype with no downstream consumers. Its declarations are evidence to evaluate, without compatibility obligations that would constrain the selected API.

Declaring an optional integration is distinct from installing it. Broad references to reserved standard families in ADR 0001 do not specify when an optional name becomes reserved. Dependency selection alone should not consume an application namespace or install commands.

Configuration inspection can select several scopes. Those scopes may share an application-owned cache while receiving different selector values. Combining selector storage and shared state in one context obscures their distinct lifetimes and can cause scope-specific inputs to alias. Separate input and state types make those lifetimes explicit. Integrations also need to compose typed bindings without accessing private core parsers and setters or exposing backend structures.

Decision

Uniform captured declarations

Applications and integrations construct the same opaque core command declarations and install them through core application construction or registration. Initial integrations provide complete-family factories and focused conveniences; they do not add separate integration-specific installation APIs. Suite/provider types remain integration-owned rather than becoming a universal core provider model.

Factories capture metadata, bindings, capability selections, selector applicability, and callback references eagerly. They copy the declaration collections needed so later edits to the source metadata do not change a returned command. Construction errors remain attached to the declaration and are reported at installation. There is no public deferred preparation lifecycle or untyped integration payload.

Core installation validates the whole batch before registry mutation, including retained errors, bindings, placement, and collisions. Failed application construction returns no usable application; failed later registration leaves the existing registry intact. This atomicity applies to one installation call, not separate calls or arbitrary effects in caller code. Report retained errors with useful command context and preserve supported wrapped error identity without requiring a particular aggregate-error representation.

An explicit absence declaration contributes no command, help entry, or namespace claim. Installation checks its attached errors before skipping it. A zero command declaration remains invalid. Integrations define their own empty-suite semantics: the initial configuration suite needs at least one named scope, while a valid definition suite with no capabilities is absent. Metadata-only configuration scopes remain discoverable. A nil loader supplied to an adapter is invalid wiring; narrowing its capability list does not erase that error.

Declarations may be installed in multiple applications with independent registry and invocation state. Callback references and the objects they capture remain application-owned; declaration reuse neither deep-copies those objects nor establishes concurrent-dispatch support. Application helpers performing other fallible work may still return errors.

Optional-family namespace lifetime and placement

Core construction always reserves help and version. An optional standard family owns its complete root namespace when a real family declaration is installed. An import or absent declaration reserves nothing. An application may use that name while the family is uninstalled, but later installation then fails with a collision. Application registration after standard-family installation fails in the same way. Neither order merges groups, replaces handlers, or permits private application operations beneath an installed standard family.

Standard-family declarations carry root-only placement metadata. Core rejects their installation below another group. Placement is a public declaration property, not authentication of the declaring package or a core allowlist of integration names. A real empty root-only group still claims its namespace and obeys placement checks; explicit absence does neither.

Separate typed input from shared acquisition state

Integration suites distinguish typed CLI input from typed shared acquisition state. Configuration inspection initializes and populates one input per selected scope, applying only applicable selector bindings, and initializes shared state once for the selected inspection invocation. Named definition inspection uses one input and one shared state. Providers receive both explicitly. Configuration input validation receives the selected scope identity together with its applicable typed input.

Availability preflight and applicable input checks precede shared-state initialization and acquisition. A failed check stops that path; later input factories need not run. Construction, installation, help, version, and scope discovery invoke neither input/state factories nor acquisition providers.

An omitted factory supplies its type's zero value, including nil for a pointer. Applications supply initialization when their accessors or providers need it. CommandKit does not allocate arbitrary pointer types, clone arbitrary application graphs, repair aliases returned by factories, or reject a provider that accepts nil intentionally. Core-owned parsed collections remain independent when applied to separate inputs. Caller-owned objects and callback effects retain their own lifetimes.

Provide a narrow typed core binding-composition contract to capture converted values and apply a selected binding subset to an initialized input. Preserve binding identity, defaults, and presence without string-based handler lookup or exposure of private setters, parser state, or backend objects. Copying a binding preserves its identity; constructing a lookalike binding does not.

Execution-required checks apply to the selected subset, so an app-only selector does not become required for an engine-only scope request. Missing required input is a usage failure. An invalid binding selection or destination is a wiring failure, classified as operational when encountered at invocation. Those categories remain identifiable through supported wrapping; assembly-detectable metadata defects remain installation errors. Both invocation failures stop before shared-state initialization and acquisition. This composition is part of the existing typed-input model, not another parser or a raw-argument handler API.

Relationship to earlier decisions

No compatibility wrappers or downstream migration guide are required for this unreleased prototype transition. This replaces ADR 0010's consumer-migration consequence for this transition only. Changes to published contracts remain governed by the release policy.

ADR 0001 continues to govern dispatcher ownership, explicit instance registries, typed bindings, presence, and application/domain boundaries. This decision clarifies that optional-family namespace ownership begins upon installation. It refines the construction and installation mechanics left open there and distinguishes integration input from shared state. It does not change the grammar, report formats, supported operation inventory, or outcome/delivery rules in ADRs 0004 through 0009.

ADR 0010 continues to govern separate core/integration module contracts and dependency direction. Integration factories return core declarations through supported public composition contracts. Deliberate exports, private implementation state, and backend isolation remain governed by ADR 0003 as scoped by ADR 0010. Exact exported signatures belong in Go declarations and GoDoc when implemented; this record owns the architectural choices rather than an implementation inventory.

Alternatives considered

Alternative Analysis
Fallible constructors plus integration-specific registration helpers Makes each construction failure immediate, but creates several assembly conventions and repeated consumer checks. Captured errors keep one installation boundary.
Deferred preparation callbacks Allows installation to construct a family, but adds lifecycle and error timing beyond eager capture. No demonstrated assembly need requires it.
Reserve all known optional family names at core construction Prevents later name conflicts, but consumes namespaces for integrations the application has not selected. Installed-family ownership keeps conflicts explicit without that restriction.
Separate family/plugin registration framework Could encode ownership and composition, but a captured command with root-only placement and batch validation expresses the required behavior without another installation model.
One shared input/state object Uses fewer type parameters and can share a cache through references in separate input values, but leaves the distinct lifetimes implicit and scope-input isolation to application conventions. Explicit input and state types make those lifetimes visible.
Clone hooks or automatic pointer allocation Can hide input construction, but adds object-graph and initialization responsibilities that belong to the application. Explicit factories keep those choices visible.
Public binding getters/setters or an untyped value bag Enables integration assembly, but exposes private mechanics or returns typed-input assembly to string lookup. Narrow typed composition addresses the actual need.

Consequences

Consumers assemble domain commands and optional families visibly through one installation boundary. Integrations remain convenient without private core access. Errors identified during eager assembly are observed at installation, before provider acquisition.

Installing an optional family can fail if the application already uses its root name; the application decides which declaration to install. Installed standard families retain their ownership and root placement in either registration order.

Composed suites use two types and explicit initialization when needed. The context-free application-configuration convenience hides unused suite machinery for simple consumers. The public typed-composition seam adds a deliberate core contract while keeping implementation and backend state private.

The architecture policy owns normative boundaries. Implementation status and sequencing remain in the roadmaps. These choices govern CommandKit's declarations and calls; application loading, cache design, startup, domain validation, callback effects, and process exit remain consumer choices.