diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index b17c3b3..c257946 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -461,6 +461,13 @@ Focused check: `go test ./internal/engine -run 'History|Explain|Inheritance|Over Finding: **A05**; requires Stage 2. Owners: `internal/engine/value_operation.go`, projection/clone tests. +Status: **complete** (A05). Prior completed commits: `2736ddf`, `deb9aa5`, +`ae3408a`, `ea27f17`, `08a8c25`, `877c497`, `8aba43d`, `7bf2f0c`, +`0f9ecf1`, `119b42c`, `247a12b`, `4fcff5f`, `12d350a`. Validation passed: +`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Dynamic|Projection|Clone|Resolution' -count=1`, +`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`, +and `git diff --check`. + Add an operation-local set of successfully approved concrete types/descriptors. After a completed descriptor build and a full successful secret-eligibility walk, record approval. Subsequent values of that type in the same `valueOperation` reuse diff --git a/internal/engine/dynamic_descriptor_transaction_test.go b/internal/engine/dynamic_descriptor_transaction_test.go index 2c68377..353eede 100644 --- a/internal/engine/dynamic_descriptor_transaction_test.go +++ b/internal/engine/dynamic_descriptor_transaction_test.go @@ -4,6 +4,8 @@ import ( "errors" "reflect" "testing" + + "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type unsupportedDynamicRoot struct { @@ -63,6 +65,9 @@ func TestDynamicDescriptorFailuresRollBackProvisionalTypes(t *testing.T) { if _, issue := operation.dynamicDescriptor(reflect.TypeFor[unsupportedDynamicRoot](), Keys("failure")); issue == nil || issue.code != CodeSchemaDynamicType { t.Fatalf("unsupported dynamic descriptor issue = %#v", issue) } + if _, approved := operation.approvedDynamic[reflect.TypeFor[unsupportedDynamicRoot]()]; approved { + t.Fatal("unsupported dynamic descriptor was cached as eligible") + } if schema.types[reflect.TypeFor[map[string]string]()] != prior { t.Fatal("failed build removed a previously completed descriptor") } @@ -87,9 +92,69 @@ func TestDynamicDescriptorFailuresRollBackProvisionalTypes(t *testing.T) { if _, issue := secretOperation.dynamicDescriptor(reflect.TypeFor[secretDynamicRoot](), Keys("secret")); issue == nil || issue.code != CodeSchemaDynamicType { t.Fatalf("dynamic secret eligibility issue = %#v", issue) } + if len(secretOperation.approvedDynamic) != 0 { + t.Fatalf("secret-bearing dynamic descriptor was cached as eligible: %#v", secretOperation.approvedDynamic) + } for _, current := range []reflect.Type{reflect.TypeFor[secretDynamicRoot](), reflect.TypeFor[secretDynamicNested](), reflect.TypeFor[Secret]()} { if _, exists := secretSchema.types[current]; exists { t.Errorf("secret eligibility failure retained provisional descriptor for %v", current) } } } + +type wideDynamicEligibilityValue struct { + Alpha string + Bravo string + Charlie string + Delta string + Echo string + Foxtrot string + Golf string + Hotel string + India string + Juliet string + Kilo string + Lima string + Mike string + November string + Oscar string + Papa string +} + +type repeatedNilDynamicEligibilityConfig struct { + Values []any +} + +func TestDynamicEligibilityIsReusedWithinProjectionOperation(t *testing.T) { + schema, schemaIssue := buildSchema(reflect.TypeFor[repeatedNilDynamicEligibilityConfig](), testSecretBinding()) + if schemaIssue != nil { + t.Fatalf("schema issue = %#v", schemaIssue) + } + values := make([]any, 32) + for index := range values { + values[index] = (*wideDynamicEligibilityValue)(nil) + } + configuration := repeatedNilDynamicEligibilityConfig{Values: values} + operation := valueOperation{schema: schema, limits: defaultProjectionLimits(), active: make(map[valueVisit]bool)} + node, issue := operation.project(reflect.ValueOf(configuration), schema.root, Path{}, 0) + if issue != nil { + t.Fatalf("repeated typed-nil projection issue = %#v", issue) + } + if len(node.Mapping) != 1 || len(node.Mapping[0].Value.Sequence) != len(values) { + t.Fatalf("projected repeated values = %#v", node) + } + for index, child := range node.Mapping[0].Value.Sequence { + if child.Kind != document.Null { + t.Errorf("projected value %d kind = %v, want null", index, child.Kind) + } + } + current := reflect.TypeFor[*wideDynamicEligibilityValue]() + if len(operation.approvedDynamic) != 1 || operation.approvedDynamic[current] == nil || + operation.approvedDynamic[current] != schema.types[current] { + t.Fatalf("operation eligibility cache = %#v", operation.approvedDynamic) + } + otherOperation := valueOperation{schema: schema, limits: defaultProjectionLimits(), active: make(map[valueVisit]bool)} + if len(otherOperation.approvedDynamic) != 0 { + t.Fatalf("dynamic eligibility leaked into another operation: %#v", otherOperation.approvedDynamic) + } +} diff --git a/internal/engine/value_operation.go b/internal/engine/value_operation.go index db6bf79..2331b20 100644 --- a/internal/engine/value_operation.go +++ b/internal/engine/value_operation.go @@ -24,21 +24,25 @@ type valueVisit struct { length int } -// valueOperation owns one traversal's cycle and budget state and reuses the -// current load's descriptor cache for dynamic values. +// valueOperation owns one traversal's cycle and budget state, and caches +// dynamic descriptors approved for that traversal. type valueOperation struct { - cloneMode secretCloneMode - schema *schemaDescriptor - limits projectionLimits - active map[valueVisit]bool - nodes int - nodeCredits int + cloneMode secretCloneMode + schema *schemaDescriptor + limits projectionLimits + active map[valueVisit]bool + approvedDynamic map[reflect.Type]*typeDescriptor + nodes int + nodeCredits int } func (o *valueOperation) dynamicDescriptor(current reflect.Type, path Path) (*typeDescriptor, *projectionIssue) { if o.schema.secrets.Type != nil && current == o.schema.secrets.Type { return nil, &projectionIssue{code: CodeSchemaDynamicType, path: path.clone(), message: "dynamic configuration values cannot establish secret-reference semantics"} } + if descriptor, approved := o.approvedDynamic[current]; approved { + return descriptor, nil + } builder := schemaBuilder{cache: o.schema.types, building: make(map[reflect.Type]bool), secrets: o.schema.secrets} descriptor, issue := builder.describe(current, path) if issue != nil { @@ -49,6 +53,10 @@ func (o *valueOperation) dynamicDescriptor(current reflect.Type, path Path) (*ty builder.rollback() return nil, &projectionIssue{code: CodeSchemaDynamicType, path: path.clone(), message: "dynamic configuration values cannot establish secret-reference semantics"} } + if o.approvedDynamic == nil { + o.approvedDynamic = make(map[reflect.Type]*typeDescriptor) + } + o.approvedDynamic[current] = descriptor return descriptor, nil }