From eeb9fdd66cee844afe636644a9d0fcefbbf44bee Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 10 Oct 2026 13:55:22 +0000 Subject: [PATCH] Compare integration registration API alternatives --- docs/roadmap/registration-alternatives.md | 384 ++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 docs/roadmap/registration-alternatives.md diff --git a/docs/roadmap/registration-alternatives.md b/docs/roadmap/registration-alternatives.md new file mode 100644 index 0000000..32dc7f9 --- /dev/null +++ b/docs/roadmap/registration-alternatives.md @@ -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.