Compile configuration selector metadata during capture
This commit is contained in:
@@ -3,7 +3,8 @@
|
|||||||
Reviewed on 2026-10-11, against committed implementation
|
Reviewed on 2026-10-11, against committed implementation
|
||||||
`cc574915ee17e2d738f27874d2c8857aa1f51a47`. The implementation sprint starts
|
`cc574915ee17e2d738f27874d2c8857aa1f51a47`. The implementation sprint starts
|
||||||
after planning commit `0d23676144ab1ce5047924e98f1ab6a2a9d6aeb6`.
|
after planning commit `0d23676144ab1ce5047924e98f1ab6a2a9d6aeb6`.
|
||||||
Completion status and evidence are recorded per issue below. P1 identifies a functional contract violation; P2
|
Completion status and evidence are recorded per issue below. P1 identifies a
|
||||||
|
functional contract violation; P2
|
||||||
identifies architecture or documentation corrections; P3 identifies smaller
|
identifies architecture or documentation corrections; P3 identifies smaller
|
||||||
maintenance corrections. These priorities do not add publication gates.
|
maintenance corrections. These priorities do not add publication gates.
|
||||||
|
|
||||||
@@ -82,7 +83,16 @@ Avoid assertions about a particular private representation.
|
|||||||
|
|
||||||
## R2: Finish declaration-time compilation of configuration metadata
|
## R2: Finish declaration-time compilation of configuration metadata
|
||||||
|
|
||||||
**Status:** Open. **Priority:** P2.
|
**Status:** Completed (2026-10-11). **Priority:** P2.
|
||||||
|
|
||||||
|
**Completed work:** family capture orders scope references and prepares each
|
||||||
|
scope's original binding list, each operation's binding union, and scope-choice
|
||||||
|
text. Inspection preparation reads those lists without static sorting or
|
||||||
|
membership compilation. A behavioral regression covers operation-specific flags
|
||||||
|
and capture of capabilities/applicability/handles after source edits. Existing
|
||||||
|
ordering, metadata-only, unused/required-selector, isolated-input, and shared-state
|
||||||
|
coverage passes; all three module suites pass offline with Go 1.26.8. No provider
|
||||||
|
cache or performance guarantee was introduced.
|
||||||
|
|
||||||
**Evidence:** [ckconfig/input.go](../../integrations/ckconfig/input.go),
|
**Evidence:** [ckconfig/input.go](../../integrations/ckconfig/input.go),
|
||||||
`newConfigOperationCommand`, `selectorIndexesForOperation`, and
|
`newConfigOperationCommand`, `selectorIndexesForOperation`, and
|
||||||
|
|||||||
@@ -23,20 +23,21 @@ type scopeInventory struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type preparedScope[I, S any] struct {
|
type preparedScope[I, S any] struct {
|
||||||
scope Scope[I, S]
|
scope Scope[I, S]
|
||||||
selectorIndexes []int
|
bindings []commandkit.Flag[I]
|
||||||
}
|
}
|
||||||
|
|
||||||
type preparedSuite[I, S any] struct {
|
type preparedSuite[I, S any] struct {
|
||||||
newInput func() I
|
newInput func() I
|
||||||
newState func() S
|
newState func() S
|
||||||
checkInput func(string, Selection, I) error
|
checkInput func(string, Selection, I) error
|
||||||
scopes []preparedScope[I, S]
|
scopes []*preparedScope[I, S]
|
||||||
byName map[string]*preparedScope[I, S]
|
byName map[string]*preparedScope[I, S]
|
||||||
selectors []Selector[I]
|
bindingsByOperation map[Operation][]commandkit.Flag[I]
|
||||||
operations []Operation
|
scopeChoices string
|
||||||
inventory scopeInventory
|
operations []Operation
|
||||||
issues []error
|
inventory scopeInventory
|
||||||
|
issues []error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command eagerly captures a configuration suite as a root-only config family.
|
// Command eagerly captures a configuration suite as a root-only config family.
|
||||||
@@ -80,15 +81,16 @@ func AppCommand[T any](load func(context.Context, Selection) (*configkit.Result[
|
|||||||
|
|
||||||
func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
||||||
prepared := &preparedSuite[I, S]{
|
prepared := &preparedSuite[I, S]{
|
||||||
newInput: suite.NewInput,
|
newInput: suite.NewInput,
|
||||||
newState: suite.NewState,
|
newState: suite.NewState,
|
||||||
checkInput: suite.CheckInput,
|
checkInput: suite.CheckInput,
|
||||||
byName: make(map[string]*preparedScope[I, S], len(suite.Scopes)),
|
byName: make(map[string]*preparedScope[I, S], len(suite.Scopes)),
|
||||||
selectors: make([]Selector[I], len(suite.Selectors)),
|
bindingsByOperation: make(map[Operation][]commandkit.Flag[I]),
|
||||||
}
|
}
|
||||||
|
selectors := make([]Selector[I], len(suite.Selectors))
|
||||||
for index, selector := range suite.Selectors {
|
for index, selector := range suite.Selectors {
|
||||||
selector.Scopes = append([]string(nil), selector.Scopes...)
|
selector.Scopes = append([]string(nil), selector.Scopes...)
|
||||||
prepared.selectors[index] = selector
|
selectors[index] = selector
|
||||||
}
|
}
|
||||||
|
|
||||||
seenNames := make(map[string]struct{}, len(suite.Scopes))
|
seenNames := make(map[string]struct{}, len(suite.Scopes))
|
||||||
@@ -113,16 +115,19 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
|||||||
if len(scope.Operations) > 0 && scope.Acquire == nil {
|
if len(scope.Operations) > 0 && scope.Acquire == nil {
|
||||||
prepared.issues = append(prepared.issues, fmt.Errorf("configuration scope %q declares operations without an acquisition callback", scope.Name))
|
prepared.issues = append(prepared.issues, fmt.Errorf("configuration scope %q declares operations without an acquisition callback", scope.Name))
|
||||||
}
|
}
|
||||||
prepared.scopes = append(prepared.scopes, preparedScope[I, S]{scope: scope})
|
prepared.scopes = append(prepared.scopes, &preparedScope[I, S]{scope: scope})
|
||||||
}
|
}
|
||||||
// Appending above may move the backing array. Rebuild the lookup only after
|
sort.SliceStable(prepared.scopes, func(i, j int) bool {
|
||||||
// the ordered scope slice is complete.
|
return scopeOrder(prepared.scopes[i].scope.Name) < scopeOrder(prepared.scopes[j].scope.Name)
|
||||||
prepared.byName = make(map[string]*preparedScope[I, S], len(prepared.scopes))
|
})
|
||||||
for index := range prepared.scopes {
|
names := make([]string, 0, len(prepared.scopes))
|
||||||
prepared.byName[prepared.scopes[index].scope.Name] = &prepared.scopes[index]
|
for _, scope := range prepared.scopes {
|
||||||
|
prepared.byName[scope.scope.Name] = scope
|
||||||
|
names = append(names, scope.scope.Name)
|
||||||
}
|
}
|
||||||
|
prepared.scopeChoices = joinNames(names)
|
||||||
|
|
||||||
for index, selector := range prepared.selectors {
|
for index, selector := range selectors {
|
||||||
seen := make(map[string]struct{}, len(selector.Scopes))
|
seen := make(map[string]struct{}, len(selector.Scopes))
|
||||||
uniqueScopes := make([]string, 0, len(selector.Scopes))
|
uniqueScopes := make([]string, 0, len(selector.Scopes))
|
||||||
for _, name := range selector.Scopes {
|
for _, name := range selector.Scopes {
|
||||||
@@ -135,17 +140,17 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
|||||||
seen[name] = struct{}{}
|
seen[name] = struct{}{}
|
||||||
uniqueScopes = append(uniqueScopes, name)
|
uniqueScopes = append(uniqueScopes, name)
|
||||||
}
|
}
|
||||||
prepared.selectors[index].Scopes = uniqueScopes
|
selectors[index].Scopes = uniqueScopes
|
||||||
if len(selector.Scopes) == 0 {
|
if len(selector.Scopes) == 0 {
|
||||||
for scopeIndex := range prepared.scopes {
|
for scopeIndex := range prepared.scopes {
|
||||||
if len(prepared.scopes[scopeIndex].scope.Operations) > 0 {
|
if len(prepared.scopes[scopeIndex].scope.Operations) > 0 {
|
||||||
prepared.scopes[scopeIndex].selectorIndexes = append(prepared.scopes[scopeIndex].selectorIndexes, index)
|
prepared.scopes[scopeIndex].bindings = append(prepared.scopes[scopeIndex].bindings, selector.Binding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, name := range uniqueScopes {
|
for _, name := range uniqueScopes {
|
||||||
if scope, exists := prepared.byName[name]; exists && len(scope.scope.Operations) > 0 {
|
if scope, exists := prepared.byName[name]; exists && len(scope.scope.Operations) > 0 {
|
||||||
scope.selectorIndexes = append(scope.selectorIndexes, index)
|
scope.bindings = append(scope.bindings, selector.Binding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,8 +163,16 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, selector := range selectors {
|
||||||
|
for _, scope := range prepared.scopes {
|
||||||
|
if containsOperation(scope.scope.Operations, operation) && selectorApplies(selector.Scopes, scope.scope.Name) {
|
||||||
|
prepared.bindingsByOperation[operation] = append(prepared.bindingsByOperation[operation], selector.Binding)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, scope := range orderedScopeRecords(prepared.scopes) {
|
for _, scope := range prepared.scopes {
|
||||||
prepared.inventory.Scopes = append(prepared.inventory.Scopes, scopeMetadata{
|
prepared.inventory.Scopes = append(prepared.inventory.Scopes, scopeMetadata{
|
||||||
Name: scope.scope.Name,
|
Name: scope.scope.Name,
|
||||||
Operations: append([]Operation(nil), scope.scope.Operations...),
|
Operations: append([]Operation(nil), scope.scope.Operations...),
|
||||||
@@ -187,14 +200,6 @@ func normalizeOperations(operations []Operation, issues *[]error, scope string)
|
|||||||
return ordered
|
return ordered
|
||||||
}
|
}
|
||||||
|
|
||||||
func orderedScopeRecords[I, S any](scopes []preparedScope[I, S]) []preparedScope[I, S] {
|
|
||||||
ordered := append([]preparedScope[I, S](nil), scopes...)
|
|
||||||
sort.SliceStable(ordered, func(i, j int) bool {
|
|
||||||
return scopeOrder(ordered[i].scope.Name) < scopeOrder(ordered[j].scope.Name)
|
|
||||||
})
|
|
||||||
return ordered
|
|
||||||
}
|
|
||||||
|
|
||||||
func scopeOrder(name string) string {
|
func scopeOrder(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
case "app":
|
case "app":
|
||||||
|
|||||||
@@ -47,6 +47,60 @@ func TestScopeDiscoveryIsOrderedAndDoesNotUseSuiteFactories(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOperationSelectorsAreCapturedForSupportedScopes(t *testing.T) {
|
||||||
|
type input struct{ Tenant, Campaign commandkit.Supplied[string] }
|
||||||
|
tenant := commandkit.ValueFlag(commandkit.FlagMeta{Name: "tenant", Required: true}, (*string)(nil), parseConfigString,
|
||||||
|
func(target *input, value commandkit.Supplied[string]) { target.Tenant = value })
|
||||||
|
campaign := commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign", Required: true}, (*string)(nil), parseConfigString,
|
||||||
|
func(target *input, value commandkit.Supplied[string]) { target.Campaign = value })
|
||||||
|
calls := 0
|
||||||
|
suite := Suite[input, struct{}]{
|
||||||
|
Selectors: []Selector[input]{{Binding: tenant, Scopes: []string{"app"}}, {Binding: campaign, Scopes: []string{"promptkit"}}},
|
||||||
|
Scopes: []Scope[input, struct{}]{
|
||||||
|
{Name: "app", Operations: []Operation{Validate}, Acquire: func(_ context.Context, _ Request, value input, _ struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
calls++
|
||||||
|
if !value.Tenant.Set || value.Tenant.Value != "team" || value.Campaign.Set {
|
||||||
|
t.Errorf("app input = %#v", value)
|
||||||
|
}
|
||||||
|
return commandkit.Acquired[View]{}, nil
|
||||||
|
}},
|
||||||
|
{Name: "promptkit", Operations: []Operation{Sources}, Acquire: func(_ context.Context, _ Request, value input, _ struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
calls++
|
||||||
|
if !value.Campaign.Set || value.Campaign.Value != "spring" || value.Tenant.Set {
|
||||||
|
t.Errorf("promptkit input = %#v", value)
|
||||||
|
}
|
||||||
|
return commandkit.Acquired[View]{Value: View{Sources: &sourceInventory{}}}, nil
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app := newConfigTestApp(t, Command(suite))
|
||||||
|
// Captured capabilities, applicability, and handles survive source edits.
|
||||||
|
suite.Scopes[0].Operations[0] = Sources
|
||||||
|
suite.Selectors[0].Scopes[0] = "promptkit"
|
||||||
|
suite.Selectors[1].Binding = commandkit.Flag[input]{}
|
||||||
|
for _, args := range [][]string{
|
||||||
|
{"config", "validate", "--tenant", "team"},
|
||||||
|
{"config", "sources", "--scope", "promptkit", "--campaign", "spring"},
|
||||||
|
} {
|
||||||
|
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||||
|
t.Fatalf("%v: %#v", args, outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, args := range [][]string{
|
||||||
|
{"config", "validate", "--campaign", "spring"},
|
||||||
|
{"config", "sources", "--scope", "promptkit", "--tenant", "team"},
|
||||||
|
} {
|
||||||
|
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeUsage {
|
||||||
|
t.Fatalf("selector for another operation accepted: %v: %#v", args, outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if calls != 2 {
|
||||||
|
t.Fatalf("acquisition calls = %d, want 2", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidationPreparesIndependentScopeInputsAndOneSharedState(t *testing.T) {
|
func TestValidationPreparesIndependentScopeInputsAndOneSharedState(t *testing.T) {
|
||||||
type inspectInput struct {
|
type inspectInput struct {
|
||||||
Campaign commandkit.Supplied[string]
|
Campaign commandkit.Supplied[string]
|
||||||
|
|||||||
@@ -56,12 +56,7 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
|||||||
flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer path to explain.", Required: true}, (*string)(nil), parseConfigString,
|
flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer path to explain.", Required: true}, (*string)(nil), parseConfigString,
|
||||||
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value }))
|
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value }))
|
||||||
}
|
}
|
||||||
selectorIndexes := selectorIndexesForOperation(prepared, operation)
|
flags = append(flags, commandkit.CaptureFlags(prepared.bindingsByOperation[operation], func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] {
|
||||||
selectorBindings := make([]commandkit.Flag[I], 0, len(selectorIndexes))
|
|
||||||
for _, index := range selectorIndexes {
|
|
||||||
selectorBindings = append(selectorBindings, prepared.selectors[index].Binding)
|
|
||||||
}
|
|
||||||
flags = append(flags, commandkit.CaptureFlags(selectorBindings, func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] {
|
|
||||||
if input == nil || *input == nil {
|
if input == nil || *input == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -85,19 +80,6 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectorIndexesForOperation[I, S any](prepared *preparedSuite[I, S], operation Operation) []int {
|
|
||||||
used := make([]int, 0, len(prepared.selectors))
|
|
||||||
for index := range prepared.selectors {
|
|
||||||
for _, scope := range prepared.scopes {
|
|
||||||
if containsOperation(scope.scope.Operations, operation) && selectorApplies(prepared.selectors[index].Scopes, scope.scope.Name) {
|
|
||||||
used = append(used, index)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return used
|
|
||||||
}
|
|
||||||
|
|
||||||
func operationDeclaration(operation Operation) (string, string) {
|
func operationDeclaration(operation Operation) (string, string) {
|
||||||
switch operation {
|
switch operation {
|
||||||
case Validate:
|
case Validate:
|
||||||
@@ -133,19 +115,13 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
|||||||
return fmt.Errorf("scope all is supported only for validate and show")
|
return fmt.Errorf("scope all is supported only for validate and show")
|
||||||
}
|
}
|
||||||
|
|
||||||
selected := make([]*preparedScope[I, S], 0, len(prepared.scopes))
|
var selected []*preparedScope[I, S]
|
||||||
if scopeName == "all" {
|
if scopeName == "all" {
|
||||||
for _, scope := range orderedScopeRecords(prepared.scopes) {
|
selected = prepared.scopes
|
||||||
selected = append(selected, prepared.byName[scope.scope.Name])
|
|
||||||
}
|
|
||||||
} else if scope, exists := prepared.byName[scopeName]; exists {
|
} else if scope, exists := prepared.byName[scopeName]; exists {
|
||||||
selected = append(selected, scope)
|
selected = []*preparedScope[I, S]{scope}
|
||||||
} else {
|
} else {
|
||||||
choices := make([]string, 0, len(prepared.scopes))
|
return fmt.Errorf("unknown configuration scope %q (available: %s)", scopeName, prepared.scopeChoices)
|
||||||
for _, scope := range orderedScopeRecords(prepared.scopes) {
|
|
||||||
choices = append(choices, scope.scope.Name)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unknown configuration scope %q (available: %s)", scopeName, joinNames(choices))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, scope := range selected {
|
for _, scope := range selected {
|
||||||
@@ -175,23 +151,14 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
|||||||
path: path,
|
path: path,
|
||||||
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
||||||
}
|
}
|
||||||
operationSelectors := selectorIndexesForOperation(prepared, operation)
|
|
||||||
capturedSelectors := make(map[int]struct{}, len(operationSelectors))
|
|
||||||
for _, index := range operationSelectors {
|
|
||||||
capturedSelectors[index] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, scope := range selected {
|
for _, scope := range selected {
|
||||||
var value I
|
var value I
|
||||||
if prepared.newInput != nil {
|
if prepared.newInput != nil {
|
||||||
value = prepared.newInput()
|
value = prepared.newInput()
|
||||||
}
|
}
|
||||||
bindings := make([]commandkit.Flag[I], 0, len(scope.selectorIndexes))
|
// Availability was checked above, so every binding for this scope
|
||||||
for _, index := range scope.selectorIndexes {
|
// belongs to the operation's captured union.
|
||||||
if _, ok := capturedSelectors[index]; ok {
|
if err := input.values.ApplyTo(&value, scope.bindings...); err != nil {
|
||||||
bindings = append(bindings, prepared.selectors[index].Binding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := input.values.ApplyTo(&value, bindings...); err != nil {
|
|
||||||
return fmt.Errorf("configuration scope %q: %w", scope.scope.Name, err)
|
return fmt.Errorf("configuration scope %q: %w", scope.scope.Name, err)
|
||||||
}
|
}
|
||||||
if prepared.checkInput != nil {
|
if prepared.checkInput != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user