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

188 lines
11 KiB
Markdown

# ADR 0011: Capture command declarations and install optional families explicitly
## Status
Accepted. Refines [ADR 0001](0001-command-dispatch-and-inputs.md) for declaration
construction, installation, optional-family namespace reservation, and integration
input/state lifetimes. Partially supersedes
[ADR 0010](0010-optional-integration-modules.md) 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](../policy/release.md).
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](0003-public-facade.md) 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](../policy/architecture.md) 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.