Refine registration alternatives and list remaining decisions

This commit is contained in:
2026-10-10 14:10:59 +00:00
parent eeb9fdd66c
commit d9f60d2d5a

View File

@@ -1,6 +1,6 @@
# Registration API alternatives # Registration API alternatives
Status: design mockup for discussion; neither alternative is implemented or Status: design mockup for discussion; none of the alternatives is implemented or
selected by this document. selected by this document.
## Purpose and authority ## Purpose and authority
@@ -26,7 +26,7 @@ proposed here; deferred capabilities remain deferred.
## Common requirements ## Common requirements
Both alternatives must provide: All alternatives must provide:
- A core-only consumer importing neither integration nor its upstream library. - A core-only consumer importing neither integration nor its upstream library.
- Integration-supplied standard commands and adapters; applications supply - Integration-supplied standard commands and adapters; applications supply
@@ -70,22 +70,35 @@ The illustrative adapter signatures are:
func FromConfigKit[C, T any]( func FromConfigKit[C, T any](
name, summary string, name, summary string,
load func(context.Context, Selection, C) (*configkit.Result[T], error), load func(context.Context, Selection, C) (*configkit.Result[T], error),
) (Scope[C], error) ) Scope[C]
// In ckprompt: // In ckprompt:
func ConfigScope[C any]( func ConfigScope[C any](
load func(context.Context, ckconfig.Selection, C) (*promptkit.Engine, error), load func(context.Context, ckconfig.Selection, C) (*promptkit.Engine, error),
) (ckconfig.Scope[C], error) ) ckconfig.Scope[C]
func FromEngine[C any]( func FromEngine[C any](
load func(context.Context, ckconfig.Selection, C) (*promptkit.Engine, error), load func(context.Context, ckconfig.Selection, C) (*promptkit.Engine, error),
) (Suite[C], error) ) Suite[C]
``` ```
Using `ckconfig.Selection` for the PromptKit adapters describes their existing Using `ckconfig.Selection` for the PromptKit adapters describes their existing
configuration-selection inputs. It does not move that integration-specific type configuration-selection inputs. It does not move that integration-specific type
back into the core. `ckprompt` already has the selected dependency on `ckconfig`. 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 Adapters supply appropriate supported operations. Provider declarations can also
describe supported subsets through typed callbacks, so callers need not enable describe supported subsets through typed callbacks, so callers need not enable
every available inspection operation. Advanced composition does not require every available inspection operation. Advanced composition does not require
@@ -98,38 +111,29 @@ without forcing PromptKit definition providers to use a configuration-specific
selector wrapper. The exact selector constructors are a separate part of the selector wrapper. The exact selector constructors are a separate part of the
typed-input design, not a reason to add a registration framework. typed-input design, not a reason to add a registration framework.
For the composed sketches below, `makeInspectionSuites` represents this wiring: ### Consumer assumptions
```go The simple ConfigKit examples have no domain selectors, overrides, or application
appScope, err := ckconfig.FromConfigKit( state. Under the current generic sketch, their loader accepts an unused
"app", "Application settings.", loadAppConfig, `struct{}` context argument and their suite uses `struct{}` with no `NewContext`
) callback. This makes the remaining type plumbing visible rather than hiding it
if err != nil { behind an `AppInput` type or helper. Whether to offer a context-free loader form
return err is still open; these examples do not settle that API.
}
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 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 invoked by the selected handler, not during assembly. Application state can hold
an invocation-local cache shared by the selected configuration scopes. Separate an invocation-local cache shared by the selected configuration scopes. Separate
families do not acquire state together merely because both are registered. families do not acquire state together merely because both are registered.
Keeping `engineScope` while omitting definition registration enables only Keeping the engine scope while omitting definition registration enables only
PromptKit configuration inspection; keeping definitions while omitting that PromptKit configuration inspection; keeping definitions while omitting that
scope enables only definition inspection. Importing `ckprompt` enables neither. 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 ## Option A: integration convenience functions install complete groups
### Proposed public shape ### Proposed public shape
@@ -169,18 +173,14 @@ provided by construction; there is no inspection-provider setup.
### ConfigKit-only consumer ### ConfigKit-only consumer
```go ```go
scope, err := ckconfig.FromConfigKit(
"app", "Application settings.", loadAppConfig,
)
if err != nil {
return err
}
app, err := commandkit.New(identity, greet) app, err := commandkit.New(identity, greet)
if err != nil { if err != nil {
return err return err
} }
if err := ckconfig.Register(app, ckconfig.Suite[AppInput]{ if err := ckconfig.Register(app, ckconfig.Suite[struct{}]{
Scopes: []ckconfig.Scope[AppInput]{scope}, Scopes: []ckconfig.Scope[struct{}]{
ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
},
}); err != nil { }); err != nil {
return err return err
} }
@@ -192,17 +192,21 @@ its loader, with no `ckprompt` or PromptKit dependency.
### Composed consumer ### Composed consumer
```go ```go
configSuite, definitions, err := makeInspectionSuites()
if err != nil {
return err
}
app, err := commandkit.New(identity, sessionGroup) app, err := commandkit.New(identity, sessionGroup)
if err != nil { if err != nil {
return err return err
} }
if err := ckconfig.Register(app, configSuite); err != nil { 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 return err
} }
definitions := ckprompt.FromEngine(loadEngine)
definitions.NewContext = newRunState
if err := ckprompt.Register(app, definitions); err != nil { if err := ckprompt.Register(app, definitions); err != nil {
return err return err
} }
@@ -220,7 +224,8 @@ registration independently installs the supplied `promptkit` operations.
family-builder API solely to support extraction. family-builder API solely to support extraction.
- Works with ordinary core command/group concepts, without a new integration - Works with ordinary core command/group concepts, without a new integration
object, callback protocol, or application-wide options language. object, callback protocol, or application-wide options language.
- Provider validation and installation are coordinated within one call. - 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. - Each module can evolve its provider declarations independently.
### Costs and limits ### Costs and limits
@@ -250,6 +255,9 @@ func Command[C any](suite Suite[C]) (commandkit.Command, error)
the supplied definition-inspection group. The returned core handle has private the supplied definition-inspection group. The returned core handle has private
implementation state. It does not expose child mutation or standard handlers. implementation state. It does not expose child mutation or standard handlers.
Factory validation and group construction leave acquisition callbacks uninvoked. 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 ### Core-only consumer
@@ -259,14 +267,10 @@ integration dependency or configuration to core construction.
### ConfigKit-only consumer ### ConfigKit-only consumer
```go ```go
scope, err := ckconfig.FromConfigKit( configCommand, err := ckconfig.Command(ckconfig.Suite[struct{}]{
"app", "Application settings.", loadAppConfig, Scopes: []ckconfig.Scope[struct{}]{
) 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 { if err != nil {
return err return err
@@ -280,14 +284,18 @@ if err != nil {
### Composed consumer ### Composed consumer
```go ```go
configSuite, definitions, err := makeInspectionSuites() configCommand, err := ckconfig.Command(ckconfig.Suite[*RunState]{
if err != nil { NewContext: newRunState,
return err Scopes: []ckconfig.Scope[*RunState]{
} ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
configCommand, err := ckconfig.Command(configSuite) ckprompt.ConfigScope(loadEngine),
},
})
if err != nil { if err != nil {
return err return err
} }
definitions := ckprompt.FromEngine(loadEngine)
definitions.NewContext = newRunState
promptCommand, err := ckprompt.Command(definitions) promptCommand, err := ckprompt.Command(definitions)
if err != nil { if err != nil {
return err return err
@@ -324,18 +332,103 @@ batch. No guarantee extends across separate registration calls.
The registry must still recognize any selected standard-namespace restrictions The registry must still recognize any selected standard-namespace restrictions
rather than assume every valid command may be installed at any location. 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. Its exact
shape remains open; 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 ## Ownership, collisions, and the core boundary
Neither alternative needs to expose registry maps, an installed-config boolean, None of the alternatives needs to expose registry maps, an installed-config boolean,
individual handler constructors, or backend objects. Ordinary registration can individual handler constructors, or backend objects. Ordinary registration can
reject an occupied root path and commit a fully validated batch without merging reject an occupied root path and commit a fully validated batch without merging
children. That protects installed-family ownership for both designs. children. That protects installed-family ownership for all three designs.
Evaluate any additional reservation requirements against the accepted ownership Evaluate any additional reservation requirements against the accepted ownership
decision, not prototype flags or incidental checks. If a reserved namespace must decision, not prototype flags or incidental checks. If a reserved namespace must
be unavailable to ordinary application declarations before its integration is be unavailable to ordinary application declarations before its integration is
installed, group immutability and duplicate checks alone do not express that installed, group immutability and duplicate checks alone do not express that
rule. A narrow core installation distinction may then be justified. Both designs rule. A narrow core installation distinction may then be justified. All designs
can accommodate it, and the final proposal should identify the exact rule it can accommodate it, and the final proposal should identify the exact rule it
serves before selecting its public signature. Do not invent package-origin serves before selecting its public signature. Do not invent package-origin
authentication or a general plugin framework to solve registration ownership. authentication or a general plugin framework to solve registration ownership.
@@ -346,22 +439,50 @@ second public declaration model. Adopt it only if a concrete reservation or
composition responsibility requires that distinction; module extraction alone composition responsibility requires that distinction; module extraction alone
does not. This is a conditional extension to the alternatives, not a prerequisite. does not. This is a conditional extension to the alternatives, not a prerequisite.
## 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. The precise capture boundary
for B and C, before installation, remains an open decision below.
Empty declarations also need an explicit choice. A suite with no definition
capabilities could reasonably install nothing, but B and C then need a way to
represent that result without registering an empty standard namespace or treating
every invalid zero-value command as a no-op. An empty configuration suite may
have different semantics because scope discovery is itself an operation. These
cases remain open rather than inheriting a choice from illustrative code.
## Comparison ## Comparison
| Consideration | Option A: `Register` | Option B: `Command` factory | | Consideration | Option A: `Register` | Option B: validated `Command` | Option C: deferred `Command` |
| --- | --- | --- | | --- | --- | --- | --- |
| Typical consumer task | Supply suite and install family | Supply suite, receive group, then install | | 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 | | Integration exports for registration | One convenience function | One complete-group factory | One deferred-declaration factory |
| Domain commands | Ordinary core declarations | Same | | Suite errors reported at | Integration registration | Group construction | Core installation |
| Atomicity | Each integration registration call | Each core registration batch | | Domain commands | Ordinary core declarations | Same | Same; infallible constructors not required |
| Standard assembly visible to callers | Integration-owned | Complete group handle exposed | | Atomicity | Each integration registration call | Each core registration batch | Each core registration batch, including preparation |
| Pre-assembly of mixed command collections | Separate domain declarations and family calls | Directly supported | | Standard assembly visible to callers | Integration-owned | Complete group handle exposed | Deferred group handle exposed |
| Additional core family abstraction | Only for a demonstrated reservation need | Same | | Pre-assembly of mixed command collections | Separate domain declarations and family calls | Directly supported | Directly supported |
| Dependency isolation and lazy acquisition | Supported | Supported | | Additional core mechanism | Only for demonstrated ownership needs | Same | Deferred preparation, plus any ownership mechanism |
| Dependency isolation and lazy acquisition | Supported | Supported | Supported |
## Recommendation ## Recommendation
Choose **Option A: integration-owned `Register` functions with typed suites**. Prefer **Option A: integration-owned `Register` functions with typed suites and
declaration-only adapters**. Rank Option C second and Option B third.
The representative consumers benefit from installing standard behavior directly; The representative consumers benefit from installing standard behavior directly;
they do not demonstrate a need to collect standard command handles or roll back 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 all family registrations together. Per-call atomicity and discarding a failed
@@ -373,12 +494,52 @@ Resolve the reservation distinction above before finalizing core signatures;
add only the capability its actual responsibility requires. add only the capability its actual responsibility requires.
Do not ship both `Register` and `Command` merely to cover every possible assembly 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 style. Options B and C are credible alternatives if pre-assembly or batch
becomes a demonstrated consumer requirement. Selecting it would still preserve installation becomes a demonstrated consumer requirement. C reduces consumer
the same module and capability boundaries. 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 Once this proposal is accepted, update the owning module roadmap with the chosen
public registration shape and reservation semantics. Its staged implementation public registration shape and reservation semantics. Its staged implementation
plan can then specify exact declarations and validation without asking the coding plan can then specify exact declarations and validation without asking the coding
agent to invent a family framework. Prototype-compatible names, intermediate agent to invent a family framework. Prototype-compatible names, intermediate
buildability, and mechanical file relocation are not selection criteria. buildability, and mechanical file relocation are not selection criteria.
## Remaining decisions and incomplete areas
1. **Select the registration alternative.** A remains the recommendation; C and
B remain proposals. Evaluate the full setup shown above rather than counting
only the final installation calls. No registration shape is selected here.
2. **Settle the context-free case.** The simple examples still require
`Suite[struct{}]`, a scope slice, and an unused loader argument. Decide whether
that is acceptable or whether a focused convenience entry point should accept
a loader without application context. Show its complete call site and avoid
creating a parallel configuration language just to remove a few lines.
3. **Specify empty-suite behavior.** Decide independently for configuration and
definition suites whether zero capabilities mean no installation, discovery
only, or a declaration error. If selecting B or C, define the representation
of an intentional no-op and distinguish it from an invalid command.
4. **Resolve pre-installation namespace reservations.** Identify the exact
accepted rule for names before an integration is installed, reconcile ADR
and architecture-policy language where necessary, and distinguish it from
protecting an already installed family. For B or C, also specify how standard
root placement is retained when declarations are passed through ordinary
group APIs. Add a core distinction only for the selected rule.
5. **Finalize declaration capture and reuse.** Confirm the proposed separation
of captured metadata and application-owned callback state. For B and C,
decide whether capture happens at factory invocation or installation, and
whether the returned declaration can be installed in multiple applications.
Do not infer concurrent-dispatch guarantees from declaration reuse.
6. **Complete validation details.** Define how declaration-only adapters retain
invalid wiring for the eventual validation boundary, how errors identify the
affected family or scope, and how nil loaders differ from deliberately absent
capabilities. If selecting C, specify the public preparation mechanism and
show that it validates the whole batch before registry mutation without
invoking providers or application context factories.
7. **Exercise typed customization.** Add one realistic selector/override example
showing explicit presence and scope applicability. Specify absent
`NewContext` behavior, particularly for pointer context types, and keep the
configuration and definition selector contracts appropriately distinct.
These details should support the chosen registration model without becoming
a universal integration framework.