Clarify registration validation and invocation state contracts
This commit is contained in:
@@ -193,7 +193,7 @@ 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 `NewContext` supplies the zero value of its respective
|
||||
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
|
||||
@@ -201,7 +201,7 @@ 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 `NewContext` initializes `S` once for the selected inspection invocation.
|
||||
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
|
||||
@@ -418,6 +418,12 @@ 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],
|
||||
@@ -440,12 +446,30 @@ 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
|
||||
are input-validation errors, routed through the standard command's ordinary
|
||||
`CheckInput`/usage path before shared-state initialization or acquisition. Help
|
||||
uses metadata without applying values or requiring execution inputs.
|
||||
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
|
||||
@@ -531,9 +555,9 @@ type Selector[I any] struct {
|
||||
|
||||
type Suite[I, S any] struct {
|
||||
NewInput func() I
|
||||
NewContext func() S
|
||||
NewState func() S
|
||||
Selectors []Selector[I]
|
||||
CheckInput func(Selection, I) error
|
||||
CheckInput func(scope string, selection Selection, input I) error
|
||||
Scopes []Scope[I, S]
|
||||
}
|
||||
|
||||
@@ -563,8 +587,12 @@ 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` validates the selection and initialized input for each requested
|
||||
scope before acquisition. It may inspect presence without loading configuration.
|
||||
`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.
|
||||
@@ -609,7 +637,7 @@ type ProfileRequest struct {
|
||||
|
||||
type Suite[I, S any] struct {
|
||||
NewInput func() I
|
||||
NewContext func() S
|
||||
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)
|
||||
@@ -662,7 +690,14 @@ change to unified construction or the independently owned suite principle.
|
||||
|
||||
### Factory timing
|
||||
|
||||
| Path | `NewInput` | Suite `NewContext` | Acquisition |
|
||||
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 |
|
||||
@@ -674,15 +709,17 @@ change to unified construction or the independently owned suite principle.
|
||||
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/context callbacks do not run for unselected scopes
|
||||
input checks succeed. The input/state callbacks do not run for unselected scopes
|
||||
or unrelated families. This explicitly separates the former mixed-context role:
|
||||
`NewContext` remains once per invocation, and the new `NewInput` initializes scope
|
||||
storage independently. No existing callback is silently called once per scope.
|
||||
`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 `NewContext`. Factories that deliberately
|
||||
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
|
||||
@@ -695,6 +732,7 @@ 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
|
||||
@@ -894,14 +932,21 @@ func App(
|
||||
},
|
||||
)
|
||||
newInput := func() *InspectionInput { return &InspectionInput{} }
|
||||
newContext := func() *RunCache {
|
||||
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, NewContext: newContext,
|
||||
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"}},
|
||||
@@ -911,7 +956,7 @@ func App(
|
||||
|
||||
definitions := ckprompt.FromEngine(loadEngine)
|
||||
definitions.NewInput = newInput
|
||||
definitions.NewContext = newContext
|
||||
definitions.NewState = newState
|
||||
definitions.Selectors = []commandkit.Flag[*InspectionInput]{campaignFlag}
|
||||
definitions.InspectPrompt = nil // Install profiles only; no prompt command.
|
||||
definitionCommand := ckprompt.Command(definitions)
|
||||
@@ -956,7 +1001,7 @@ For `example config show --scope all --campaign spring --limit=0`:
|
||||
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. `newContext` produces one `*RunCache`, passed to both acquisitions. If both
|
||||
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
|
||||
@@ -971,6 +1016,13 @@ 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.
|
||||
@@ -1090,7 +1142,7 @@ empty-family behavior before producing these declarations.
|
||||
```go
|
||||
scope := ckconfig.FromConfigKit("app", "Application settings.", loadApp)
|
||||
suite := ckconfig.Suite[*InspectionInput, *RunCache]{
|
||||
NewInput: newInput, NewContext: newContext,
|
||||
NewInput: newInput, NewState: newState,
|
||||
Selectors: []ckconfig.Selector[*InspectionInput]{
|
||||
{Binding: campaignFlag, Scopes: []string{"app"}},
|
||||
},
|
||||
@@ -1117,8 +1169,9 @@ concurrent execution or clone a callback's object graph.
|
||||
|
||||
### Initialization and scope isolation
|
||||
|
||||
The composed example's selected aggregate inspection calls `newInput` twice and
|
||||
`newContext` once. It delivers distinct `*InspectionInput` addresses but the same
|
||||
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.
|
||||
@@ -1126,10 +1179,30 @@ 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 `NewContext`, pointer `S` is nil; providers that require a
|
||||
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;
|
||||
@@ -1166,8 +1239,9 @@ The requested declaration/example work is now present:
|
||||
selection.
|
||||
4. Concrete `WithError`, `RootGroup`, and `NoCommand` contracts, with no delayed
|
||||
preparation or private-state access.
|
||||
5. Typed selector composition, distinct pointer inputs, explicit presence, and
|
||||
one shared invocation cache with documented factory timing.
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user