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

41 KiB

Registration API alternatives

Status: design mockup for discussion; none of the alternatives is implemented or selected by this document.

Purpose and authority

Settle the public registration shape for the optional modules described in the module roadmap, before finalizing its implementation plan. ADR 0001 selects explicit instance registration, typed inputs, and standard-family ownership. ADR 0010 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

All 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:

// In ckconfig:
func FromConfigKit[C, T any](
    name, summary string,
    load func(context.Context, Selection, C) (*configkit.Result[T], error),
) Scope[C]

// In ckprompt:
func ConfigScope[C any](
    load func(context.Context, ckconfig.Selection, C) (*promptkit.Engine, error),
) ckconfig.Scope[C]

func FromEngine[C any](
    load func(context.Context, ckconfig.Selection, C) (*promptkit.Engine, error),
) Suite[C]

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.

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 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.

Consumer assumptions

The simple ConfigKit examples have no domain selectors, overrides, or application 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 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 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 the engine scope while omitting definition registration enables only PromptKit configuration inspection; keeping definitions while omitting that 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

Proposed public shape

// 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

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

app, err := commandkit.New(identity, greet)
if err != nil {
    return err
}
if err := ckconfig.Register(app, ckconfig.Suite[struct{}]{
    Scopes: []ckconfig.Scope[struct{}]{
        ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
    },
}); 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

app, err := commandkit.New(identity, sessionGroup)
if err != nil {
    return err
}
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
}
definitions := ckprompt.FromEngine(loadEngine)
definitions.NewContext = newRunState
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, without separate error checks for the declaration-only adapter constructors.
  • 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:

// 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. 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

The core-only sketch from Option A is unchanged. This design adds no required integration dependency or configuration to core construction.

ConfigKit-only consumer

configCommand, err := ckconfig.Command(ckconfig.Suite[struct{}]{
    Scopes: []ckconfig.Scope[struct{}]{
        ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
    },
})
if err != nil {
    return err
}
app, err := commandkit.New(identity, greet, configCommand)
if err != nil {
    return err
}

Composed consumer

configCommand, err := ckconfig.Command(ckconfig.Suite[*RunState]{
    NewContext: newRunState,
    Scopes: []ckconfig.Scope[*RunState]{
        ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
        ckprompt.ConfigScope(loadEngine),
    },
})
if err != nil {
    return err
}
definitions := ckprompt.FromEngine(loadEngine)
definitions.NewContext = newRunState
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.

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:

// 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. 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 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

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

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

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.

The accepted ownership rule is clear for installed standard families: application registration cannot replace them or add private operations underneath them. ADR 0001 also refers to reserved standard families, while the architecture policy 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.

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

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. 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.

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

Consideration Option A: Register Option B: validated Command Option C: deferred Command
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 One deferred-declaration factory
Suite errors reported at Integration registration Group construction Core installation
Domain commands Ordinary core declarations Same Same; infallible constructors not required
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 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, 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 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. 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 installation becomes a demonstrated consumer requirement. C reduces consumer 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 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.

Additional design work and proposed resolutions

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:

// 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:

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:

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:

// 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:

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.

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.