1255 lines
54 KiB
Markdown
1255 lines
54 KiB
Markdown
# Unified command registration proposal
|
|
|
|
Status: direction agreed; declarations, consumer examples, and design checks are
|
|
prepared for final review. Supporting API names and input/state separation below
|
|
are proposed details of that direction. No implementation is authorized by this
|
|
document alone.
|
|
|
|
## Purpose and authority
|
|
|
|
Applications and integrations construct captured command declarations and install
|
|
them through the same core API. Installation validates each batch before registry
|
|
mutation. Integrations supply complete standard families and focused conveniences
|
|
while retaining ownership of their provider semantics.
|
|
|
|
This document records the selected direction following the
|
|
[registration alternatives](registration-alternatives.md). It replaces that
|
|
comparison's recommendation of Option A with a refinement of Option C: capture
|
|
declarations and any construction errors eagerly, then report errors at
|
|
installation. The comparison remains background, not the selected API proposal.
|
|
|
|
[ADR 0001](../adr/0001-command-dispatch-and-inputs.md) owns explicit registration,
|
|
typed inputs, and command ownership.
|
|
[ADR 0010](../adr/0010-optional-integration-modules.md) owns optional module
|
|
boundaries and convenient integration registration. The
|
|
[module roadmap](modules.md) tracks extraction work. Reconcile the namespace
|
|
lifetime decision below with the relevant ADR and architecture-policy wording
|
|
before implementation; this proposal does not silently rewrite those records.
|
|
|
|
Design for the best consumer API without preserving current implementation
|
|
signatures or machinery. Existing code can supply evidence and reusable work,
|
|
but does not constrain the design. This is future API design, not documentation
|
|
of implemented behavior.
|
|
|
|
## Selected consumer model
|
|
|
|
A consumer can use only the core or add any supported combination of integrations.
|
|
Every command reaches the application through core `New` or `Register`, regardless
|
|
of whether an application or integration declared it. There is no mandatory
|
|
integration interface, application-wide generic input type, or separate
|
|
integration installation lifecycle.
|
|
|
|
The core remains independent of integration modules and their upstream libraries.
|
|
Integrations may depend on one another as documented. Such module dependencies
|
|
do not install commands or acquire providers. The selected modules remain
|
|
`CommandKit`, `CommandKit/integrations/ckconfig`, and
|
|
`CommandKit/integrations/ckprompt`, under the existing repository module prefix.
|
|
|
|
Each application command retains its own typed input and domain handler.
|
|
Integration suites retain their own typed context and provider declarations.
|
|
Both produce the same opaque core `Command` handle. Consumers need not construct
|
|
individual standard handlers, duplicate integration adapters, or expose backend
|
|
objects to assemble an application.
|
|
|
|
## Declaration and installation
|
|
|
|
### One declaration model
|
|
|
|
A `Command` is a captured declaration, not proof that the declaration is valid
|
|
or installed. Ordinary command, group, scope, and suite adapter construction
|
|
does not require a separate error check at each call site.
|
|
|
|
The uniform construction shape is below. The complete declarations and examples
|
|
later in this document provide the supporting fields and semantics for review.
|
|
|
|
```go
|
|
// Core package:
|
|
func NewCommand[I any](spec CommandSpec[I]) Command
|
|
func Group(name, summary string, children ...Command) Command
|
|
|
|
func New(identity Identity, commands ...Command) (*App, error)
|
|
func (app *App) Register(commands ...Command) error
|
|
```
|
|
|
|
The initial integrations expose complete-family factories using their own suite
|
|
types:
|
|
|
|
```go
|
|
// In each initial integration package; Suite belongs to that integration:
|
|
func Command[I, S any](suite Suite[I, S]) commandkit.Command
|
|
```
|
|
|
|
Here `I` is the suite's CLI input and `S` is its shared invocation state. This
|
|
proposed separation resolves the pointer-isolation issue without a clone hook or
|
|
an application-wide type. Future integrations can accept declarations suited to
|
|
their resources while returning the same core `Command`; the core does not
|
|
prescribe a universal suite.
|
|
|
|
Application-owned command factories follow the same declaration pattern.
|
|
Application helpers that genuinely perform other fallible work may still return
|
|
errors; the design does not suppress those errors or require all application
|
|
functions to be infallible.
|
|
|
|
### Eager capture, errors reported at installation
|
|
|
|
Factories capture declaration metadata and callback references and assemble
|
|
neutral command descriptions immediately. Validation that can already be done
|
|
may occur then, but its errors remain attached to the declaration for installation
|
|
to report. Internally fallible assembly can capture an error without returning it
|
|
to the consumer at every construction step.
|
|
|
|
Use the proposed `WithError(command, err)` core operation to retain integration
|
|
construction errors in opaque declarations. Its complete signature and no-op
|
|
interaction are specified below. Do not substitute an untyped integration payload,
|
|
root-private access, or a general preparation callback. The proposed
|
|
`DeferredGroup` lifecycle from the comparison is not selected.
|
|
|
|
`New` and `Register` report retained declaration errors and validate the batch,
|
|
including bindings, root placement, and collisions, before registry mutation.
|
|
A failed `New` yields no usable application. A failed `Register` leaves the
|
|
existing registry intact. No rollback guarantee spans separate registration calls.
|
|
|
|
Declaration construction, installation, help, and version do not invoke provider
|
|
acquisition or invocation input/context factories. They do not validate files,
|
|
credentials, or application configuration. Invalid wiring fails at installation;
|
|
an upstream error returned by a valid provider remains an invocation result.
|
|
|
|
An originally nil loader remains invalid wiring even if an adapter would wrap
|
|
it in a non-nil function. Integration declarations retain enough information to
|
|
detect this. An intentionally absent capability is distinct from an invalid
|
|
adapter. Errors identify the affected command family and relevant scope, binding,
|
|
or capability and preserve supported wrapped error identity; exact prose is not
|
|
selected here.
|
|
|
|
## Integration composition and conveniences
|
|
|
|
Configuration scopes are composed before installation into one configuration
|
|
suite. Application and engine scopes can share that suite. Named definition
|
|
inspection remains independently selectable: installing an engine configuration
|
|
scope does not automatically install definition commands, or vice versa.
|
|
|
|
Keep provider declarations integration-owned. Configuration supports ConfigKit
|
|
for now; the core does not acquire a universal configuration or suite interface.
|
|
The PromptKit integration can use the ConfigKit integration's selection and scope
|
|
contracts through its selected dependency.
|
|
|
|
Provide the focused `ckconfig.AppCommand(load)` convenience for a context-free
|
|
application configuration loader. It declares the standard `app` scope, with a
|
|
default description, through the same declaration model as the full suite. It
|
|
does not require an unused context argument or `Suite[struct{}]` at the consumer
|
|
call site. A loader still receives `context.Context` and configuration selection.
|
|
Use the full suite when adding scopes, selectors, custom state, or other metadata.
|
|
|
|
Do not initially expose integration-specific `Register` or `RegisterApp`
|
|
functions alongside declaration factories. Core registration is the installation
|
|
path for all consumers. Additional conveniences should address demonstrated
|
|
consumer needs rather than mirror every underlying constructor automatically.
|
|
|
|
## Namespaces and empty declarations
|
|
|
|
Help and version are reserved by core construction. Optional families claim
|
|
their complete root namespace when installed. Before installation, a domain group
|
|
may occupy that name; subsequent integration installation then fails with a
|
|
collision. The reverse order also rejects a collision. Neither order replaces or
|
|
merges an existing group.
|
|
|
|
Standard-family declarations carry enforceable root-only placement metadata.
|
|
Installing them beneath an ordinary group is an assembly error. Use the same
|
|
`Command` type with the proposed `RootGroup` constructor specified below. It
|
|
represents placement, not authentication of the declaring package. Installed
|
|
standard families cannot be extended with private application operations or have
|
|
their handlers replaced.
|
|
|
|
The selected empty-suite behavior is:
|
|
|
|
| Declaration | Installation result |
|
|
| --- | --- |
|
|
| Configuration suite with no scopes | Declaration error |
|
|
| Named configuration scopes with no inspection operations | Install scope discovery with the actual empty capability sets |
|
|
| Configuration scopes with supplied operations | Install discovery and supported operations |
|
|
| Otherwise valid definition suite with no capabilities | Successful no-op; no namespace or help entry |
|
|
| Adapter with a nil loader | Declaration error, even when selected capabilities otherwise appear empty |
|
|
|
|
Use an explicit `commandkit.NoCommand()` declaration for intentional absence.
|
|
Core installation skips it. A zero-value `Command` remains invalid. Integration
|
|
factories validate or retain wiring errors before representing an empty family
|
|
as a no-op; absence must not swallow errors. A no-op does not reserve a namespace.
|
|
|
|
## Capture, reuse, and invocation inputs
|
|
|
|
Capture scope metadata, capability lists, selector applicability, bindings, and
|
|
callback references when declarations are constructed. Copy the declaration
|
|
collections needed for those semantics. Later edits to the source suite or its
|
|
metadata slices do not change a returned command declaration.
|
|
|
|
Declarations may be installed in multiple applications. Registries maintain
|
|
independent installed state, and invocations have independent parsed state.
|
|
Application callback closures and objects they reference remain application-owned
|
|
and may deliberately be shared. These choices do not promise arbitrary deep
|
|
copies or concurrent dispatch.
|
|
|
|
Use the same typed, presence-aware binding primitives for application commands
|
|
and integration inputs. Configuration selectors additionally declare scope
|
|
applicability; definition selectors need no configuration-specific wrapper.
|
|
Explicit empty, false, and zero values remain distinguishable from omission.
|
|
|
|
An omitted `NewInput` or `NewState` supplies the zero value of its respective
|
|
type. For pointer types that is nil; do not silently allocate arbitrary pointer
|
|
state or prohibit a loader that intentionally accepts nil. Applications supply a
|
|
factory when initialized state is needed. A supplied factory returning nil retains
|
|
its application-owned meaning.
|
|
|
|
Scope-specific parsed values stay separate from shared acquisition state through
|
|
the proposed `Suite[I, S]`: `NewInput` initializes each selected scope's input,
|
|
while `NewState` initializes `S` once for the selected inspection invocation.
|
|
Providers receive both explicitly. Pointer inputs are independently initialized,
|
|
not shallow copies of a single pointer. The detailed lifetime table below makes
|
|
this change from the comparison's mixed context model explicit; it does not
|
|
require arbitrary deep copies or prohibit pointer inputs or shared pointer state.
|
|
|
|
## Proposed core declarations
|
|
|
|
The following declarations specify the public registration and binding surface
|
|
used by these examples. Opaque types retain unexported representation; these are
|
|
API declarations, not an implementation of their bodies. `context` and `io` are
|
|
standard-library packages. Outcome semantics remain governed by
|
|
[ADR 0005](../adr/0005-invocation-outcomes.md); this pass changes neither status
|
|
mapping nor diagnostic delivery policy.
|
|
|
|
```go
|
|
package commandkit
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
)
|
|
|
|
type App struct {
|
|
// Unexported representation.
|
|
}
|
|
type Command struct {
|
|
// Unexported representation.
|
|
}
|
|
type Flag[I any] struct {
|
|
// Unexported representation.
|
|
}
|
|
type Argument[I any] struct {
|
|
// Unexported representation.
|
|
}
|
|
|
|
type Identity struct {
|
|
Name string
|
|
Version string
|
|
Revision string
|
|
}
|
|
|
|
type Streams struct {
|
|
Out io.Writer
|
|
Err io.Writer
|
|
}
|
|
|
|
type Invocation struct {
|
|
Context context.Context
|
|
Streams Streams
|
|
}
|
|
|
|
type Supplied[T any] struct {
|
|
Value T
|
|
Set bool
|
|
}
|
|
|
|
type FlagMeta struct {
|
|
Name string
|
|
Short string
|
|
Usage string
|
|
Required bool
|
|
}
|
|
|
|
type ArgumentMeta struct {
|
|
Name string
|
|
Usage string
|
|
}
|
|
|
|
type CommandSpec[I any] struct {
|
|
Name string
|
|
Summary string
|
|
Description string
|
|
NewInput func() I
|
|
Flags []Flag[I]
|
|
Arguments []Argument[I]
|
|
CheckInput func(I) error
|
|
Run func(Invocation, I) Outcome
|
|
}
|
|
|
|
func NewCommand[I any](spec CommandSpec[I]) Command
|
|
func Group(name, summary string, children ...Command) Command
|
|
func RootGroup(name, summary string, children ...Command) Command
|
|
func WithError(command Command, err error) Command
|
|
func NoCommand() Command
|
|
|
|
func New(identity Identity, commands ...Command) (*App, error)
|
|
func (app *App) Register(commands ...Command) error
|
|
func (app *App) Execute(ctx context.Context, args []string, streams Streams) Outcome
|
|
|
|
func ValueFlag[I, V any](
|
|
meta FlagMeta, defaultValue *V,
|
|
parse func(string) (V, error), set func(*I, Supplied[V]),
|
|
) Flag[I]
|
|
func BoolFlag[I any](
|
|
meta FlagMeta, defaultValue bool, set func(*I, Supplied[bool]),
|
|
) Flag[I]
|
|
func RepeatFlag[I, V any](
|
|
meta FlagMeta, defaults []V,
|
|
parse func(string) (V, error), set func(*I, Supplied[[]V]),
|
|
) Flag[I]
|
|
func ValueArgument[I, V any](
|
|
meta ArgumentMeta, required bool,
|
|
parse func(string) (V, error), set func(*I, Supplied[V]),
|
|
) Argument[I]
|
|
func RepeatArgument[I, V any](
|
|
meta ArgumentMeta, min, max int,
|
|
parse func(string) (V, error), set func(*I, Supplied[[]V]),
|
|
) Argument[I]
|
|
|
|
type Severity string
|
|
|
|
const (
|
|
SeverityWarning Severity = "warning"
|
|
SeverityError Severity = "error"
|
|
)
|
|
|
|
type Diagnostic struct {
|
|
Severity Severity
|
|
Write func(io.Writer) error
|
|
}
|
|
|
|
// Neutral value/diagnostic envelope; not a universal provider interface.
|
|
type Acquired[T any] struct {
|
|
Value T
|
|
Diagnostics []Diagnostic
|
|
}
|
|
|
|
type OutcomeKind string
|
|
|
|
const (
|
|
OutcomeSuccess OutcomeKind = "success"
|
|
OutcomeUsage OutcomeKind = "usage"
|
|
OutcomeOperational OutcomeKind = "operational"
|
|
OutcomeDomain OutcomeKind = "domain"
|
|
)
|
|
|
|
type DeliveryState string
|
|
|
|
const (
|
|
DeliveryNone DeliveryState = "none"
|
|
DeliveryComplete DeliveryState = "complete"
|
|
DeliveryIncomplete DeliveryState = "incomplete"
|
|
)
|
|
|
|
type Outcome struct {
|
|
Kind OutcomeKind
|
|
Status int
|
|
Domain string
|
|
Cause error
|
|
DeliveryError error
|
|
DiagnosticDelivery DeliveryState
|
|
ReportIncomplete bool
|
|
Diagnostics []Diagnostic
|
|
}
|
|
|
|
func Success() Outcome
|
|
func Failure(cause error) Outcome
|
|
```
|
|
|
|
`NewCommand` records invalid bindings or invalid execution wiring as declaration
|
|
errors; it does not require a construction error check. An ordinary group may
|
|
have no execution handler. Opaque declarations expose no operation to mutate an
|
|
installed tree, extract its handlers, or append children after capture. Collections
|
|
in `CommandSpec`, groups, and defaults are captured as appropriate; runtime
|
|
closure state and diagnostic writer effects remain caller-owned.
|
|
|
|
### Retained errors, placement, and no-ops
|
|
|
|
`WithError` returns a new captured declaration retaining a non-nil error alongside
|
|
any errors already attached to the declaration or its children. A nil error does
|
|
not add an error. Installation reports retained errors with command context and
|
|
preserves their supported `errors.Is`/`errors.As` identity. It need not prescribe
|
|
an exact aggregate-error type or error text. Wrapping does not turn an invalid
|
|
zero declaration into a valid command or erase a root-only marker.
|
|
|
|
`RootGroup` has the same eager declaration behavior as `Group`, but carries
|
|
root-only placement metadata. Core installation rejects it anywhere below the
|
|
root, including inside a wrapper declared with ordinary `Group`. No integration
|
|
name allowlist or declaring-package authentication is needed.
|
|
|
|
`NoCommand` is an explicit absence marker. Core checks attached errors before
|
|
skipping it, so `WithError(NoCommand(), err)` fails rather than hiding `err`.
|
|
An unadorned no-op is skipped at the root or among children and contributes no
|
|
help entry or reserved namespace. A zero-value `Command` is not this marker and
|
|
fails installation. A root-only group with no children remains a real group,
|
|
not an absence marker; the integrations select their own empty-suite semantics.
|
|
|
|
These operations compose without private-state access or delayed preparation.
|
|
An integration can construct an ordinary neutral tree, mark its root, and attach
|
|
its own validation error:
|
|
|
|
```go
|
|
// Schematic integration-owned assembly: every function called here runs eagerly.
|
|
func familyDeclaration(children []commandkit.Command, wiringErr error) commandkit.Command {
|
|
group := commandkit.RootGroup("config", "Inspect configuration.", children...)
|
|
return commandkit.WithError(group, wiringErr)
|
|
}
|
|
```
|
|
|
|
The actual integration factory validates scope/provider metadata and eagerly
|
|
constructs all supported children. Core constructors retain their own binding
|
|
errors. There is no exported preparation callback, invocation-time declaration
|
|
validation, or root helper that understands `ckconfig.Suite`.
|
|
|
|
### Typed flag composition needed by optional modules
|
|
|
|
Scopes need to apply the same parsed selector to independently initialized inputs.
|
|
The integration cannot inspect a core `Flag`'s private parser or setter. Provide
|
|
one narrow typed composition mechanism, rather than exporting those internals:
|
|
|
|
```go
|
|
// In commandkit:
|
|
type FlagValues[I any] struct {
|
|
// Unexported representation.
|
|
}
|
|
|
|
// Non-nil sentinel errors; compare using errors.Is, including through wrapping.
|
|
var (
|
|
ErrFlagInput error // Missing required selected flag value.
|
|
ErrFlagWiring error // Invalid binding selection or destination.
|
|
)
|
|
|
|
func CaptureFlags[Outer, I any](
|
|
flags []Flag[I],
|
|
target func(*Outer) *FlagValues[I],
|
|
) []Flag[Outer]
|
|
|
|
func (values FlagValues[I]) ApplyTo(target *I, selected ...Flag[I]) error
|
|
```
|
|
|
|
`CaptureFlags` eagerly captures bindings and supplies ordinary outer-command flag
|
|
declarations. During an execution invocation those declarations record converted
|
|
values, defaults, and presence in the targeted `FlagValues`; they do not initialize
|
|
`I` or acquire providers. Nil target callbacks and invalid inner bindings remain
|
|
declaration errors. The core's ordinary flag validation still catches duplicate
|
|
names and conflicts with standard flags after composition.
|
|
|
|
`ApplyTo` delivers only the selected bindings into an already initialized input.
|
|
The integration selects those bindings from its captured scope-applicability
|
|
metadata. Binding handles carry sufficient identity to match their captured
|
|
values without string-based retrieval by handlers. Selecting a binding absent
|
|
from the captured set or providing a nil destination pointer is an application/
|
|
integration wiring error. The zero `FlagValues` with no selected bindings is valid.
|
|
|
|
Copying a binding handle or capturing it in a command preserves its identity.
|
|
Constructing another flag with identical metadata creates a distinct binding;
|
|
matching names, parsers, or setters do not make it interchangeable with the
|
|
original. Reuse the original handles when selecting values to apply. The same
|
|
handle can be captured by separate declarations without sharing invocation values.
|
|
|
|
Execution-required checks for captured inner flags are deferred until `ApplyTo`
|
|
selects the applicable subset; a required app-only selector must not become
|
|
mandatory for a PromptKit-only scope request. Missing required selected values
|
|
match `ErrFlagInput` through `errors.Is` and produce usage outcomes. An absent
|
|
binding or nil destination matches `ErrFlagWiring` and produces an operational
|
|
failure, not an operator usage error. Each error produced by `ApplyTo` identifies
|
|
one of these categories and includes relevant flag context without requiring
|
|
callers to parse its message.
|
|
|
|
The core's `CheckInput` error handling recognizes `ErrFlagWiring` through wrapping
|
|
and routes it to the operational path. `ErrFlagInput` and ordinary input-validation
|
|
errors use the usage path. Integrations may therefore return an `ApplyTo` error
|
|
from their input-validation path, adding family/scope context by wrapping it,
|
|
without misclassifying wiring defects. Both failures stop before shared-state
|
|
initialization or acquisition. Metadata wiring defects detectable during assembly
|
|
remain retained installation errors; these categories do not defer those checks
|
|
to execution. Help uses metadata without applying values or requiring execution
|
|
inputs.
|
|
|
|
Core-owned repeatable-value slices are independent per application of the values;
|
|
custom parsed objects and accessor effects retain application ownership. This
|
|
API does not deep-copy arbitrary application values or expose backend state.
|
|
It is a reusable typed input-composition contract for integrations, not a second
|
|
parser, a raw-argv handler API, or an untyped integration payload.
|
|
|
|
For example, the private configuration command input can contain
|
|
`FlagValues[I]` beside standard selection/scope fields. The integration declares
|
|
its selector flags through `CaptureFlags`, resolves selected scopes in its
|
|
execution input-validation path, and calls `ApplyTo` for each scope. Definition
|
|
inspection applies the whole captured flag set once. This explains the public
|
|
seam; private input structs and the exact assembly algorithm remain implementation
|
|
choices.
|
|
|
|
## Proposed configuration integration declarations
|
|
|
|
The `ckconfig` package owns the following contracts. Only the four implemented
|
|
inspection operations appear here; comparison remains deferred. `config scopes`
|
|
is supplied by suite assembly, not an acquisition operation.
|
|
|
|
```go
|
|
package ckconfig
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
|
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
type Selection struct {
|
|
Config commandkit.Supplied[string]
|
|
Overlay commandkit.Supplied[string]
|
|
}
|
|
|
|
type Operation string
|
|
|
|
const (
|
|
Validate Operation = "validate"
|
|
Show Operation = "show"
|
|
Sources Operation = "sources"
|
|
Explain Operation = "explain"
|
|
)
|
|
|
|
type Request struct {
|
|
Operation Operation
|
|
Selection Selection
|
|
}
|
|
|
|
type EffectiveWriter interface {
|
|
WriteEffective(io.Writer) error
|
|
}
|
|
|
|
type SourceReader interface {
|
|
Sources() []configkit.SourceInfo
|
|
}
|
|
|
|
type ExplanationReader interface {
|
|
Explain(configkit.Path) (configkit.Explanation, bool)
|
|
WriteExplanation(io.Writer, configkit.Path) error
|
|
}
|
|
|
|
type View struct {
|
|
Effective EffectiveWriter
|
|
Sources SourceReader
|
|
Explanation ExplanationReader
|
|
}
|
|
|
|
type Scope[I, S any] struct {
|
|
Name string
|
|
Summary string
|
|
Operations []Operation
|
|
Acquire func(context.Context, Request, I, S) (commandkit.Acquired[View], error)
|
|
// Adapter construction issues are retained in unexported representation.
|
|
}
|
|
|
|
type Selector[I any] struct {
|
|
Binding commandkit.Flag[I]
|
|
Scopes []string
|
|
}
|
|
|
|
type Suite[I, S any] struct {
|
|
NewInput func() I
|
|
NewState func() S
|
|
Selectors []Selector[I]
|
|
CheckInput func(scope string, selection Selection, input I) error
|
|
Scopes []Scope[I, S]
|
|
}
|
|
|
|
func FromConfigKit[I, S, T any](
|
|
name, summary string,
|
|
load func(context.Context, Selection, I, S) (*configkit.Result[T], error),
|
|
) Scope[I, S]
|
|
|
|
func Command[I, S any](suite Suite[I, S]) commandkit.Command
|
|
|
|
func AppCommand[T any](
|
|
load func(context.Context, Selection) (*configkit.Result[T], error),
|
|
) commandkit.Command
|
|
```
|
|
|
|
The generic types are local to the suite, not to `App`. `I` contains typed CLI
|
|
values; `S` contains invocation state such as a cache. Neither belongs to the core
|
|
configuration model because the core has no such model. A scope with no operations
|
|
can omit `Acquire` and still be discoverable. A scope declaring operations needs
|
|
valid acquisition wiring. Providers supply the relevant native view facets and
|
|
validation diagnostics at invocation time, preserving current selected reports
|
|
and classifications rather than implementing a second configuration engine.
|
|
|
|
An empty selector scope list applies to all registered scopes; a nonempty list
|
|
identifies its applicable registered scopes. Unknown identifiers and reserved
|
|
standard-flag collisions are declaration errors. Scope identifiers, capability
|
|
selection, defaults, and aggregate availability follow
|
|
[ADR 0009](../adr/0009-configuration-scopes.md). Non-applicable selectors are not
|
|
applied to a scope's input; this adds no new unused-selector rejection policy.
|
|
`CheckInput` receives the registered name of each requested scope alongside its
|
|
selection and initialized, filtered input before acquisition. It can distinguish
|
|
a selector omitted for an applicable scope from one intentionally not delivered
|
|
to another scope, and apply domain rules accordingly without loading configuration.
|
|
The definition suite retains its scope-free validation signature; it has no
|
|
configuration scope to supply.
|
|
|
|
`FromConfigKit` declares the operations supported by the selected ConfigKit
|
|
adapter and records the original loader, including an invalid nil loader.
|
|
Consumers may explicitly narrow `Operations` before calling `Command`; a
|
|
constructor issue is not erased by clearing the operation list. The suite factory
|
|
captures those lists and its generated neutral command tree immediately.
|
|
|
|
`AppCommand` uses an internal state-free suite with an `app` scope and a plain
|
|
application-configuration description. It neither exposes placeholder type
|
|
arguments nor demands a forwarding wrapper from the caller. A nil loader becomes
|
|
a retained `config`/`app` declaration error. The full suite is used for composition;
|
|
there is no incremental scope append operation after installation.
|
|
|
|
## Proposed PromptKit integration declarations
|
|
|
|
The `ckprompt` package uses `ckconfig` selections and scope contracts through its
|
|
selected module dependency. Definition selectors are plain typed flags; they do
|
|
not use a configuration-scope wrapper. Named prompt and profile inspection remain
|
|
the implemented subset; this pass adds no catalog operations.
|
|
|
|
```go
|
|
package ckprompt
|
|
|
|
import (
|
|
"context"
|
|
|
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
|
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
|
promptkit "gitea.maximumdirect.net/eric/promptkit"
|
|
)
|
|
|
|
type PromptRequest struct {
|
|
Selection ckconfig.Selection
|
|
ID string
|
|
Version commandkit.Supplied[string]
|
|
}
|
|
|
|
type ProfileRequest struct {
|
|
Selection ckconfig.Selection
|
|
ID string
|
|
}
|
|
|
|
type Suite[I, S any] struct {
|
|
NewInput func() I
|
|
NewState func() S
|
|
Selectors []commandkit.Flag[I]
|
|
CheckInput func(ckconfig.Selection, I) error
|
|
InspectPrompt func(context.Context, PromptRequest, I, S) (commandkit.Acquired[*promptkit.PromptInspection], error)
|
|
InspectProfile func(context.Context, ProfileRequest, I, S) (commandkit.Acquired[*promptkit.ProfileInspection], error)
|
|
// Adapter construction issues are retained in unexported representation.
|
|
}
|
|
|
|
func ConfigScope[I, S any](
|
|
load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error),
|
|
) ckconfig.Scope[I, S]
|
|
|
|
func FromEngine[I, S any](
|
|
load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error),
|
|
) Suite[I, S]
|
|
|
|
func Command[I, S any](suite Suite[I, S]) commandkit.Command
|
|
```
|
|
|
|
`ConfigScope` declares the supported configuration facets for the `promptkit`
|
|
scope; `FromEngine` declares supported named inspection callbacks. Both leave
|
|
acquisition untouched until invocation and retain nil-loader errors. Each adapter
|
|
maps the owning library's public diagnostics into core-owned `Diagnostic` values.
|
|
Definition providers preserve diagnostics even when a named lookup fails.
|
|
|
|
Consumers may leave either definition callback nil to omit that capability.
|
|
A valid suite with both nil is an explicit no-op after eager declaration validation.
|
|
A nil loader supplied to `FromEngine` is invalid wiring, not missing capability;
|
|
setting its callbacks nil does not erase the retained error. For prompt lookup,
|
|
the adapter passes the requested version value to PromptKit, with omission using
|
|
its existing empty/default selection. The presence field remains available to a
|
|
custom inspection callback; this does not redefine upstream version semantics.
|
|
|
|
## Input isolation and invocation lifetimes
|
|
|
|
### Why inputs and shared state have separate types
|
|
|
|
The comparison used one context type holding both selector fields and a cache.
|
|
A shared pointer to that object cannot isolate selector fields between scopes.
|
|
Cloning arbitrary application graphs would add a promise CommandKit cannot make.
|
|
The proposed `Suite[I, S]` makes the actual two responsibilities explicit:
|
|
|
|
- `I`: per-scope/per-command CLI input, initialized and populated independently.
|
|
- `S`: shared acquisition state for one selected inspection invocation.
|
|
|
|
There is no public clone hook and no reflection-based allocation of pointer inputs.
|
|
Two type parameters cost some declaration syntax in composed consumers, but make
|
|
sharing deliberate and lifetimes predictable. `AppCommand` hides both for the
|
|
simple case. This is a supporting API detail proposed for final review, not a
|
|
change to unified construction or the independently owned suite principle.
|
|
|
|
### Factory timing
|
|
|
|
The execution rows describe the normal path after applicable conversion and
|
|
preflight checks succeed and execution reaches the indicated stage. They do not
|
|
require callbacks to run after a failure. An input-application or validation
|
|
failure can stop processing before later scopes are initialized;
|
|
shared state and acquisition are reached only after all requested input checks
|
|
succeed. The no-call rows remain unconditional for CommandKit-owned paths.
|
|
|
|
| Path | `NewInput` | Suite `NewState` | Acquisition |
|
|
| --- | --- | --- | --- |
|
|
| Construct declarations / `New` / `Register` | Not called | Not called | Not called |
|
|
| Help, version, `config scopes` | Not called | Not called | Not called |
|
|
| Application command execution | Once for the selected command | No suite factory | Caller handler |
|
|
| Configuration inspection of one scope | Once for that scope | Once for the inspection invocation | Requested scope |
|
|
| Configuration inspection of several scopes | Once per selected scope | Once, shared by selected scopes | Requested scopes, in selected operation order |
|
|
| Named definition inspection | Once for the selected command | Once for that invocation | Selected inspection callback |
|
|
|
|
Input conversion and scope/capability preflight use metadata without acquisition.
|
|
For configuration execution, initialize and populate each requested scope input,
|
|
then run its `CheckInput`; initialize the shared state before acquisition after
|
|
input checks succeed. The input/state callbacks do not run for unselected scopes
|
|
or unrelated families. This explicitly separates the former mixed-context role:
|
|
`NewState` names the shared acquisition-state factory, distinct from
|
|
`context.Context`; it runs once when execution reaches that stage. `NewInput`
|
|
initializes scope storage independently. No shared-state callback is silently
|
|
called once per scope.
|
|
|
|
A missing factory supplies the zero value of its type. A nil pointer input/state
|
|
is permitted when the receiving binding callbacks and providers support it.
|
|
Applications requiring allocated pointer input supply `NewInput`; those requiring
|
|
a cache or other initialized state supply `NewState`. Factories that deliberately
|
|
return shared mutable input objects retain caller-owned effects; CommandKit does
|
|
not promise to repair those aliases. For scope-independent CLI storage, return
|
|
fresh input storage as demonstrated below. Pointer shared state is supported and
|
|
is shared deliberately, not copied.
|
|
|
|
Typed captured flag values are applied only to applicable inputs. Repeatable
|
|
collections owned by CommandKit are independent when materialized into different
|
|
inputs. Arbitrary objects supplied by factories, custom parsers, or closures are
|
|
not deeply cloned. Providers can retain their input pointer according to their
|
|
callback contract without a later scope overwriting it through a shared template.
|
|
Subsequent invocations initialize new input/state; registration does not retain
|
|
an invocation cache. No concurrent-dispatch guarantee follows from these rules.
|
|
|
|
## Complete consumer registration examples
|
|
|
|
These examples target the proposed APIs, not today's prototype. Each block is a
|
|
complete consumer assembly function with imports and input declarations. Domain
|
|
loaders and domain execution are supplied as typed function parameters where
|
|
appropriate: their bodies are application behavior, not hidden registration
|
|
helpers. No constructor error checks or provider acquisition are concealed in
|
|
those parameters. The modules and declarations must be implemented before these
|
|
blocks can compile against CommandKit itself.
|
|
|
|
### 1. Core only: typed commands and a composable group
|
|
|
|
```go
|
|
package coreexample
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
|
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
|
)
|
|
|
|
type GreetInput struct {
|
|
Count commandkit.Supplied[int]
|
|
Name commandkit.Supplied[string]
|
|
Enabled commandkit.Supplied[bool]
|
|
}
|
|
|
|
func App() (*commandkit.App, error) {
|
|
countDefault := 1
|
|
nameDefault := "reader"
|
|
greet := commandkit.NewCommand(commandkit.CommandSpec[GreetInput]{
|
|
Name: "greet", Summary: "Write a greeting.",
|
|
Flags: []commandkit.Flag[GreetInput]{
|
|
commandkit.ValueFlag(
|
|
commandkit.FlagMeta{Name: "count", Usage: "Number of greetings."},
|
|
&countDefault, strconv.Atoi,
|
|
func(input *GreetInput, value commandkit.Supplied[int]) {
|
|
input.Count = value
|
|
},
|
|
),
|
|
commandkit.ValueFlag(
|
|
commandkit.FlagMeta{Name: "name", Usage: "Greeting recipient."},
|
|
&nameDefault, func(raw string) (string, error) { return raw, nil },
|
|
func(input *GreetInput, value commandkit.Supplied[string]) {
|
|
input.Name = value
|
|
},
|
|
),
|
|
commandkit.BoolFlag(
|
|
commandkit.FlagMeta{Name: "enabled", Usage: "Enable greeting output."},
|
|
true,
|
|
func(input *GreetInput, value commandkit.Supplied[bool]) {
|
|
input.Enabled = value
|
|
},
|
|
),
|
|
},
|
|
CheckInput: func(input GreetInput) error {
|
|
if input.Count.Value < 0 {
|
|
return fmt.Errorf("count must be nonnegative")
|
|
}
|
|
return nil
|
|
},
|
|
Run: func(invocation commandkit.Invocation, input GreetInput) commandkit.Outcome {
|
|
if !input.Enabled.Value {
|
|
return commandkit.Success()
|
|
}
|
|
for i := 0; i < input.Count.Value; i++ {
|
|
if _, err := fmt.Fprintf(invocation.Streams.Out, "Hello, %s!\n", input.Name.Value); err != nil {
|
|
return commandkit.Failure(err)
|
|
}
|
|
}
|
|
return commandkit.Success()
|
|
},
|
|
})
|
|
text := commandkit.Group("text", "Text utilities.", greet)
|
|
return commandkit.New(commandkit.Identity{Name: "Example", Version: "dev"}, text)
|
|
}
|
|
```
|
|
|
|
`NewInput` is omitted, so ordinary command execution starts with a zero-valued
|
|
`GreetInput` and applies defaults/presence through bindings. Explicit `--count=0`,
|
|
`--name=`, and `--enabled=false` remain supplied. Invalid command or binding
|
|
metadata is reported by the single final `New` call, not by `greet` or `text`.
|
|
Negative count is instead a usage result from invocation-time input validation.
|
|
No integration package or upstream configuration library is imported.
|
|
|
|
For a caller that wants to execute the assembled app, the common invocation is:
|
|
|
|
```go
|
|
outcome := app.Execute(ctx, args, commandkit.Streams{Out: stdout, Err: stderr})
|
|
// The caller owns streams, cancellation, and final process exit/status mapping.
|
|
```
|
|
|
|
### 2. Context-free application configuration
|
|
|
|
```go
|
|
package configexample
|
|
|
|
import (
|
|
"context"
|
|
|
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
|
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
type AppConfig struct {
|
|
Limit int `yaml:"limit"`
|
|
}
|
|
|
|
func App(
|
|
load func(context.Context, ckconfig.Selection) (*configkit.Result[AppConfig], error),
|
|
) (*commandkit.App, error) {
|
|
return commandkit.New(
|
|
commandkit.Identity{Name: "Example", Version: "dev"},
|
|
ckconfig.AppCommand(load),
|
|
)
|
|
}
|
|
```
|
|
|
|
The parameter is the complete loader signature. It can be an application's normal
|
|
ConfigKit acquisition function directly; no unused context argument, dummy suite,
|
|
or forwarding wrapper is required. `load` is not called during construction,
|
|
installation, help, or version. An explicitly empty configuration/overlay value
|
|
is available to the loader through `Selection` with `Set == true`.
|
|
|
|
### 3. Composed application: pointer inputs, shared cache, custom command
|
|
|
|
```go
|
|
package composedexample
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
|
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
|
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
|
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckprompt"
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
promptkit "gitea.maximumdirect.net/eric/promptkit"
|
|
)
|
|
|
|
type AppConfig struct {
|
|
Limit int `yaml:"limit"`
|
|
}
|
|
|
|
type InspectionInput struct {
|
|
Campaign commandkit.Supplied[string]
|
|
Limit commandkit.Supplied[int]
|
|
}
|
|
|
|
// Application-owned cache. Loaders may use Source for shared file reads.
|
|
// Source bytes are treated as read-only by this example's loaders.
|
|
type RunCache struct {
|
|
files map[string][]byte
|
|
}
|
|
|
|
func (cache *RunCache) Source(ctx context.Context, path string) ([]byte, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if contents, ok := cache.files[path]; ok {
|
|
return contents, nil
|
|
}
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cache.files[path] = contents
|
|
return contents, nil
|
|
}
|
|
|
|
type RestoreInput struct {
|
|
ID commandkit.Supplied[string]
|
|
}
|
|
|
|
func App(
|
|
loadApp func(context.Context, ckconfig.Selection, *InspectionInput, *RunCache) (*configkit.Result[AppConfig], error),
|
|
loadEngine func(context.Context, ckconfig.Selection, *InspectionInput, *RunCache) (*promptkit.Engine, error),
|
|
restore func(context.Context, string) commandkit.Outcome,
|
|
) (*commandkit.App, error) {
|
|
parseString := func(raw string) (string, error) { return raw, nil }
|
|
campaignFlag := commandkit.ValueFlag[*InspectionInput, string](
|
|
commandkit.FlagMeta{Name: "campaign", Usage: "Select campaign inputs."},
|
|
nil, parseString,
|
|
func(input **InspectionInput, value commandkit.Supplied[string]) {
|
|
(*input).Campaign = value
|
|
},
|
|
)
|
|
limitFlag := commandkit.ValueFlag[*InspectionInput, int](
|
|
commandkit.FlagMeta{Name: "limit", Usage: "Override the application item limit."},
|
|
nil, strconv.Atoi,
|
|
func(input **InspectionInput, value commandkit.Supplied[int]) {
|
|
(*input).Limit = value
|
|
},
|
|
)
|
|
newInput := func() *InspectionInput { return &InspectionInput{} }
|
|
newState := func() *RunCache {
|
|
return &RunCache{files: make(map[string][]byte)}
|
|
}
|
|
|
|
appScope := ckconfig.FromConfigKit("app", "Application settings.", loadApp)
|
|
engineScope := ckprompt.ConfigScope(loadEngine)
|
|
configCommand := ckconfig.Command(ckconfig.Suite[*InspectionInput, *RunCache]{
|
|
NewInput: newInput, NewState: newState,
|
|
CheckInput: func(scope string, selection ckconfig.Selection, input *InspectionInput) error {
|
|
// Example application policy; it does not apply to engine inspection.
|
|
if scope == "app" && input.Campaign.Set && !input.Limit.Set {
|
|
return fmt.Errorf("application campaign inspection requires --limit")
|
|
}
|
|
return nil
|
|
},
|
|
Selectors: []ckconfig.Selector[*InspectionInput]{
|
|
{Binding: campaignFlag, Scopes: []string{"app", "promptkit"}},
|
|
{Binding: limitFlag, Scopes: []string{"app"}},
|
|
},
|
|
Scopes: []ckconfig.Scope[*InspectionInput, *RunCache]{appScope, engineScope},
|
|
})
|
|
|
|
definitions := ckprompt.FromEngine(loadEngine)
|
|
definitions.NewInput = newInput
|
|
definitions.NewState = newState
|
|
definitions.Selectors = []commandkit.Flag[*InspectionInput]{campaignFlag}
|
|
definitions.InspectPrompt = nil // Install profiles only; no prompt command.
|
|
definitionCommand := ckprompt.Command(definitions)
|
|
|
|
restoreCommand := commandkit.NewCommand(commandkit.CommandSpec[RestoreInput]{
|
|
Name: "restore", Summary: "Restore a session.",
|
|
Arguments: []commandkit.Argument[RestoreInput]{
|
|
commandkit.ValueArgument(
|
|
commandkit.ArgumentMeta{Name: "ID", Usage: "Session identifier."},
|
|
true, parseString,
|
|
func(input *RestoreInput, value commandkit.Supplied[string]) {
|
|
input.ID = value
|
|
},
|
|
),
|
|
},
|
|
Run: func(invocation commandkit.Invocation, input RestoreInput) commandkit.Outcome {
|
|
return restore(invocation.Context, input.ID.Value)
|
|
},
|
|
})
|
|
session := commandkit.Group("session", "Manage sessions.", restoreCommand)
|
|
// Domain callback validation also uses captured errors, without a helper error.
|
|
if restore == nil {
|
|
session = commandkit.WithError(session, fmt.Errorf("session restore: handler is nil"))
|
|
}
|
|
return commandkit.New(
|
|
commandkit.Identity{Name: "Example", Version: "dev"},
|
|
configCommand, definitionCommand, session,
|
|
)
|
|
}
|
|
```
|
|
|
|
Domain loaders are passed directly to the adapters, so a nil original loader
|
|
cannot be hidden by a forwarding closure. Each signature shows both the isolated
|
|
pointer input and shared cache. Loader bodies choose ConfigKit schemas and
|
|
override application rules; CommandKit does
|
|
not prescribe those application internals.
|
|
|
|
For `example config show --scope all --campaign spring --limit=0`:
|
|
|
|
1. Capability preflight selects the `app` and `promptkit` scopes.
|
|
2. `newInput` produces two distinct pointers; both receive `Campaign.Set == true`
|
|
and `Campaign.Value == "spring"`.
|
|
3. Only the app input receives `Limit.Set == true` and `Limit.Value == 0`. The
|
|
engine input retains its independently initialized, unsupplied limit.
|
|
4. `newState` produces one `*RunCache`, passed to both acquisitions. If both
|
|
loaders request the same path through `Source`, its bytes are read once within
|
|
that invocation. Sharing is application-owned, not a global CommandKit cache.
|
|
5. An app loader applies its limit override only when `input.Limit.Set`; omission
|
|
preserves its ordinary inherited configuration, while explicit zero overrides
|
|
it. `--campaign=` likewise remains an explicitly empty campaign selection.
|
|
6. Native reports are presented under the existing `config show` preparation and
|
|
delivery contract. No subsequent scope mutates the first input's selector fields.
|
|
|
|
For `example promptkit show profile briefing --campaign spring`, definition input
|
|
and state are initialized once for that invocation. Configuration scopes are not
|
|
acquired. The definition suite does not declare `--limit`, so that flag is not
|
|
accepted on the profile command. `session restore` invokes only the domain handler.
|
|
Help and scope discovery create neither cache nor pointer input.
|
|
|
|
The example's scope-aware validation permits
|
|
`example config show --scope promptkit --campaign spring` without `--limit`.
|
|
The same campaign selection for `--scope app` requires an explicitly supplied
|
|
limit, including zero. An aggregate request missing that limit fails input
|
|
validation before shared-state creation or either acquisition. This is the
|
|
example application's policy, not a new CommandKit selector requirement.
|
|
|
|
`restore` validation illustrates application wiring errors, not a requirement that
|
|
CommandKit inspect arbitrary closure internals. The application's declaration
|
|
factory records a known nil dependency because it wrapped that callback itself.
|
|
|
|
### Independent installation and supported subsets
|
|
|
|
Using the declarations assembled above, each of these is a separate assembly
|
|
choice, not another installation API:
|
|
|
|
```go
|
|
// Configuration including engine scope, without named definition inspection:
|
|
app, err := commandkit.New(identity, configCommand, session)
|
|
|
|
// Named profile inspection, without any config family:
|
|
app, err := commandkit.New(identity, definitionCommand, session)
|
|
|
|
// Add a complete optional family later through the same atomic batch API:
|
|
err := app.Register(definitionCommand)
|
|
```
|
|
|
|
These are separate illustrative snippets; they are not successive redeclarations
|
|
in one function. Importing `ckprompt` without passing one of its declarations does
|
|
not install either surface. A metadata-only configuration scope can be declared
|
|
with a name/summary, an empty `Operations` list, and no `Acquire`; it remains
|
|
visible under `config scopes`. A full adapter's capabilities can also be narrowed:
|
|
|
|
```go
|
|
appScope := ckconfig.FromConfigKit("app", "Application settings.", loadApp)
|
|
appScope.Operations = []ckconfig.Operation{ckconfig.Validate, ckconfig.Show}
|
|
```
|
|
|
|
The suite factory captures this selection and installs only supplied operations
|
|
plus discovery. It does not invent comparison or catalog providers.
|
|
|
|
## Focused design checks and expected outcomes
|
|
|
|
These checks specify observable design results for review, not a requirement to
|
|
add an exhaustive new test matrix. They use the declarations above; `identity`
|
|
has a valid name. Declaration errors are returned Go errors from installation,
|
|
not invocation status codes. Provider failures and ordinary input errors retain
|
|
the invocation model.
|
|
|
|
### Retained errors are eager evidence, not delayed preparation
|
|
|
|
```go
|
|
sentinel := errors.New("configuration declaration is incomplete")
|
|
declaration := commandkit.WithError(commandkit.NoCommand(), sentinel)
|
|
_, err := commandkit.New(identity, declaration)
|
|
// err != nil; errors.Is(err, sentinel) is true despite the absence marker.
|
|
```
|
|
|
|
This snippet uses standard-library `errors`. `WithError` needs no callback and
|
|
invokes no constructor later. A group's retained child errors and placement
|
|
metadata survive copying, wrapping, and installation in another app. Nil error
|
|
wrapping preserves the underlying declaration, including invalid zero commands.
|
|
|
|
### Invalid wiring versus missing capability
|
|
|
|
```go
|
|
var missingLoader func(context.Context, ckconfig.Selection) (*configkit.Result[AppConfig], error)
|
|
_, err := commandkit.New(identity, ckconfig.AppCommand(missingLoader))
|
|
// Installation fails with config/app loader context; no loader runs.
|
|
|
|
emptyDefinitions := ckprompt.Command(ckprompt.Suite[struct{}, struct{}]{})
|
|
app, err := commandkit.New(identity, emptyDefinitions)
|
|
// Success; no promptkit family appears in help.
|
|
```
|
|
|
|
An engine adapter created from a typed nil engine loader fails installation even
|
|
if its inspection callbacks are later cleared. In contrast, a manually composed
|
|
suite with one non-nil inspection provider and the other absent installs only the
|
|
supplied capability. Metadata validation does not require every family operation.
|
|
A valid provider that later returns a load/lookup error reports operational status
|
|
1 with supported public error identity and its captured diagnostics.
|
|
|
|
### Collisions, placement, and per-call atomicity
|
|
|
|
```go
|
|
existing := commandkit.Group("config", "Application-owned configuration tools.")
|
|
app, err := commandkit.New(identity, existing)
|
|
// Success while no standard config family is installed under the selected rule.
|
|
err = app.Register(ckconfig.AppCommand(load))
|
|
// Collision; the original domain group remains unchanged.
|
|
|
|
wrapper := commandkit.Group("tools", "Utilities.", ckconfig.AppCommand(load))
|
|
_, err = commandkit.New(identity, wrapper)
|
|
// Root-only placement error, not a new tools/config standard command path.
|
|
```
|
|
|
|
Installing the standard family first and then attempting a domain `config` group
|
|
also fails. Neither direction merges children or replaces handlers. Installing
|
|
`Group("help", ...)` or `Group("version", ...)` always collides with built-ins.
|
|
For `app.Register(goodNewCommand, conflictingCommand)`, neither command is added
|
|
when the batch fails; commands installed by earlier successful calls remain.
|
|
A failed `New` returns no usable app. This is registry atomicity, not rollback
|
|
of arbitrary effects inside application callbacks.
|
|
|
|
### Empty declarations
|
|
|
|
| Input | Expected installation result |
|
|
| --- | --- |
|
|
| `ckconfig.Command(ckconfig.Suite[struct{}, struct{}]{})` | Error: no configuration scopes |
|
|
| One named scope with empty operations and nil acquisition | Success; `config scopes` describes it, no inspection verbs supplied by that scope |
|
|
| Valid definition suite with no callbacks | Explicit absence, no namespace claimed |
|
|
| `NoCommand()` beside ordinary children | Skipped, no placeholder help item |
|
|
| `WithError(NoCommand(), sentinel)` | Error retaining sentinel identity |
|
|
| Zero `Command{}` at the root or as a child | Declaration error |
|
|
| `RootGroup("config", ..., child)` under an ordinary parent | Placement error |
|
|
|
|
An empty definition no-op nested under `Group("tools", ...)` remains absence,
|
|
not a misplaced real family. A real empty `RootGroup` is still subject to placement
|
|
and collision checks. Configuration and definition factories choose the relevant
|
|
empty-family behavior before producing these declarations.
|
|
|
|
### Capture and reuse
|
|
|
|
```go
|
|
scope := ckconfig.FromConfigKit("app", "Application settings.", loadApp)
|
|
suite := ckconfig.Suite[*InspectionInput, *RunCache]{
|
|
NewInput: newInput, NewState: newState,
|
|
Selectors: []ckconfig.Selector[*InspectionInput]{
|
|
{Binding: campaignFlag, Scopes: []string{"app"}},
|
|
},
|
|
Scopes: []ckconfig.Scope[*InspectionInput, *RunCache]{scope},
|
|
}
|
|
declaration := ckconfig.Command(suite)
|
|
suite.Scopes[0].Summary = "Changed source metadata"
|
|
suite.Scopes[0].Operations = []ckconfig.Operation{ckconfig.Validate}
|
|
suite.Selectors[0].Scopes[0] = "another-scope"
|
|
|
|
first, firstErr := commandkit.New(identity, declaration)
|
|
second, secondErr := commandkit.New(identity, declaration)
|
|
// Both installations use the original captured scope, operation, and selector metadata.
|
|
```
|
|
|
|
The variables here have the types from the composed example. Original captured
|
|
operations remain available, the summary remains unchanged, and the selector
|
|
still applies to `app`; the later source edit is not revalidated as though it
|
|
were the returned command. Registry and invocation state are independent in
|
|
`first` and `second`. `firstErr` and `secondErr` are nil for otherwise valid wiring.
|
|
Callback references are shared intentionally, so mutable objects captured inside
|
|
those callbacks remain application-owned. Reusing a declaration does not authorize
|
|
concurrent execution or clone a callback's object graph.
|
|
|
|
### Initialization and scope isolation
|
|
|
|
When its preflight and input checks succeed, the composed example's selected
|
|
aggregate inspection calls `newInput` twice and `newState` once. It delivers
|
|
distinct `*InspectionInput` addresses but the same
|
|
`*RunCache` to the two providers. Retaining the app input in a provider does not
|
|
expose it to later engine-selector writes. Selecting one scope calls each factory
|
|
once; selecting profile inspection initializes only its own invocation.
|
|
|
|
Without input selectors, a suite may deliberately use pointer `I` with no
|
|
`NewInput` and a loader accepting nil. With selectors whose accessors dereference
|
|
that pointer, it supplies the input factory, just as a pointer-shaped application
|
|
command does. Without `NewState`, pointer `S` is nil; providers that require a
|
|
cache supply the state factory. A fresh parsed-value store does not imply that
|
|
arbitrary application factories returning aliases are repaired or deep-copied.
|
|
|
|
### Scope validation, flag identity, and error categories
|
|
|
|
| Situation | Expected result |
|
|
| --- | --- |
|
|
| Campaign selected for the example's app scope, with no limit | Scope-aware `CheckInput` returns a usage error; no shared state or acquisition |
|
|
| Same campaign selected for only the engine scope | Validation receives scope `promptkit`; the app-only limit rule does not apply |
|
|
| A required applicable flag was not supplied | `ApplyTo` returns an error matching `ErrFlagInput`; usage outcome |
|
|
| A required flag applies only to an unselected scope | Its requirement is not enforced for the selected scope |
|
|
| A copied original binding is selected from its captured values | Identity matches; values apply to the initialized input |
|
|
| A newly constructed lookalike binding is selected instead | It is absent from that captured set; error matches `ErrFlagWiring` |
|
|
| The destination pointer passed to `ApplyTo` is nil | Error matches `ErrFlagWiring` |
|
|
| An `ApplyTo` error is wrapped with family/scope context | `errors.Is` still identifies the category; wiring failures remain operational |
|
|
| Input validation fails before later scopes are initialized | Later input factories need not run; `NewState` and acquisitions do not run |
|
|
|
|
The flag-required checks here describe binding metadata, while the campaign/limit
|
|
relationship is the example's application validation rule. Neither requires
|
|
examining error-message text. `ErrFlagInput` and `ErrFlagWiring` classify errors
|
|
reported by flag application; they do not replace captured installation errors
|
|
or upstream provider errors.
|
|
|
|
## Namespace reconciliation and implementation handoff
|
|
|
|
The user-selected lifetime is precise: help/version are always reserved;
|
|
configuration and PromptKit standard roots are owned only when their real family
|
|
is installed. An absent/no-op family reserves nothing. A domain group using an
|
|
uninstalled integration name is permitted, but blocks later installation of the
|
|
same root with a collision. Root-only placement is independent of that lifetime.
|
|
|
|
This clarifies an ambiguity rather than assuming that earlier broad reservation
|
|
wording already specifies the same lifetime. The durable documents need deliberate
|
|
alignment before implementation:
|
|
|
|
| Owner | Required alignment after final proposal review |
|
|
| --- | --- |
|
|
| ADR 0001 | Clarify the reservation lifetime through the ADR change process if its accepted wording is interpreted as unconditional reservation |
|
|
| ADR 0010 | Retain core/integration dependency and convenient-composition boundaries; exact factories now return captured declarations |
|
|
| Architecture policy | State installed-family ownership uniformly for config and promptkit; align its public facade rules with ADR 0010 |
|
|
| Module roadmap | Replace the open registration alternative with this selected uniform model and reviewed supporting signatures |
|
|
| Staged module implementation plan | Specify eager capture, error retention, neutral bindings, scope isolation, examples, and release adoption using the reviewed API |
|
|
|
|
A new or superseding ADR can record the lifetime clarification where needed;
|
|
accepted decision text is not revised silently. This authoring pass updates only
|
|
this proposal, leaving policy/ADR reconciliation and the staged plan for the
|
|
explicit post-review handoff requested above. It does not claim those documents
|
|
are already aligned or authorize implementation.
|
|
|
|
## Review checklist
|
|
|
|
The requested declaration/example work is now present:
|
|
|
|
1. Core-only typed command/group construction with a single installation boundary.
|
|
2. Context-free `AppCommand` with the complete loader signature and no dummy state.
|
|
3. Visible composed suite assembly, application command, and independent capability
|
|
selection.
|
|
4. Concrete `WithError`, `RootGroup`, and `NoCommand` contracts, with no delayed
|
|
preparation or private-state access.
|
|
5. Typed selector composition with stable binding identity, scope-aware validation,
|
|
usage/wiring error categories, distinct pointer inputs, and one shared
|
|
invocation cache through `NewState`, with normal and failure-path timing.
|
|
6. Focused expected outcomes for invalid wiring, collisions, placement, emptiness,
|
|
capture, and reuse.
|
|
7. An explicit durable-document reconciliation and post-review implementation-plan
|
|
handoff, without prematurely editing accepted records or implementing code.
|
|
|
|
Final review should particularly consider the proposed `Suite[I, S]` separation
|
|
and the narrow `CaptureFlags`/`FlagValues` composition seam. These are now concrete
|
|
supporting API choices rather than unresolved isolation mechanics. The uniform
|
|
declaration model is selected; these names and details await the user's final
|
|
review before implementation.
|