Finalize the registration API roadmap

This commit is contained in:
2026-10-10 15:44:53 +00:00
parent 892186d4ac
commit 8f86852456
3 changed files with 57 additions and 964 deletions

View File

@@ -12,8 +12,9 @@ should acquire neither ConfigKit nor PromptKit through CommandKit's package or
module dependencies. ConfigKit-only consumers should not acquire PromptKit.
This roadmap records the targeted end state, selected module layout, and
migration work. Recommendations under [Open Decisions](#open-decisions) remain
proposals. It does not describe these changes as implemented.
migration work. The finalized [registration API](registration-api.md) owns the
selected declarations, installation semantics, and representative consumer
examples. Neither document describes these changes as implemented.
[ADR 0010](../adr/0010-optional-integration-modules.md) owns the accepted module
boundary and convenient registration principles; it partially supersedes
[ADR 0003](../adr/0003-public-facade.md), whose remaining principles still apply.
@@ -130,11 +131,16 @@ shapes merely because they already exist.
### Selected API and release principles
- Design convenient integration registration around representative consumers.
Reuse public command/group registration when it fits the final design. Add a
narrow core capability only where a concrete responsibility requires it; do
not introduce a general family framework or preserve private bookkeeping for
its own sake. Exact signatures remain implementation-planning work.
- Use the finalized [uniform registration API](registration-api.md#declaration-and-installation).
Applications and integrations produce captured core command declarations and
install them through core `New` or `App.Register`; construction errors are
retained for installation-time validation. Integration factories provide
complete families and focused conveniences. The API document owns exact
signatures and the [input/shared-state separation](registration-api.md#why-inputs-and-shared-state-have-separate-types).
- Follow the selected [namespace lifetime and reconciliation requirements](registration-api.md#namespace-reconciliation-and-implementation-handoff).
Built-in help/version remain reserved; optional integration roots are owned
when their real family is installed. Complete the identified durable-document
alignment before implementing that lifetime.
- Core diagnostics use a small CommandKit-owned severity type. Upstream
diagnostic conversion belongs in the integrations, preserving selected
classification, delivery, and error semantics without copying ConfigKit's
@@ -188,25 +194,27 @@ needed for CommandKit's source-only releases.
## Work sequence
### 1. Align policy and settle implementation boundaries
### 1. Align policy and prepare the selected API for implementation
Completed: ADR 0010 is accepted and ADR 0003 is marked partially superseded,
with its decision text preserved. The partial status follows the explicitly
authorized exception to the documentation policy for this decision.
- Resolve the remaining choices under Open Decisions before moving public
symbols, using the selected module paths and package names above.
- Align the architecture policy with ADR 0010 and review other
ADRs for affected ownership statements without reopening unrelated decisions.
- Use the finalized [registration API](registration-api.md) and selected module
layout as the implementation target before moving public symbols.
- Align the architecture policy with ADR 0010 and complete the registration
API document's namespace reconciliation. Review other ADRs for affected
ownership statements without reopening unrelated decisions.
- Inventory root exports, imports, private helper use, and family registration
behavior. Use the inventory as evidence of responsibilities and coupling, not
as a list of APIs to preserve. Select final exports and ownership from consumer
needs; no compatibility mapping for the unpublished prototype is required.
- Sketch three representative consumers: core only, ConfigKit only, and both
libraries. Confirm that combined configuration scopes remain straightforward
and that registration meets ADR 0010's convenience principle without replacing
removed boilerplate with new plumbing. Use these examples to evaluate the
registration API; no fixed call count or particular options pattern is selected.
as a list of APIs to preserve. Implement final exports and ownership from the
selected API and consumer needs; no compatibility mapping for the unpublished prototype is required.
- Use the API document's [three representative consumers](registration-api.md#complete-consumer-registration-examples):
core only, ConfigKit only, and both libraries. They define the target wiring,
including combined configuration scopes and independent definition inspection;
port them into maintained examples as implementation lands.
- Prepare a staged implementation plan from this roadmap and the finalized API,
preserving the unreleased-prototype organizing principle above.
Completion: the dependency direction, final API, and registration approach
are concrete enough to implement without inventing a general plugin framework.
@@ -307,22 +315,3 @@ change after extraction unless an implementation dependency justifies combining
them. Catalogs and requirement discovery are subsequent integration features.
Semantic comparison retains the upstream-contract dependency recorded in
[deferred work](deferred.md). None is needed to demonstrate module isolation.
## Open Decisions
### Exact registration API
The design principle is selected: convenient registration through the simplest
coherent public core contracts, without preserving the prototype. Exact
signatures and any additional core capability remain to be settled through the
three representative consumer sketches and an ownership/collision review.
**Recommendation:** integrations construct their standard groups and install them
through ordinary public registration, adding a narrow capability only if the
selected ownership or atomicity behavior cannot be expressed coherently. Remove
redundant private flags and checks rather than reproducing them publicly.
**Alternative:** an explicit public family declaration and installation API can
represent ownership and collision handling if command groups cannot adequately
express those responsibilities. It adds a lasting abstraction, so select it only
for a demonstrated consumer or core responsibility, not to imitate current state.

View File

@@ -1,868 +0,0 @@
# Registration API alternatives
Status: design mockup for discussion; none of the alternatives is implemented or
selected by this document.
## Purpose and authority
Settle the public registration shape for the optional modules described in
[the module roadmap](modules.md), before finalizing its implementation plan.
[ADR 0001](../adr/0001-command-dispatch-and-inputs.md) selects explicit instance
registration, typed inputs, and standard-family ownership.
[ADR 0010](../adr/0010-optional-integration-modules.md) places optional handlers,
providers, and adapters in their owning modules and requires convenient family
registration through supported core contracts.
Design for the best, simplest, most coherent final API. CommandKit is an
unreleased prototype with no consumers; its signatures and private machinery
receive zero deference. Existing code supplies evidence of responsibilities and
can be reused where suitable. Accepted decisions remain authoritative.
The Go fragments below illustrate proposed consumer APIs. They are not runnable
examples, published contracts, or instructions to implement these names exactly.
Application loaders, domain command constructors, imports, and execution code
are omitted where unrelated to registration. No new inspection operations are
proposed here; deferred capabilities remain deferred.
## Common requirements
All alternatives must provide:
- A core-only consumer importing neither integration nor its upstream library.
- Integration-supplied standard commands and adapters; applications supply
acquisition and domain context without constructing each standard handler.
- One `config` suite containing application, PromptKit, and other supported
configuration scopes. Definition inspection remains independently selectable.
- Registration and help that leave acquisition callbacks uninvoked.
- Typed, presence-aware context flags and overrides, with invocation-local state.
- Registration errors for collisions and attempts to modify installed standard
families. A failed registration operation must not partially install its inputs.
- No public Cobra types, root-private registry access, import-time installation,
or ConfigKit/PromptKit dependencies in the core.
The ownership boundary is the complete installed group. There is no public
incremental operation for appending application commands underneath an installed
standard family, or replacing its handlers. Configuration composition happens
before installation through integration-owned scope declarations.
## Shared provider and scope mockup
The alternatives differ in how declarations reach the application registry.
They can use the same integration-owned provider declarations:
| Proposed declaration | Owner | Role |
| --- | --- | --- |
| `Scope[C]`, `Suite[C]`, `Selection` | `ckconfig` | Configuration capabilities, scope composition, and config/overlay inputs |
| `FromConfigKit[C, T]` | `ckconfig` | Adapt an application ConfigKit loader into a configuration scope |
| `ConfigScope[C]` | `ckprompt` | Adapt a PromptKit engine loader into a `ckconfig.Scope[C]` |
| `Suite[C]`, `FromEngine[C]` | `ckprompt` | Definition inspection providers and their engine adapter |
| Typed bindings, diagnostics, command declarations | `commandkit` | Shared command infrastructure, without upstream-library types |
These short names are proposed for consumer readability, rather than preserving
the prototype's root-package names. The two `Suite` types belong to distinct
integration packages and describe distinct resources. No universal suite,
provider, or integration interface is needed.
The illustrative adapter signatures are:
```go
// In ckconfig:
func FromConfigKit[C, T any](
name, summary string,
load func(context.Context, Selection, C) (*configkit.Result[T], error),
) Scope[C]
// In ckprompt:
func ConfigScope[C any](
load func(context.Context, ckconfig.Selection, C) (*promptkit.Engine, error),
) ckconfig.Scope[C]
func FromEngine[C any](
load func(context.Context, ckconfig.Selection, C) (*promptkit.Engine, error),
) Suite[C]
```
Using `ckconfig.Selection` for the PromptKit adapters describes their existing
configuration-selection inputs. It does not move that integration-specific type
back into the core. `ckprompt` already has the selected dependency on `ckconfig`.
These adapters construct declarations without acquiring providers or returning
errors. Option A validates the complete suite in `Register`; Option B validates
it in the complete-group factory; Option C validates it during core installation.
All validate before registry mutation. Invalid wiring, including a nil loader,
must remain detectable in the declaration rather than becoming an invocation-time
failure inside an otherwise non-nil wrapper. Invalid configuration returned by a
valid loader remains an invocation-time failure.
This consolidates declaration errors at a useful assembly boundary. It does not
require every future constructor to be infallible: constructors that perform
genuinely fallible work may still return errors. The existing adapter constructors
only reject nil loaders at construction, so they do not demonstrate that need.
Adapters supply appropriate supported operations. Provider declarations can also
describe supported subsets through typed callbacks, so callers need not enable
every available inspection operation. Advanced composition does not require
exposing individual standard command constructors.
Suites allow application context and explicitly bound selector flags where
needed. Configuration selectors may identify the scopes to which they apply;
definition selectors belong to their own suite. Share core binding primitives,
without forcing PromptKit definition providers to use a configuration-specific
selector wrapper. The exact selector constructors are a separate part of the
typed-input design, not a reason to add a registration framework.
### Consumer assumptions
The simple ConfigKit examples have no domain selectors, overrides, or application
state. Under the current generic sketch, their loader accepts an unused
`struct{}` context argument and their suite uses `struct{}` with no `NewContext`
callback. This makes the remaining type plumbing visible rather than hiding it
behind an `AppInput` type or helper. Whether to offer a context-free loader form
is evaluated below, with a focused convenience function recommended for Option A.
The generic sketches remain visible for comparison; they are not the recommended
shortest setup for that case.
In the composed examples, both loaders accept `*RunState` and all provider and
suite assembly is shown inline. The illustrative `newRunState` callback is
invoked by the selected handler, not during assembly. Application state can hold
an invocation-local cache shared by the selected configuration scopes. Separate
families do not acquire state together merely because both are registered.
Keeping the engine scope while omitting definition registration enables only
PromptKit configuration inspection; keeping definitions while omitting that
scope enables only definition inspection. Importing `ckprompt` enables neither.
`greet` and `sessionGroup` represent already constructed application-owned
commands, whose construction is identical across the alternatives. Their domain
implementation and the loaders' bodies are omitted; no integration assembly or
error handling is hidden in a `makeInspectionSuites` helper.
## Option A: integration convenience functions install complete groups
### Proposed public shape
```go
// Existing core concepts, evaluated for their final shape:
func New(identity Identity, commands ...Command) (*App, error)
func (app *App) Register(commands ...Command) error
// In the integration packages:
func Register[C any](app *commandkit.App, suite Suite[C]) error
```
Each integration's `Register` validates its declaration, assembles its complete
standard group privately, and installs that group through supported public core
registration. Applications do not receive individual standard handlers or need
to declare standard paths. The function name is qualified by its owning package:
`ckconfig.Register` or `ckprompt.Register`.
### Core-only consumer
```go
greet, err := makeGreetCommand() // Application-owned typed command.
if err != nil {
return err
}
app, err := commandkit.New(identity, greet)
if err != nil {
return err
}
// Execute using caller-owned context and streams.
```
Only `commandkit` is imported for command infrastructure. Help/version are
provided by construction; there is no inspection-provider setup.
### ConfigKit-only consumer
```go
app, err := commandkit.New(identity, greet)
if err != nil {
return err
}
if err := ckconfig.Register(app, ckconfig.Suite[struct{}]{
Scopes: []ckconfig.Scope[struct{}]{
ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
},
}); err != nil {
return err
}
```
This consumer imports `commandkit`, `ckconfig`, and the ConfigKit types used by
its loader, with no `ckprompt` or PromptKit dependency.
### Composed consumer
```go
app, err := commandkit.New(identity, sessionGroup)
if err != nil {
return err
}
if err := ckconfig.Register(app, ckconfig.Suite[*RunState]{
NewContext: newRunState,
Scopes: []ckconfig.Scope[*RunState]{
ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
ckprompt.ConfigScope(loadEngine),
},
}); err != nil {
return err
}
definitions := ckprompt.FromEngine(loadEngine)
definitions.NewContext = newRunState
if err := ckprompt.Register(app, definitions); err != nil {
return err
}
```
The configuration suite installs `config` once with both scopes. Definition
registration independently installs the supplied `promptkit` operations.
`session restore` remains an application-owned command inside `sessionGroup`.
### Advantages
- Expresses the consumer task directly: install this integration's standard
commands using these providers.
- Keeps standard assembly behind the integration facade and avoids exposing a
family-builder API solely to support extraction.
- Works with ordinary core command/group concepts, without a new integration
object, callback protocol, or application-wide options language.
- Provider validation and installation are coordinated within one call, without
separate error checks for the declaration-only adapter constructors.
- Each module can evolve its provider declarations independently.
### Costs and limits
- Several integrations require several registration calls and error checks.
- Atomicity covers each call, not the entire application setup. If PromptKit
registration fails after configuration succeeds, the app contains the earlier
registration. The application can discard that unfinished app; no rollback
framework is required for this assembly pattern.
- Standard group construction is not independently available to consumers.
Tests exercise it through registration and the public application unless a
concrete composition need later justifies another surface.
## Option B: integrations return complete command declarations
### Proposed public shape
Keep the same core construction and registration concepts, but expose integration
factories instead of installation functions:
```go
// In the integration packages:
func Command[C any](suite Suite[C]) (commandkit.Command, error)
```
`ckconfig.Command` returns the complete `config` group; `ckprompt.Command` returns
the supplied definition-inspection group. The returned core handle has private
implementation state. It does not expose child mutation or standard handlers.
Factory validation and group construction leave acquisition callbacks uninvoked.
Adapter declarations use the shared error-free constructors; suite wiring errors
are reported by `Command`, while registry collisions and placement errors are
reported by `New` or `Register`.
### Core-only consumer
The core-only sketch from Option A is unchanged. This design adds no required
integration dependency or configuration to core construction.
### ConfigKit-only consumer
```go
configCommand, err := ckconfig.Command(ckconfig.Suite[struct{}]{
Scopes: []ckconfig.Scope[struct{}]{
ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
},
})
if err != nil {
return err
}
app, err := commandkit.New(identity, greet, configCommand)
if err != nil {
return err
}
```
### Composed consumer
```go
configCommand, err := ckconfig.Command(ckconfig.Suite[*RunState]{
NewContext: newRunState,
Scopes: []ckconfig.Scope[*RunState]{
ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
ckprompt.ConfigScope(loadEngine),
},
})
if err != nil {
return err
}
definitions := ckprompt.FromEngine(loadEngine)
definitions.NewContext = newRunState
promptCommand, err := ckprompt.Command(definitions)
if err != nil {
return err
}
app, err := commandkit.New(identity,
configCommand, promptCommand, sessionGroup,
)
if err != nil {
return err
}
```
Alternatively, an existing app can receive the complete declarations in one
`app.Register(configCommand, promptCommand, sessionGroup)` call. Declaration
validation happens before registry mutation, so a collision rejects the whole
batch. No guarantee extends across separate registration calls.
### Advantages
- Uses one declaration model for both standard and domain commands.
- Supports batch registration without new core transaction machinery.
- Applications can assemble a collection of commands before constructing an app.
- Factories can be reused in larger application assembly code without receiving
an app or modifying its registry.
### Costs and limits
- The common task requires constructing and holding additional command handles
before installation, with separate factory and registry errors.
- The public factory makes standard-group composition part of the consumer API.
Documentation must explain that these groups occupy their standard root
namespaces; arbitrary nesting must not redefine standard command paths.
- Returning an ordinary command handle alone does not settle reservation rules.
The registry must still recognize any selected standard-namespace restrictions
rather than assume every valid command may be installed at any location.
## Option C: complete declarations validated during installation
### Proposed public shape
This variant of Option B defers suite validation and fallible group assembly to
core construction or registration:
```go
// In the integration packages:
func Command[C any](suite Suite[C]) commandkit.Command
```
A returned command is an uninstalled, potentially invalid declaration. `New` or
`Register` prepares and validates the complete batch before committing it. The
core needs a supported mechanism for integration-owned validation and assembly
without importing integration types or exposing backend machinery. The concrete
`DeferredGroup` proposal below makes that cost visible; the shorter consumer
syntax does not make that work disappear.
Preparation uses only declarations and metadata. It does not acquire providers
or call application context factories. Installation errors include both invalid
suite wiring and registry conflicts. For `New`, a failure yields no usable app;
for `Register`, it leaves the prior registry intact.
### Core-only consumer
The core-only sketch from Option A remains available. Option C does not require
changing every domain command constructor to defer its own validation.
### ConfigKit-only consumer
```go
app, err := commandkit.New(identity,
greet,
ckconfig.Command(ckconfig.Suite[struct{}]{
Scopes: []ckconfig.Scope[struct{}]{
ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
},
}),
)
if err != nil {
return err
}
```
### Composed consumer
```go
definitions := ckprompt.FromEngine(loadEngine)
definitions.NewContext = newRunState
app, err := commandkit.New(identity,
ckconfig.Command(ckconfig.Suite[*RunState]{
NewContext: newRunState,
Scopes: []ckconfig.Scope[*RunState]{
ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
ckprompt.ConfigScope(loadEngine),
},
}),
ckprompt.Command(definitions),
sessionGroup,
)
if err != nil {
return err
}
```
An existing app can instead receive these declarations in one `Register` call.
No guarantee extends across separate calls.
### Advantages
- Retains Option B's collection and batch-composition capabilities.
- Removes intermediate command handles and complete-group factory error checks.
- Reports invalid wiring and installation conflicts at one installation boundary.
### Costs and limits
- A command handle no longer implies that its integration declaration has passed
validation; consumers must install it to obtain that result.
- The core must support deferred integration validation and assembly while
retaining batch atomicity and dependency independence.
- Standard-root placement and empty declarations still need explicit semantics,
as in Option B.
- Declaration capture and reuse need care: deferring work must not accidentally
let later suite mutations change installed commands or share invocation state.
## Ownership, collisions, and the core boundary
None of the alternatives needs to expose registry maps, an installed-config
boolean, individual handler constructors, or backend objects. Ordinary
registration can reject an occupied root path and commit a fully validated batch
without merging children. That protects installed-family ownership for all three designs.
The accepted ownership rule is clear for installed standard families: application
registration cannot replace them or add private operations underneath them.
[ADR 0001](../adr/0001-command-dispatch-and-inputs.md) also refers to reserved
standard families, while the [architecture policy](../policy/architecture.md)
explicitly describes an installed `promptkit` family. Neither provides a uniform,
explicit lifetime rule for every uninstalled optional family. That interpretation
must be decided deliberately rather than inferred from prototype flags.
**Recommendation:** help/version are reserved by core construction; an optional
standard family claims its complete root namespace when installed. Before that,
a domain group can occupy the name, but a subsequent integration installation
then fails with a collision. Neither registration order can replace a winner.
This avoids a core registry of every possible future integration namespace and
does not prohibit core-only applications from using an otherwise absent family
name. It preserves ownership throughout an installed family's lifetime.
**Alternative:** reserve the known standard names before integration installation.
This catches domain misuse earlier but constrains applications that never install
those integrations and requires the core to recognize the reserved names and an
installation distinction. If selected, a narrow standard-group constructor can
mark integration declarations; do not expose private registry state.
Before implementation, align the roadmap, policy, and ADR interpretation with
the selected lifetime rule. If it changes an accepted decision, record that
change through the ADR process; this mockup does not authorize weakening it.
Option A assembles and registers its group at the root internally. B and C expose
handles, so they additionally need enforceable root placement. For B, a proposed
`commandkit.RootGroup(name, summary, children...) (Command, error)` returns an
opaque group declaration that normal `New`/`Register` accepts at the root and
rejects as a child of another group. It adds one public core constructor for a
concrete placement need, without exposing its handlers or creating a second
`Family` type. C's deferred constructor carries the same root placement metadata.
Moving a returned standard declaration under `tools`, for example, is an assembly
error rather than silently producing `app tools config show`.
This public distinction represents placement, not proof of a caller's package
identity. Do not invent package-origin authentication. An explicit `Family` type
remains viable if a concrete responsibility eventually warrants a separate
model, but it is not needed merely to relocate packages.
## Validation and declaration ownership
The following timing distinguishes the proposals; none shifts declaration errors
to command execution:
| Boundary | Option A | Option B | Option C |
| --- | --- | --- | --- |
| Scope/provider adapter | Record declaration | Record declaration | Record declaration |
| Integration suite validation | Integration `Register` | Integration `Command` | Core `New`/`Register` through integration-owned preparation |
| Registry and placement checks | Core registration | Core `New`/`Register` | Core `New`/`Register` |
| Provider acquisition | Selected handler | Selected handler | Selected handler |
Recommended ownership behavior: successful installation captures the declaration
metadata it needs, so later edits to suite fields or declaration slices do not
change installed commands. Application callbacks and the runtime objects they
capture retain their documented application-owned lifecycle; this is not a
promise to deep-copy arbitrary application state. For B and C, the proposal below
makes capture happen when a declaration factory is called; installation does not
read mutable caller-owned declaration slices.
The empty-suite proposal below distinguishes a configuration suite with no
scopes from a definition suite with no capabilities. B and C use an explicit
no-op declaration for the latter; an invalid zero-value command remains an error.
## Comparison
| Consideration | Option A: `Register` | Option B: validated `Command` | Option C: deferred `Command` |
| --- | --- | --- | --- |
| Typical consumer task | Supply suite and install family | Supply suite, receive validated group, then install | Supply suite as a declaration to core installation |
| Integration exports for registration | One convenience function | One complete-group factory | One deferred-declaration factory |
| Suite errors reported at | Integration registration | Group construction | Core installation |
| Domain commands | Ordinary core declarations | Same | Same; infallible constructors not required |
| Atomicity | Each integration registration call | Each core registration batch | Each core registration batch, including preparation |
| Standard assembly visible to callers | Integration-owned | Complete group handle exposed | Deferred group handle exposed |
| Pre-assembly of mixed command collections | Separate domain declarations and family calls | Directly supported | Directly supported |
| Additional core mechanism | Ordinary groups/registration under the recommended reservation rule | Root-placement declaration and explicit no-op | Deferred preparation, root placement, and explicit no-op |
| Dependency isolation and lazy acquisition | Supported | Supported | Supported |
## Recommendation
Prefer **Option A: integration-owned `Register` functions with typed suites and
declaration-only adapters, plus the focused `RegisterApp` convenience below**.
Rank Option C second and Option B third.
The representative consumers benefit from installing standard behavior directly;
they do not demonstrate a need to collect standard command handles or roll back
all family registrations together. Per-call atomicity and discarding a failed
application setup are sufficient for these sketches.
Use ordinary group construction and registry collision handling wherever they
satisfy the selected ownership rules. Remove redundant prototype bookkeeping.
Adopt the proposed installed-family reservation rule only after its deliberate
policy/ADR reconciliation. With that rule, A needs no extra core family or
preparation abstraction. Add only capabilities serving a selected responsibility.
Do not ship both `Register` and `Command` merely to cover every possible assembly
style. Options B and C are credible alternatives if pre-assembly or batch
installation becomes a demonstrated consumer requirement. C reduces consumer
error-handling boilerplate but adds deferred preparation to the core contract;
B offers earlier validation with more explicit assembly steps. All preserve the
same module and capability boundaries.
Once this proposal is accepted, update the owning module roadmap with the chosen
public registration shape and reservation semantics. Its staged implementation
plan can then specify exact declarations and validation without asking the coding
agent to invent a family framework. Prototype-compatible names, intermediate
buildability, and mechanical file relocation are not selection criteria.
## Additional design work and proposed resolutions
These proposals address the requested follow-up areas. They are recommendations
for review, not new accepted contracts. Select their semantics before producing a
decision-complete implementation plan.
### 1. Registration choice and the full setup cost
Retain the ranking **A, C, B**. All three generic sketches include real adapter,
scope, and suite assembly. A's common case improves further with the focused
entry point below. C has the shortest mixed declaration expression but needs a
new public preparation contract, root-placement handling, and no-op representation.
B exposes early validation and batch composition at the cost of more intermediate
handles and error checks. None needs a transaction spanning separate calls.
### 2. A context-free application configuration entry point
**Recommendation for A:** add a single focused convenience function:
```go
// In ckconfig:
func RegisterApp[T any](
app *commandkit.App,
load func(context.Context, Selection) (*configkit.Result[T], error),
) error
```
Complete registration call site, assuming `greet` is already declared:
```go
app, err := commandkit.New(identity, greet)
if err != nil {
return err
}
if err := ckconfig.RegisterApp(app, loadAppConfig); err != nil {
return err
}
```
Here `loadAppConfig` accepts only `context.Context` and `ckconfig.Selection`.
`RegisterApp` adapts it internally, declares the standard `app` scope, and uses
the same validation and installation path as `Register`. A nil original loader
remains a declaration error. The default scope summary can be a plain application
configuration description; callers needing a different summary, additional
scopes, selectors, or state use `Register` with a typed suite.
This is an alternative entry point for a demonstrated common case, not an options
language or a second scope model. It does not install a suite to which more
scopes are appended later. An application needing composition selects the full
suite entry point from the start.
**Alternative:** keep only generic `Register`. A context-free loader can use the
following explicit wrapper and declaration, with no additional export:
```go
if err := ckconfig.Register(app, ckconfig.Suite[struct{}]{
Scopes: []ckconfig.Scope[struct{}]{
ckconfig.FromConfigKit("app", "Application settings.",
func(ctx context.Context, selection ckconfig.Selection, _ struct{}) (*configkit.Result[AppConfig], error) {
return loadAppConfig(ctx, selection)
}),
},
}); err != nil {
return err
}
```
That is viable but makes simple consumers repeat plumbing unrelated to their
application. Prefer the one focused export; do not automatically add analogous
conveniences for every provider or family. If B or C is chosen, evaluate an
`AppCommand` factory for this same use case instead of adding A's registration
entry point to a factory-based design.
### 3. Empty suites and intentional no-op declarations
**Recommendation:** distinguish absence of resources from absence of an optional
family's operations, and validate supplied declarations before treating them as
empty.
| Declaration | Proposed result |
| --- | --- |
| Config suite with no scopes | Declaration error: there is no configuration resource to inspect |
| Named config scopes with no inspection operations | Install `config scopes`; report those scopes and their actual empty capability sets |
| Config scopes with some supplied operations | Install discovery and the operations supplied by at least one scope |
| Definition suite with no capabilities and otherwise valid wiring | Successful no-op; no `promptkit` namespace or help entry installed |
| Adapter constructed with a nil loader | Declaration error, even if its wrappers or selected capabilities would otherwise look empty |
Not calling a registration function remains the simplest way to omit a family.
The no-op case supports capability-driven assembly without requiring a special
conditional in every consuming application. It does not reserve an absent
namespace under the proposed installed-family rule.
For B and C only, add an explicit proposed `commandkit.NoCommand() Command`
representation. Core `New`/`Register` skips it. A zero-value `Command` remains
invalid. B returns the explicit no-op after validating an empty definition suite;
C's preparation returns it after validation. Invalid wiring is never swallowed
as a no-op. A needs neither a public no-op handle nor that extra export.
**Alternatives:** omit all empty suites successfully, or install configuration
discovery even with no scopes. Both avoid the configuration error, but may hide
missing resource wiring or expose a namespace with nothing to discover. Requiring
at least one definition capability is also viable, but adds conditional assembly
for an intentionally optional family. These are product choices, not necessities
inherited from the prototype.
### 4. Namespace reservation and placement
Prefer the installed-family lifetime rule described above. It protects complete
standard groups while keeping unused names available to core-only consumers.
Confirm or revise the relevant policy/ADR interpretation explicitly before
implementation. Pre-installation reservation remains a viable alternative; its
cost and required installation distinction are described above.
For B and C, enforce standard-root placement in the core declaration metadata,
not only in documentation. An ordinary wrapper group must reject a standard
root-only child during assembly/registration. A keeps standard handles private
and performs its complete root registration inside the integration function.
### 5. Declaration capture, installation, and reuse
**Recommendation:** A captures registration metadata when `Register` is called;
B and C capture it when their `Command` factory is called. Capture includes
scope names and descriptions, capability lists, selector applicability, bindings,
and callback references. Copy declaration collections and nested metadata
collections needed for these semantics, rather than retaining the caller's
mutable slices. Application callback closures and the state they reference stay
application-owned; this is not a deep-copy guarantee for arbitrary values.
After `declaration := ckconfig.Command(suite)`, changing `suite.Scopes` or a scope's
operation list does not change that declaration. C's deferred preparation reads
its captured declaration, not the later contents of `suite`. Invalid recorded
wiring also stays attached to that captured declaration.
B and C declarations may be installed in more than one application. Each registry
captures its own installed metadata and each invocation receives its own parsed
state. Callback references may remain shared, and consumer closures may share
state deliberately. This does not promise concurrent dispatch or make caller
state safe for concurrent use. A can likewise receive the same suite in separate
registration calls; each call captures the suite as it exists at that time.
**Alternative:** C could capture at installation. That permits editing a suite
through retained references until installation but makes a command handle's
meaning depend on subsequent mutations and installation timing. Factory-time
capture provides a clearer declaration model without freezing application state.
### 6. Declaration validation and Option C preparation
**Recommendation:** declaration-only adapters retain the original loader or a
private construction issue in their owning integration declarations. Validators
inspect that information before using generated wrappers. A non-nil wrapper must
not make an originally nil loader appear valid. No public invalid-loader flag,
root-private accessor, or provider invocation is needed.
For example, `ckprompt.FromEngine(nilLoader)` records invalid wiring; it is not
an empty suite. A deliberately absent capability is an absent callback in a
manually composed, otherwise valid suite, or an explicitly supported capability
selection. A zero definition suite expresses intentional absence. An upstream
configuration or named lookup error from a valid callback remains an invocation
result; it is not a registration error.
Errors identify the family and, where applicable, the scope, capability, or flag.
For example, `config scope "app": loader is nil` and
`promptkit: engine loader is nil` are illustrative diagnostic content, not exact
string contracts. Preserve wrapped error identity where a fallible declaration
operation supplies an error. Validate scope uniqueness, supported capability
wiring, selector declarations, and reserved-flag collisions from metadata.
Integrations validate their own provider and scope metadata; core command/binding
construction validates opaque binding handles and command/flag conflicts. Wrap
those errors with the integration context rather than inspecting private flag
fields or duplicating parser validation. This does not require a new public
registry inspection or binding bridge just for declaration validation.
Do not validate files, credentials, or application configuration during assembly.
If C is selected, its concrete proposed public preparation mechanism is:
```go
// In commandkit; needed only for Option C:
func DeferredGroup(
name, summary string,
prepare func() (Command, error),
) Command
```
This is a root-only deferred command declaration, not a startup hook. Its opaque
handle carries root identity and the preparation callback. Integration factories
capture the typed suite and supply their own metadata-only preparation function:
```go
func Command[C any](suite Suite[C]) commandkit.Command {
captured := captureSuiteDeclaration(suite) // Integration-private metadata copy.
return commandkit.DeferredGroup("config", "Inspect configuration.",
func() (commandkit.Command, error) {
// Validate captured wiring and assemble the complete standard group.
// No acquisition callback or NewContext callback is invoked here.
return prepareConfigGroup(captured)
})
}
```
`captureSuiteDeclaration` and `prepareConfigGroup` identify integration-owned
responsibilities; they are not additional public exports. Capturing metadata can
record an invalid declaration without returning an error. Preparation reports
that error at installation.
Core `New`/`Register` prepares the entire supplied batch before mutation, checks
resolved declarations and placement against each other and the existing registry,
and commits only after success. A nil preparation callback or a prepared group's
root differing from its declared root is an assembly error. A deliberate no-op
is permitted; malformed declarations are not. Failure leaves the registry intact,
but cannot undo arbitrary effects inside a caller-supplied callback. Standard
integration preparation functions themselves are responsible for remaining
metadata-only. Registering the same declaration in another app may prepare it
again; no cross-application preparation cache or exactly-once promise is needed.
**Alternative for C:** store a captured wiring error and an already assembled
neutral group in a command declaration, reporting the error only at installation.
This can avoid a general preparation callback if all necessary assembly can be
completed without error-returning work. Its viability depends on the final core
constructors; it must not discard validation failures or expose an untyped payload.
C's callback form is the concrete mockup here, not a requirement to choose C.
### 7. Typed customization and absent context factories
**Recommendation:** an omitted `NewContext` supplies the zero value of `C` for
the selected invocation. For a struct, that is a fresh zero-valued struct; for
`*RunState`, it is nil. Do not silently allocate arbitrary pointer contexts or
reject every pointer-context declaration. A loader may intentionally accept nil.
Applications whose binding callbacks or loaders require allocated state supply
`NewContext`. A supplied factory returning nil has the same caller-owned meaning;
this is not a promise to inspect or repair arbitrary callback state.
The following realistic A sketch uses a value input with a shared cache pointer.
All integration assembly is visible. `strconv.Atoi`, the string parser, and core
bindings are illustrative public binding primitives; their final names do not
constrain the registration choice.
```go
type RunInput struct {
Campaign commandkit.Supplied[string]
Limit commandkit.Supplied[int]
Cache *RunCache
}
campaignFlag := commandkit.ValueFlag[RunInput, string](
commandkit.FlagMeta{Name: "campaign", Usage: "Select campaign inputs."},
nil,
func(raw string) (string, error) { return raw, nil },
func(input *RunInput, value commandkit.Supplied[string]) {
input.Campaign = value
},
)
limitFlag := commandkit.ValueFlag[RunInput, int](
commandkit.FlagMeta{Name: "limit", Usage: "Override the application's item limit."},
nil, strconv.Atoi,
func(input *RunInput, value commandkit.Supplied[int]) {
input.Limit = value
},
)
newInput := func() RunInput {
return RunInput{Cache: newRunCache()}
}
app, err := commandkit.New(identity, sessionGroup)
if err != nil {
return err
}
if err := ckconfig.Register(app, ckconfig.Suite[RunInput]{
NewContext: newInput,
Selectors: []ckconfig.Selector[RunInput]{
{Binding: campaignFlag, Scopes: []string{"app", "promptkit"}},
{Binding: limitFlag, Scopes: []string{"app"}},
},
Scopes: []ckconfig.Scope[RunInput]{
ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
ckprompt.ConfigScope(loadEngine),
},
}); err != nil {
return err
}
definitions := ckprompt.FromEngine(loadEngine)
definitions.NewContext = newInput
definitions.Selectors = []commandkit.Flag[RunInput]{campaignFlag}
if err := ckprompt.Register(app, definitions); err != nil {
return err
}
```
Configuration selectors carry configuration-scope applicability. Definition
selectors are plain typed flags for that family's input; they do not need a
list containing a fictitious configuration scope. Each loader in this sketch
receives `RunInput`; registration and help call neither loader nor `newInput`.
For `app config show --scope all --campaign spring --limit=0`, both scopes receive
the campaign selector, but only the `app` scope receives the limit override.
Its loader checks `input.Limit.Set` before applying `input.Limit.Value`; explicit
zero overrides an inherited limit, while omission leaves it unchanged. An explicit
`--campaign=` similarly remains supplied with an empty value. The bindings retain
presence rather than inferring it from nonzero values.
Scope filtering uses binding metadata so a selector not applicable to a scope
is not copied into that scope's input. It does not erase shared acquisition state
such as `Cache`, or require deep-copying application state. Selecting only a scope
to which a supplied selector does not apply does not add a new rejection rule:
the selector simply is not delivered to that scope. If a stricter unused-selector
policy is desired, decide it separately rather than inventing it in registration.
For `app promptkit show profile briefing --campaign spring`, definition inspection
receives the campaign input and fresh invocation state; it does not receive the
configuration-only limit flag or invoke the application configuration scope.
**Alternative:** require a context factory for every stateful suite. That makes
initialization explicit but adds boilerplate for value inputs that work correctly
with zero values. Prefer the zero-value default plus an explicit factory when
actual application state needs one.
## Decisions still requiring approval
The requested design work is now concrete, but these proposals remain unapproved:
1. Choose A with `RegisterApp`, C, or B.
2. Confirm empty configuration errors, discovery-only named scopes, and empty
definition no-ops; B/C additionally need explicit no-op declarations.
3. Confirm installed-family namespace reservation and reconcile policy/ADR wording,
or choose pre-installation reservation and its narrow core distinction.
4. Confirm declaration capture timing and reuse, and the proposed separation from
caller-owned state and concurrent-dispatch guarantees.
5. Confirm the zero-value context default and the typed customization contracts.
If C is chosen, additionally select its preparation mechanism; if B is chosen,
select the root-placement constructor. Those extra exports are conditional and
are not additions to the recommended A API.

View File

@@ -1,9 +1,8 @@
# Unified command registration proposal
# Unified command registration API
Status: direction agreed; declarations, consumer examples, and design checks are
prepared for final review. Supporting API names and input/state separation below
are proposed details of that direction. No implementation is authorized by this
document alone.
Status: finalized API design; implementation is pending. The declarations,
consumer examples, and design checks below define the selected target API.
No implementation is authorized by this document alone.
## Purpose and authority
@@ -12,11 +11,9 @@ them through the same core API. Installation validates each batch before registr
mutation. Integrations supply complete standard families and focused conveniences
while retaining ownership of their provider semantics.
This document records the selected direction following the
[registration alternatives](registration-alternatives.md). It replaces that
comparison's recommendation of Option A with a refinement of Option C: capture
declarations and any construction errors eagerly, then report errors at
installation. The comparison remains background, not the selected API proposal.
This document defines the selected registration API: capture declarations and
any construction errors eagerly, then report errors at installation. The
completed alternatives comparison is retained in Git history.
[ADR 0001](../adr/0001-command-dispatch-and-inputs.md) owns explicit registration,
typed inputs, and command ownership.
@@ -24,7 +21,7 @@ typed inputs, and command ownership.
boundaries and convenient integration registration. The
[module roadmap](modules.md) tracks extraction work. Reconcile the namespace
lifetime decision below with the relevant ADR and architecture-policy wording
before implementation; this proposal does not silently rewrite those records.
before implementation; this API design does not silently rewrite those records.
Design for the best consumer API without preserving current implementation
signatures or machinery. Existing code can supply evidence and reusable work,
@@ -60,7 +57,8 @@ or installed. Ordinary command, group, scope, and suite adapter construction
does not require a separate error check at each call site.
The uniform construction shape is below. The complete declarations and examples
later in this document provide the supporting fields and semantics for review.
later in this document provide the supporting fields and semantics for
implementation.
```go
// Core package:
@@ -80,7 +78,7 @@ func Command[I, S any](suite Suite[I, S]) commandkit.Command
```
Here `I` is the suite's CLI input and `S` is its shared invocation state. This
proposed separation resolves the pointer-isolation issue without a clone hook or
selected separation resolves the pointer-isolation issue without a clone hook or
an application-wide type. Future integrations can accept declarations suited to
their resources while returning the same core `Command`; the core does not
prescribe a universal suite.
@@ -98,11 +96,10 @@ may occur then, but its errors remain attached to the declaration for installati
to report. Internally fallible assembly can capture an error without returning it
to the consumer at every construction step.
Use the proposed `WithError(command, err)` core operation to retain integration
Use the selected `WithError(command, err)` core operation to retain integration
construction errors in opaque declarations. Its complete signature and no-op
interaction are specified below. Do not substitute an untyped integration payload,
root-private access, or a general preparation callback. The proposed
`DeferredGroup` lifecycle from the comparison is not selected.
root-private access, or a deferred preparation lifecycle.
`New` and `Register` report retained declaration errors and validate the batch,
including bindings, root placement, and collisions, before registry mutation.
@@ -155,7 +152,7 @@ merges an existing group.
Standard-family declarations carry enforceable root-only placement metadata.
Installing them beneath an ordinary group is an assembly error. Use the same
`Command` type with the proposed `RootGroup` constructor specified below. It
`Command` type with the selected `RootGroup` constructor specified below. It
represents placement, not authentication of the declaring package. Installed
standard families cannot be extended with private application operations or have
their handlers replaced.
@@ -200,14 +197,14 @@ factory when initialized state is needed. A supplied factory returning nil retai
its application-owned meaning.
Scope-specific parsed values stay separate from shared acquisition state through
the proposed `Suite[I, S]`: `NewInput` initializes each selected scope's input,
the selected `Suite[I, S]`: `NewInput` initializes each selected scope's input,
while `NewState` initializes `S` once for the selected inspection invocation.
Providers receive both explicitly. Pointer inputs are independently initialized,
not shallow copies of a single pointer. The detailed lifetime table below makes
this change from the comparison's mixed context model explicit; it does not
require arbitrary deep copies or prohibit pointer inputs or shared pointer state.
## Proposed core declarations
## Selected core declarations
The following declarations specify the public registration and binding surface
used by these examples. Opaque types retain unexported representation; these are
@@ -485,7 +482,7 @@ inspection applies the whole captured flag set once. This explains the public
seam; private input structs and the exact assembly algorithm remain implementation
choices.
## Proposed configuration integration declarations
## Selected configuration integration declarations
The `ckconfig` package owns the following contracts. Only the four implemented
inspection operations appear here; comparison remains deferred. `config scopes`
@@ -606,7 +603,7 @@ arguments nor demands a forwarding wrapper from the caller. A nil loader becomes
a retained `config`/`app` declaration error. The full suite is used for composition;
there is no incremental scope append operation after installation.
## Proposed PromptKit integration declarations
## Selected PromptKit integration declarations
The `ckprompt` package uses `ckconfig` selections and scope contracts through its
selected module dependency. Definition selectors are plain typed flags; they do
@@ -674,10 +671,10 @@ custom inspection callback; this does not redefine upstream version semantics.
### Why inputs and shared state have separate types
The comparison used one context type holding both selector fields and a cache.
Combining selector fields and a cache in one context type obscures their lifetimes.
A shared pointer to that object cannot isolate selector fields between scopes.
Cloning arbitrary application graphs would add a promise CommandKit cannot make.
The proposed `Suite[I, S]` makes the actual two responsibilities explicit:
The selected `Suite[I, S]` makes the actual two responsibilities explicit:
- `I`: per-scope/per-command CLI input, initialized and populated independently.
- `S`: shared acquisition state for one selected inspection invocation.
@@ -685,8 +682,8 @@ The proposed `Suite[I, S]` makes the actual two responsibilities explicit:
There is no public clone hook and no reflection-based allocation of pointer inputs.
Two type parameters cost some declaration syntax in composed consumers, but make
sharing deliberate and lifetimes predictable. `AppCommand` hides both for the
simple case. This is a supporting API detail proposed for final review, not a
change to unified construction or the independently owned suite principle.
simple case. This selected separation supports unified construction and
independently owned suites.
### Factory timing
@@ -735,7 +732,7 @@ an invocation cache. No concurrent-dispatch guarantee follows from these rules.
## Complete consumer registration examples
These examples target the proposed APIs, not today's prototype. Each block is a
These examples target the selected APIs, not today's prototype. Each block is a
complete consumer assembly function with imports and input declarations. Domain
loaders and domain execution are supplied as typed function parameters where
appropriate: their bodies are application behavior, not hidden registration
@@ -1059,7 +1056,7 @@ plus discovery. It does not invent comparison or catalog providers.
## Focused design checks and expected outcomes
These checks specify observable design results for review, not a requirement to
These checks specify observable target behavior, not a requirement to
add an exhaustive new test matrix. They use the declarations above; `identity`
has a valid name. Declaration errors are returned Go errors from installation,
not invocation status codes. Provider failures and ordinary input errors retain
@@ -1215,40 +1212,15 @@ This clarifies an ambiguity rather than assuming that earlier broad reservation
wording already specifies the same lifetime. The durable documents need deliberate
alignment before implementation:
| Owner | Required alignment after final proposal review |
| Owner | Required alignment before implementation |
| --- | --- |
| ADR 0001 | Clarify the reservation lifetime through the ADR change process if its accepted wording is interpreted as unconditional reservation |
| ADR 0010 | Retain core/integration dependency and convenient-composition boundaries; exact factories now return captured declarations |
| Architecture policy | State installed-family ownership uniformly for config and promptkit; align its public facade rules with ADR 0010 |
| Module roadmap | Replace the open registration alternative with this selected uniform model and reviewed supporting signatures |
| Staged module implementation plan | Specify eager capture, error retention, neutral bindings, scope isolation, examples, and release adoption using the reviewed API |
| Staged module implementation plan | Specify eager capture, error retention, neutral bindings, scope isolation, examples, and release adoption using this finalized API |
A new or superseding ADR can record the lifetime clarification where needed;
accepted decision text is not revised silently. This authoring pass updates only
this proposal, leaving policy/ADR reconciliation and the staged plan for the
explicit post-review handoff requested above. It does not claim those documents
are already aligned or authorize implementation.
## Review checklist
The requested declaration/example work is now present:
1. Core-only typed command/group construction with a single installation boundary.
2. Context-free `AppCommand` with the complete loader signature and no dummy state.
3. Visible composed suite assembly, application command, and independent capability
selection.
4. Concrete `WithError`, `RootGroup`, and `NoCommand` contracts, with no delayed
preparation or private-state access.
5. Typed selector composition with stable binding identity, scope-aware validation,
usage/wiring error categories, distinct pointer inputs, and one shared
invocation cache through `NewState`, with normal and failure-path timing.
6. Focused expected outcomes for invalid wiring, collisions, placement, emptiness,
capture, and reuse.
7. An explicit durable-document reconciliation and post-review implementation-plan
handoff, without prematurely editing accepted records or implementing code.
Final review should particularly consider the proposed `Suite[I, S]` separation
and the narrow `CaptureFlags`/`FlagValues` composition seam. These are now concrete
supporting API choices rather than unresolved isolation mechanics. The uniform
declaration model is selected; these names and details await the user's final
review before implementation.
accepted decision text is not revised silently. The [module roadmap](modules.md)
now references this finalized API. Policy/ADR reconciliation and a staged
implementation plan remain pending; this document does not claim that those
records are already aligned or authorize implementation.