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

10 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.

Refined by ADR 0012 for two-selection configuration comparison and its input/state lifetime. The single-selection lifecycle and other decisions remain in force.

Date

2026-10-10

Context

Applications declare domain commands, while optional integrations assemble standard command families. A common construction and installation model reduces repeated error checks and registration plumbing. Integrations need public typed composition contracts to assemble those families while preserving core and backend boundaries.

Declaration and installation have different responsibilities. An optional family claims a namespace when installed; selecting a dependency alone should leave the application's command surface unchanged. During execution, scope-specific CLI inputs and application-owned shared state also have distinct lifetimes. Making those lifetimes explicit avoids accidental aliasing between selected scopes.

At the time of this decision, the implementation was an unreleased prototype with no downstream consumers. Its API shapes carried no compatibility obligations that would constrain the selected design.

Decision

Declaration capture and reuse

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 through that common installation API. Suite and provider types remain integration-owned.

Factories eagerly capture metadata, bindings, capability selections, selector applicability, and callback references. They copy the declaration collections needed to isolate returned commands from later edits to source metadata. Construction errors remain attached for installation to report. This model uses eager declarations with public typed composition; a deferred preparation lifecycle and untyped integration payload are outside the selected API.

Declarations may be reused across applications with independent registry and invocation state. Callback references and captured application objects retain caller ownership. Reuse provides no deep-copy or concurrent-dispatch guarantee. Application helpers performing other fallible work may still return errors.

Installation and retained errors

Core validates each installation batch before registry mutation, checking retained errors, bindings, placement, and collisions. Failed application construction returns no usable application; failed later registration preserves the existing registry. Atomicity covers one installation call. Separate calls and arbitrary callback effects retain their own lifetimes.

Report retained errors with useful command context and preserve supported wrapped error identity. The aggregate-error representation remains an implementation choice.

An explicit absence declaration contributes no command, help entry, or namespace. Installation checks 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, and metadata-only scopes remain discoverable. A definition suite with no capabilities contributes nothing.

Binding validation applies to generated command declarations. Unused selector bindings in an absent family are ignored; explicitly retained adapter-construction errors still reach installation. A nil loader supplied to an adapter remains invalid wiring even if its capabilities are subsequently cleared.

Namespace ownership and placement

Core construction reserves help and version. An optional standard family owns its complete root namespace upon installation. Imports and absent declarations claim nothing. An application may occupy an uninstalled family name; installing a conflicting declaration then fails in either registration order. Installation preserves existing handlers and group ownership. Private application operations remain outside installed standard families.

Standard families carry root-only placement metadata, which core enforces when they are nested beneath another group. Placement is a public declaration property; its enforcement is independent of package identity or a list of integration names. A real empty root-only group claims its namespace and obeys placement checks. Explicit absence contributes no node to place.

Input and shared-state lifetimes

Integration suites distinguish typed CLI input from typed shared acquisition state. Ordinary configuration inspection initializes one input per selected scope, applies its applicable selector bindings, and initializes shared state once for the inspection invocation. Input validation receives the scope identity and its typed input. Named definition inspection uses one input and one shared state. Providers receive both explicitly. ADR 0012 refines this lifecycle for comparison pairs with two selections of one scope.

Availability preflight and applicable input checks precede shared-state initialization and acquisition. Failures may stop later input factories. Construction, installation, help, version, and scope discovery leave suite input/state factories and acquisition providers uninvoked.

An omitted factory supplies its type's zero value, including nil pointers. Applications provide initialization needed by their accessors and providers; providers may intentionally accept nil. Allocation of application objects, cloning, and aliases returned by factories remain caller responsibilities. Core-owned parsed collections stay independent when applied to separate inputs. Caller-owned objects and callback effects retain their own lifetimes.

Typed binding composition

Core provides a narrow typed contract to capture converted values and apply a selected binding subset to initialized input. It preserves binding identity, defaults, and presence while keeping private setters, parser state, and backend objects behind their existing boundaries. A copied binding retains its identity; a newly constructed lookalike has its own identity.

Execution-required checks apply to the selected subset. Missing required input is a usage failure; invalid binding selection or destination is a wiring failure with operational classification at invocation. These categories remain identifiable through supported wrapping and stop execution before shared-state initialization and acquisition. Metadata defects detectable during assembly of generated commands remain installation errors under the validation boundary above.

Relationship to earlier decisions

For the unreleased prototype transition covered by this decision, compatibility wrappers and a downstream migration guide were unnecessary. This exception to ADR 0010's migration consequence is limited to that transition. Changes to published contracts follow the release policy.

ADR 0001 retains dispatcher, registry, typed-input, and application-ownership principles. ADR 0010 governs module boundaries and dependency direction, with ADR 0003 governing deliberate exports and backend isolation within that scope. ADRs 0004 through 0009 retain their operation, grammar, report, and outcome decisions. Go declarations and GoDoc own exact implemented signatures.

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 through one installation boundary. Eager assembly errors reach that boundary before provider acquisition. Namespace collisions remain explicit choices for the application to resolve.

Separate input and state types make initialization and sharing visible. Focused conveniences support simple applications, while typed composition gives integrations the public contracts needed to assemble their families.

The architecture policy owns normative boundaries; roadmaps own implementation status and sequencing. Application loading, cache design, startup, domain validation, callback effects, and process exit remain consumer responsibilities.