Files
commandkit/docs/roadmap/registration-alternatives.md

385 lines
16 KiB
Markdown
Raw Normal View History

# Registration API alternatives
Status: design mockup for discussion; neither alternative 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
Both 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], error)
// In ckprompt:
func ConfigScope[C any](
load func(context.Context, ckconfig.Selection, C) (*promptkit.Engine, error),
) (ckconfig.Scope[C], error)
func FromEngine[C any](
load func(context.Context, ckconfig.Selection, C) (*promptkit.Engine, error),
) (Suite[C], error)
```
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`.
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.
For the composed sketches below, `makeInspectionSuites` represents this wiring:
```go
appScope, err := ckconfig.FromConfigKit(
"app", "Application settings.", loadAppConfig,
)
if err != nil {
return err
}
engineScope, err := ckprompt.ConfigScope(loadEngine)
if err != nil {
return err
}
configSuite := ckconfig.Suite[*RunState]{
NewContext: newRunState,
Scopes: []ckconfig.Scope[*RunState]{appScope, engineScope},
}
definitions, err := ckprompt.FromEngine(loadEngine)
if err != nil {
return err
}
definitions.NewContext = newRunState
```
Here the loaders accept `*RunState`. 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 `engineScope` while omitting definition registration enables only
PromptKit configuration inspection; keeping definitions while omitting that
scope enables only definition inspection. Importing `ckprompt` enables neither.
## 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
scope, err := ckconfig.FromConfigKit(
"app", "Application settings.", loadAppConfig,
)
if err != nil {
return err
}
app, err := commandkit.New(identity, greet)
if err != nil {
return err
}
if err := ckconfig.Register(app, ckconfig.Suite[AppInput]{
Scopes: []ckconfig.Scope[AppInput]{scope},
}); 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
configSuite, definitions, err := makeInspectionSuites()
if err != nil {
return err
}
app, err := commandkit.New(identity, sessionGroup)
if err != nil {
return err
}
if err := ckconfig.Register(app, configSuite); err != nil {
return err
}
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.
- 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.
### 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
scope, err := ckconfig.FromConfigKit(
"app", "Application settings.", loadAppConfig,
)
if err != nil {
return err
}
configCommand, err := ckconfig.Command(ckconfig.Suite[AppInput]{
Scopes: []ckconfig.Scope[AppInput]{scope},
})
if err != nil {
return err
}
app, err := commandkit.New(identity, greet, configCommand)
if err != nil {
return err
}
```
### Composed consumer
```go
configSuite, definitions, err := makeInspectionSuites()
if err != nil {
return err
}
configCommand, err := ckconfig.Command(configSuite)
if err != nil {
return err
}
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.
## Ownership, collisions, and the core boundary
Neither alternative 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 both designs.
Evaluate any additional reservation requirements against the accepted ownership
decision, not prototype flags or incidental checks. If a reserved namespace must
be unavailable to ordinary application declarations before its integration is
installed, group immutability and duplicate checks alone do not express that
rule. A narrow core installation distinction may then be justified. Both designs
can accommodate it, and the final proposal should identify the exact rule it
serves before selecting its public signature. Do not invent package-origin
authentication or a general plugin framework to solve registration ownership.
An explicit `Family` handle with separate creation and installation is also
possible. It would distinguish standard families from domain groups, but adds a
second public declaration model. Adopt it only if a concrete reservation or
composition responsibility requires that distinction; module extraction alone
does not. This is a conditional extension to the alternatives, not a prerequisite.
## Comparison
| Consideration | Option A: `Register` | Option B: `Command` factory |
| --- | --- | --- |
| Typical consumer task | Supply suite and install family | Supply suite, receive group, then install |
| Integration exports for registration | One convenience function | One complete-group factory |
| Domain commands | Ordinary core declarations | Same |
| Atomicity | Each integration registration call | Each core registration batch |
| Standard assembly visible to callers | Integration-owned | Complete group handle exposed |
| Pre-assembly of mixed command collections | Separate domain declarations and family calls | Directly supported |
| Additional core family abstraction | Only for a demonstrated reservation need | Same |
| Dependency isolation and lazy acquisition | Supported | Supported |
## Recommendation
Choose **Option A: integration-owned `Register` functions with typed suites**.
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.
Resolve the reservation distinction above before finalizing core signatures;
add only the capability its actual responsibility requires.
Do not ship both `Register` and `Command` merely to cover every possible assembly
style. Option B is a credible alternative if pre-assembly or batch installation
becomes a demonstrated consumer requirement. Selecting it would still 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.