5.8 KiB
ADR 0003: Own public contracts through a root facade and internal CLI adapter
Status
Accepted.
Date
2026-10-06
Context
CommandKit needs room to change implementation while consumers retain stable registration, input, and invocation contracts. Exposing backend assembly or extension structures would let applications depend on details that maintainers may later replace.
ConfigKit, PromptKit, and ReleaseKit demonstrate facades that own public types and delegate through private state and explicit conversion. ConfigKit also translates upstream YAML parser structures into an independent internal document model. ReleaseKit has a second facade for a distinct lightweight runtime responsibility; CommandKit has no corresponding requirement for another public package initially.
The Cobra selection is compatible with dependency isolation. Public API independence needs a genuine contract boundary, and keeping backend structures out of shared internal representations also makes future replacement easier.
Decision
Use the module root commandkit as the sole initial supported public facade.
Define CommandKit's public named types there and expose only selected operations,
extension contracts, and documented methods. Handles keep implementation state in
private fields.
Implementation packages belong under Go's internal/ boundary. Public code
forwards inward with explicit wrappers and conversions; internal production
packages do not import the root. Shared internal contracts live inside that
boundary. Additional public packages require a deliberate decision based on a
durable consumer responsibility.
Do not alias public types to internal or backend types, anonymously embed those types to promote methods, or expose implementation bridge methods. Exported fields, signatures, variables, callbacks, interface methods, constraints, and method sets must not reveal internal or Cobra/pflag structures. Loosely typed values and returned errors must not provide indirect backend-object access.
Standard contracts such as context.Context and io.Writer remain appropriate.
ConfigKit or PromptKit public types may be selected as deliberate integration
dependencies. Application input types remain application-owned. Go declarations
and GoDoc own exact exported contracts.
Confine Cobra/pflag types and operations to an internal adapter. Registry logic, standard handlers, and shared definitions use CommandKit-owned representations. The adapter translates paths, descriptions, bindings, presence, callbacks, and selected help/outcome semantics into backend machinery. Do not reproduce Cobra's complete API through renamed wrappers.
Typed input factories and handlers can cross the facade through private callbacks without internal packages importing public declarations. Preserve fresh input and backend state per invocation. Introduce narrow internal interfaces where actual engine/adapter interaction justifies them; exact package names and splits remain implementation choices.
Use one concrete backend initially. There is no public backend interface, plugin registry, or requirement for a second implementation. Replacement is a future maintainer change.
Translate backend-owned syntax failures into CommandKit diagnostics and outcomes. Preserve useful public identity for application handler errors and cancellation according to the invocation contract. Explicit conversions must preserve value ownership, callback lifetime, presence, and supported error semantics.
Alternatives considered
| Alternative | Analysis |
|---|---|
| Keep all machinery in the public root | Less forwarding, but harder consumer navigation and easier accidental public expansion. |
| Alias or embed implementation types | Less conversion code, but couples public type identity, fields, or method sets to internal choices. |
| Hide Cobra only behind the public facade | Hides types from applications but allows backend structures to spread through internal logic, complicating replacement. |
| Mirror Cobra's API with wrappers | Removes dependency names while preserving its conceptual coupling and compatibility surface. |
| Public packages for registry, bindings, backend, and other mechanisms | Adds supported imports and assembly contracts without a distinct consumer responsibility. |
| Selectable/pluggable backends immediately | Adds selection and extension machinery before a concrete need exists. |
Consequences
Consumers depend on deliberate root contracts while internal algorithms, collaborators, and package boundaries can evolve. Useful application registration capabilities still need a proportionate API; minimizing accidental exports must not recreate repetitive consumer infrastructure.
Explicit forwarding and conversion require maintenance. Type identity, ownership, method sets, callback lifetime, and error semantics must be protected by evidence, not inferred from package relocation.
Compiled public API checks protect aliases, fields, signatures, constraints, and promoted methods. Import checks keep the backend in its adapter and internal production code independent of root. External-package tests protect consumer semantics through the real adapter. ConfigKit and ReleaseKit provide precedents for these checks without requiring their full validation workflows.
Backend replacement can preserve the public Go API but must also preserve selected CLI behavior or follow the release policy for intentional incompatibilities. Backend defaults do not independently establish new CommandKit contracts.
The architecture policy owns normative boundaries. The facade roadmap owns implementation status, package layout illustrations, and pending work. ADR 0001 records the dispatcher/input model, and ADR 0002 selects the initial backend. The CLI roadmap retains individual command approval status.