Files
commandkit/docs/adr/0001-command-dispatch-and-inputs.md

206 lines
12 KiB
Markdown
Raw Permalink Normal View History

# ADR 0001: Own dispatch through an explicit registry and typed inputs
## Status
Accepted.
## Date
2026-10-06
## Context
2026-10-06 19:36:02 -05:00
Several downstream consumers of the ConfigKit and PromptKit libraries include
duplicated or near-duplicated code relating to configuration inspection,
routing, parsing, help, and error handling. However, their command vocabulary
2026-10-06 19:36:02 -05:00
and syntax have diverged. CommandKit aims to provide a single, unified CLI
implementation that will reduce duplication, provide consistent vocabulary and
grammar, and reduce the amount of code each individual application must own.
Applications have genuine domain commands, such as Narratio's `session restore`,
that remain outside the standard suite. Merely forwarding standard families from
separate application dispatchers would leave custom-command mechanics and root
help dependent on each application's implementation.
Input handling also needs a common boundary. Raw argv handlers or string-based
flag retrieval leave repeated parsing and input assembly with applications.
Presence matters for configuration overrides: omitted values differ from
explicitly supplied empty strings, false, and zero.
## Decision
### Ownership and namespace structure
CommandKit owns the dispatcher, shared parsing and presence tracking, help,
syntax diagnostics, and standard-family handlers and outcomes. Applications own
custom command names, domain meaning, validation, effects, artifacts, and
operational outcomes.
2026-10-06 19:36:02 -05:00
Use namespace-first syntax for the standard suite, which provides a clear
family ownership boundary. The initial standard command families are `config`
for ConfigKit inspection and validation, and `promptkit` for PromptKit inspection
and utilities. CommandKit integrates these libraries through their supported public
2026-10-06 19:36:02 -05:00
semantics.
CommandKit accepts custom commands outside its reserved standard families and
reports registration errors for attempts to replace standard handlers or add
private operations beneath them. Its input extension mechanism accepts context
selectors and typed overrides for standard operations.
CommandKit accepts caller-supplied context, streams, handlers, and inspection
providers and returns invocation outcomes. It leaves process termination to the
caller. Executable startup, signals, runtime construction, schemas, acquisition,
2026-10-06 19:36:02 -05:00
and domain resolution remain application choices.
### Explicit registry and capability installation
Each explicitly constructed CommandKit instance owns a registry. Its API accepts
individual commands and composable groups. Registration uses explicit instance
state with caller-controlled construction and composition.
Definitions provide paths, descriptions, flags and defaults, repetition and
presence metadata, operand requirements, and execution handlers. One definition
supplies routing, parsing, and help. Duplicate paths and attempts to replace
reserved commands return registration errors; order does not select a winner.
Help and version are installed automatically using supplied application identity
and build metadata. CommandKit assembles standard families from declared provider
capabilities, installing their supported handlers and metadata together. Each
family defines its own resource model, supported verbs, and per-operation data
contracts. Configuration scopes declare their available inspection operations.
The API allows supported capabilities to be integrated independently. Adding
comparison, catalogs, or other inspection features extends the installed surface
according to the supplied providers. Shared verbs have consistent purposes across
families, while the available subsets reflect their resources and capabilities.
Help and discovery expose those subsets. Registration validates the wiring of
declared capabilities and reports unsupported selections before acquisition.
CommandKit registration validates declarations with acquisition callbacks left
uninvoked. Its construction, help, and version paths use metadata. The selected
handler requests dependencies when its operation needs them. Invalid configuration
returned by an acquisition callback is an invocation-time failure. Caller code
executed before registration and effects inside callbacks remain caller-owned.
CommandKit installs handlers for supplied capabilities and describes actual
availability. Each enabled operation retains its standard meaning and output
contract. The exact operation inventory and remaining command contracts are
separate design decisions.
### Typed inputs with explicit bindings
The public binding contract accepts application-defined input structs for custom
commands and uses CommandKit-defined inputs for standard handlers. Explicit
registration binds flags and operands to fields and supplies parsing and help
metadata. CommandKit delivers parsed typed inputs to registered handlers.
Bindings support ordinary typed fields and presence-aware fields where omission
has a separate meaning. CommandKit records supplied state independently of values
and defaults, preserving explicitly supplied empty, false, and zero values.
CommandKit keeps parsed values, collections, and presence from one invocation out
of subsequent invocations. Storage allocation and reuse are implementation
choices. Handler closures and application state retain their own lifecycle;
concurrent dispatch is a separate contract.
The invocation API supplies context and streams separately from CLI inputs.
CommandKit creates its declarations and bindings from metadata and defers its
acquisition calls to the selected handler. The binding system accepts domain value
parsers; domain interpretation and validation belong to registered callbacks.
The public factory, binding, registration, and handler signatures and presence
type names remain API design choices. No illustrative declaration establishes
an exact public API.
### Outcomes and domain behavior
Resolve the command and handle help/version before loading application state.
For execution, prepare independent invocation inputs, apply shared parsing and
operand checks, then invoke the typed handler. Return its invocation outcome to
the application.
CommandKit classifies shared usage errors and standard-family outcomes. Its
handler result contract accepts application-specific outcomes and preserves their
supplied classification. Scriptorium output validation and Notarius artifact
outcomes, for example, remain domain results. CommandKit translates results from
explicit classification and emits usage guidance for usage failures.
Common mechanics do not require a common success-report format for custom
commands. The application owns its artifact and operational output contracts.
Exact result/error interfaces and unresolved grammar choices are separate design
work, not approvals implied by this decision.
### Contract scope and consumer guidance
This decision governs CommandKit's public contracts and implemented behavior.
Each operation specifies the inputs, result semantics, callback lifetimes, and
metadata its provider supplies. Consumers choose how to implement those callbacks
and how to structure loading, execution, startup, and process status mapping.
Sharing acquisition and resolution between inspection and execution, and keeping
acquisition focused on producing inspection views, are recommended practices.
CommandKit supports those patterns through reusable inputs and lazy provider
hooks. Its API preserves declared results and coordinates its own calls; arbitrary
application internals remain application-owned.
Cobra isolation applies to CommandKit's exports and internal adapter. Independent
consumer use of another CLI library remains an application choice. Additional
families and source models can use capability contracts appropriate to their
resources, with standard meanings preserved for operations that are installed.
## Alternatives considered
### Dispatcher and ownership
| Alternative | Analysis |
| --- | --- |
| Application-owned dispatcher forwarding standard families | Small integration surface and convenient migration, but custom syntax, root help, and errors still need cross-dispatcher coordination; removes less infrastructure. |
| Standard definitions mounted into an application's framework | Reuses routing infrastructure, but application configuration can preserve inconsistent behavior outside mounted families. Internal framework reuse remains compatible with this decision. |
| Shared metadata/help with separate routing | Unifies discovery but adds synchronization among metadata, routing, and parsing. One dispatcher avoids it. |
| Both full dispatch and embeddable family handlers | Serves established external CLI architectures but adds integration surfaces and maintenance. Add embedding only for a concrete need. |
| Raw-argument handlers for custom commands | Eases migration but leaves parsing and help responsibilities with applications. A future transition adapter would need to describe its separate parsing and help contract. |
| Universal application runtime framework | Could absorb startup and operational services, but expands beyond the demonstrated CLI boundary and domain ownership. |
2026-10-06 19:36:02 -05:00
| Verb-first standard paths | Groups actions, but distributes namespace ownership among individual routes. Namespace/resource-first paths keep family ownership clear. |
| Reserve root `prompt` and `profile` for PromptKit | Provides short resource paths but consumes application namespaces. A single `promptkit` family gives the library one ownership boundary. |
### Registration
| Alternative | Analysis |
| --- | --- |
| Declarative tree passed to construction | Makes assembly visible together; larger applications still need group composition. May become a convenience over the same registry. |
| Constructor options for every command/group | Compact setup, but excessive options can obscure commands versus application settings. Option conventions remain API design work. |
| Global registry populated by `init` | Less entry-point code, but hidden import effects, shared mutable state, and unclear ownership. Explicit instances avoid those costs. |
### Handler inputs
| Alternative | Analysis |
| --- | --- |
| Parsed-value accessor object | Uniform handlers, but string-based lookup, runtime type errors, and repeated assembly remain. |
| Individual typed parameters | Clear for small commands; grows into long signatures and adapter machinery. Typed structs scale more naturally. |
| Reflection and struct tags | Compact declarations, but adds a tag language, field-discovery rules, and runtime declaration validation. Explicit bindings keep declarations visible. |
| Typed command objects | Can organize complex handlers. CommandKit's invocation-state contract remains separate from caller-owned object lifetimes; objects are an application option. |
| Generated bindings/adapters | Can combine compact authoring and typing, but adds tooling and generated-file maintenance without demonstrated need. |
| Raw arguments | Simple routing integration, but returns common parsing and help to applications. |
## Consequences
One command system removes repeated routing, parser setup, help assembly, and
shared error plumbing as well as standard inspection handlers. Domain handlers
and their declarations remain recognizable application code.
The public registration surface is larger than family delegation, and explicit
bindings require connecting declarations to the correct fields. Those costs
support consistent mechanics without requiring reflection or generated code.
Shared parsing defects can affect every adopter. Consumer and backend integration
evidence must protect selected semantics, including ownership conflicts, help
without configuration, presence, independent invocation inputs, cancellation,
and domain outcomes.
CommandKit releases document intentional compatibility changes. Coordinated
application documentation and subprocess-caller updates are recommended migration
practices.
The [architecture policy](../policy/architecture.md) owns normative boundaries.
[ADR 0002](0002-cobra-backend.md) selects the backend, and
[ADR 0003](0003-public-facade.md) defines its public/internal boundary.