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.
|
applicability; definition selectors need no configuration-specific wrapper.
|
||||||
Explicit empty, false, and zero values remain distinguishable from omission.
|
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
|
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
|
state or prohibit a loader that intentionally accepts nil. Applications supply a
|
||||||
factory when initialized state is needed. A supplied factory returning nil retains
|
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
|
Scope-specific parsed values stay separate from shared acquisition state through
|
||||||
the proposed `Suite[I, S]`: `NewInput` initializes each selected scope's input,
|
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,
|
Providers receive both explicitly. Pointer inputs are independently initialized,
|
||||||
not shallow copies of a single pointer. The detailed lifetime table below makes
|
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
|
this change from the comparison's mixed context model explicit; it does not
|
||||||
@@ -418,6 +418,12 @@ type FlagValues[I any] struct {
|
|||||||
// Unexported representation.
|
// 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](
|
func CaptureFlags[Outer, I any](
|
||||||
flags []Flag[I],
|
flags []Flag[I],
|
||||||
target func(*Outer) *FlagValues[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/
|
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.
|
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`
|
Execution-required checks for captured inner flags are deferred until `ApplyTo`
|
||||||
selects the applicable subset; a required app-only selector must not become
|
selects the applicable subset; a required app-only selector must not become
|
||||||
mandatory for a PromptKit-only scope request. Missing required selected values
|
mandatory for a PromptKit-only scope request. Missing required selected values
|
||||||
are input-validation errors, routed through the standard command's ordinary
|
match `ErrFlagInput` through `errors.Is` and produce usage outcomes. An absent
|
||||||
`CheckInput`/usage path before shared-state initialization or acquisition. Help
|
binding or nil destination matches `ErrFlagWiring` and produces an operational
|
||||||
uses metadata without applying values or requiring execution inputs.
|
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;
|
Core-owned repeatable-value slices are independent per application of the values;
|
||||||
custom parsed objects and accessor effects retain application ownership. This
|
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 {
|
type Suite[I, S any] struct {
|
||||||
NewInput func() I
|
NewInput func() I
|
||||||
NewContext func() S
|
NewState func() S
|
||||||
Selectors []Selector[I]
|
Selectors []Selector[I]
|
||||||
CheckInput func(Selection, I) error
|
CheckInput func(scope string, selection Selection, input I) error
|
||||||
Scopes []Scope[I, S]
|
Scopes []Scope[I, S]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,8 +587,12 @@ standard-flag collisions are declaration errors. Scope identifiers, capability
|
|||||||
selection, defaults, and aggregate availability follow
|
selection, defaults, and aggregate availability follow
|
||||||
[ADR 0009](../adr/0009-configuration-scopes.md). Non-applicable selectors are not
|
[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.
|
applied to a scope's input; this adds no new unused-selector rejection policy.
|
||||||
`CheckInput` validates the selection and initialized input for each requested
|
`CheckInput` receives the registered name of each requested scope alongside its
|
||||||
scope before acquisition. It may inspect presence without loading configuration.
|
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
|
`FromConfigKit` declares the operations supported by the selected ConfigKit
|
||||||
adapter and records the original loader, including an invalid nil loader.
|
adapter and records the original loader, including an invalid nil loader.
|
||||||
@@ -609,7 +637,7 @@ type ProfileRequest struct {
|
|||||||
|
|
||||||
type Suite[I, S any] struct {
|
type Suite[I, S any] struct {
|
||||||
NewInput func() I
|
NewInput func() I
|
||||||
NewContext func() S
|
NewState func() S
|
||||||
Selectors []commandkit.Flag[I]
|
Selectors []commandkit.Flag[I]
|
||||||
CheckInput func(ckconfig.Selection, I) error
|
CheckInput func(ckconfig.Selection, I) error
|
||||||
InspectPrompt func(context.Context, PromptRequest, I, S) (commandkit.Acquired[*promptkit.PromptInspection], 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
|
### 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 |
|
| Construct declarations / `New` / `Register` | Not called | Not called | Not called |
|
||||||
| Help, version, `config scopes` | 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.
|
Input conversion and scope/capability preflight use metadata without acquisition.
|
||||||
For configuration execution, initialize and populate each requested scope input,
|
For configuration execution, initialize and populate each requested scope input,
|
||||||
then run its `CheckInput`; initialize the shared state before acquisition after
|
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:
|
or unrelated families. This explicitly separates the former mixed-context role:
|
||||||
`NewContext` remains once per invocation, and the new `NewInput` initializes scope
|
`NewState` names the shared acquisition-state factory, distinct from
|
||||||
storage independently. No existing callback is silently called once per scope.
|
`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
|
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.
|
is permitted when the receiving binding callbacks and providers support it.
|
||||||
Applications requiring allocated pointer input supply `NewInput`; those requiring
|
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
|
return shared mutable input objects retain caller-owned effects; CommandKit does
|
||||||
not promise to repair those aliases. For scope-independent CLI storage, return
|
not promise to repair those aliases. For scope-independent CLI storage, return
|
||||||
fresh input storage as demonstrated below. Pointer shared state is supported and
|
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.
|
callback contract without a later scope overwriting it through a shared template.
|
||||||
Subsequent invocations initialize new input/state; registration does not retain
|
Subsequent invocations initialize new input/state; registration does not retain
|
||||||
an invocation cache. No concurrent-dispatch guarantee follows from these rules.
|
an invocation cache. No concurrent-dispatch guarantee follows from these rules.
|
||||||
|
|
||||||
## Complete consumer registration examples
|
## Complete consumer registration examples
|
||||||
|
|
||||||
These examples target the proposed APIs, not today's prototype. Each block is a
|
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{} }
|
newInput := func() *InspectionInput { return &InspectionInput{} }
|
||||||
newContext := func() *RunCache {
|
newState := func() *RunCache {
|
||||||
return &RunCache{files: make(map[string][]byte)}
|
return &RunCache{files: make(map[string][]byte)}
|
||||||
}
|
}
|
||||||
|
|
||||||
appScope := ckconfig.FromConfigKit("app", "Application settings.", loadApp)
|
appScope := ckconfig.FromConfigKit("app", "Application settings.", loadApp)
|
||||||
engineScope := ckprompt.ConfigScope(loadEngine)
|
engineScope := ckprompt.ConfigScope(loadEngine)
|
||||||
configCommand := ckconfig.Command(ckconfig.Suite[*InspectionInput, *RunCache]{
|
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]{
|
Selectors: []ckconfig.Selector[*InspectionInput]{
|
||||||
{Binding: campaignFlag, Scopes: []string{"app", "promptkit"}},
|
{Binding: campaignFlag, Scopes: []string{"app", "promptkit"}},
|
||||||
{Binding: limitFlag, Scopes: []string{"app"}},
|
{Binding: limitFlag, Scopes: []string{"app"}},
|
||||||
@@ -911,7 +956,7 @@ func App(
|
|||||||
|
|
||||||
definitions := ckprompt.FromEngine(loadEngine)
|
definitions := ckprompt.FromEngine(loadEngine)
|
||||||
definitions.NewInput = newInput
|
definitions.NewInput = newInput
|
||||||
definitions.NewContext = newContext
|
definitions.NewState = newState
|
||||||
definitions.Selectors = []commandkit.Flag[*InspectionInput]{campaignFlag}
|
definitions.Selectors = []commandkit.Flag[*InspectionInput]{campaignFlag}
|
||||||
definitions.InspectPrompt = nil // Install profiles only; no prompt command.
|
definitions.InspectPrompt = nil // Install profiles only; no prompt command.
|
||||||
definitionCommand := ckprompt.Command(definitions)
|
definitionCommand := ckprompt.Command(definitions)
|
||||||
@@ -956,7 +1001,7 @@ For `example config show --scope all --campaign spring --limit=0`:
|
|||||||
and `Campaign.Value == "spring"`.
|
and `Campaign.Value == "spring"`.
|
||||||
3. Only the app input receives `Limit.Set == true` and `Limit.Value == 0`. The
|
3. Only the app input receives `Limit.Set == true` and `Limit.Value == 0`. The
|
||||||
engine input retains its independently initialized, unsupplied limit.
|
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
|
loaders request the same path through `Source`, its bytes are read once within
|
||||||
that invocation. Sharing is application-owned, not a global CommandKit cache.
|
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
|
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.
|
accepted on the profile command. `session restore` invokes only the domain handler.
|
||||||
Help and scope discovery create neither cache nor pointer input.
|
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
|
`restore` validation illustrates application wiring errors, not a requirement that
|
||||||
CommandKit inspect arbitrary closure internals. The application's declaration
|
CommandKit inspect arbitrary closure internals. The application's declaration
|
||||||
factory records a known nil dependency because it wrapped that callback itself.
|
factory records a known nil dependency because it wrapped that callback itself.
|
||||||
@@ -1090,7 +1142,7 @@ empty-family behavior before producing these declarations.
|
|||||||
```go
|
```go
|
||||||
scope := ckconfig.FromConfigKit("app", "Application settings.", loadApp)
|
scope := ckconfig.FromConfigKit("app", "Application settings.", loadApp)
|
||||||
suite := ckconfig.Suite[*InspectionInput, *RunCache]{
|
suite := ckconfig.Suite[*InspectionInput, *RunCache]{
|
||||||
NewInput: newInput, NewContext: newContext,
|
NewInput: newInput, NewState: newState,
|
||||||
Selectors: []ckconfig.Selector[*InspectionInput]{
|
Selectors: []ckconfig.Selector[*InspectionInput]{
|
||||||
{Binding: campaignFlag, Scopes: []string{"app"}},
|
{Binding: campaignFlag, Scopes: []string{"app"}},
|
||||||
},
|
},
|
||||||
@@ -1117,8 +1169,9 @@ concurrent execution or clone a callback's object graph.
|
|||||||
|
|
||||||
### Initialization and scope isolation
|
### Initialization and scope isolation
|
||||||
|
|
||||||
The composed example's selected aggregate inspection calls `newInput` twice and
|
When its preflight and input checks succeed, the composed example's selected
|
||||||
`newContext` once. It delivers distinct `*InspectionInput` addresses but the same
|
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
|
`*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
|
expose it to later engine-selector writes. Selecting one scope calls each factory
|
||||||
once; selecting profile inspection initializes only its own invocation.
|
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
|
Without input selectors, a suite may deliberately use pointer `I` with no
|
||||||
`NewInput` and a loader accepting nil. With selectors whose accessors dereference
|
`NewInput` and a loader accepting nil. With selectors whose accessors dereference
|
||||||
that pointer, it supplies the input factory, just as a pointer-shaped application
|
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
|
cache supply the state factory. A fresh parsed-value store does not imply that
|
||||||
arbitrary application factories returning aliases are repaired or deep-copied.
|
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
|
## Namespace reconciliation and implementation handoff
|
||||||
|
|
||||||
The user-selected lifetime is precise: help/version are always reserved;
|
The user-selected lifetime is precise: help/version are always reserved;
|
||||||
@@ -1166,8 +1239,9 @@ The requested declaration/example work is now present:
|
|||||||
selection.
|
selection.
|
||||||
4. Concrete `WithError`, `RootGroup`, and `NoCommand` contracts, with no delayed
|
4. Concrete `WithError`, `RootGroup`, and `NoCommand` contracts, with no delayed
|
||||||
preparation or private-state access.
|
preparation or private-state access.
|
||||||
5. Typed selector composition, distinct pointer inputs, explicit presence, and
|
5. Typed selector composition with stable binding identity, scope-aware validation,
|
||||||
one shared invocation cache with documented factory timing.
|
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,
|
6. Focused expected outcomes for invalid wiring, collisions, placement, emptiness,
|
||||||
capture, and reuse.
|
capture, and reuse.
|
||||||
7. An explicit durable-document reconciliation and post-review implementation-plan
|
7. An explicit durable-document reconciliation and post-review implementation-plan
|
||||||
|
|||||||
Reference in New Issue
Block a user