Compare integration registration API alternatives
This commit is contained in:
384
docs/roadmap/registration-alternatives.md
Normal file
384
docs/roadmap/registration-alternatives.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user