diff --git a/docs/roadmap/registration-alternatives.md b/docs/roadmap/registration-alternatives.md index 801e7df..3448183 100644 --- a/docs/roadmap/registration-alternatives.md +++ b/docs/roadmap/registration-alternatives.md @@ -118,7 +118,9 @@ 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 still open; these examples do not settle that API. +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 @@ -347,8 +349,9 @@ 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. +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 @@ -419,25 +422,51 @@ No guarantee extends across separate calls. ## 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. +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. -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. All 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. +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. -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. +**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 @@ -455,15 +484,13 @@ 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. +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. -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. +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 @@ -476,13 +503,14 @@ cases remain open rather than inheriting a choice from illustrative code. | 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 | Only for demonstrated ownership needs | Same | Deferred preparation, plus any ownership mechanism | +| 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**. Rank Option C second and Option B third. +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 @@ -490,8 +518,9 @@ 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. +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 @@ -506,40 +535,334 @@ plan can then specify exact declarations and validation without asking the codin agent to invent a family framework. Prototype-compatible names, intermediate buildability, and mechanical file relocation are not selection criteria. -## Remaining decisions and incomplete areas +## Additional design work and proposed resolutions -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. +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.