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

12 KiB

ADR 0001: Own dispatch through an explicit registry and typed inputs

Status

Accepted.

ADR 0011 refines declaration construction, installation, optional-family namespace reservation, and integration input/state lifetimes.

Date

2026-10-06

Context

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

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 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, 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.
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 owns normative boundaries. ADR 0002 selects the backend, and ADR 0003 defines its public/internal boundary.