diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 9a8b33c..ad1b7e8 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -271,6 +271,13 @@ Scope: backend/profile field capture, normalization, and defaulting in Validation: owning tests demonstrate explicit zeros/nulls, backend completion, and recorded normalization at the correct list entries. No new readiness rule. +Completion: backend and profile presence capture and resolution now use list +indexes and mutate the effective slice elements while retaining full-ID state +keys. Converted focused normalization, null/default, profile-presence, and +same-ID replacement fixtures. Their execution remains deferred: the package +still does not compile until the subsequent indexing, validation, and prompt +list migrations. Formatting and `git diff --check` passed. + ## Stage 5: Validate Final Records And Build Execution Indexes Scope: `validateEffective`, `indexedBackends`, `indexedProfiles`, and diff --git a/internal/configuration/load.go b/internal/configuration/load.go index aa9727d..9faba13 100644 --- a/internal/configuration/load.go +++ b/internal/configuration/load.go @@ -247,8 +247,8 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) Sensitive: secretProviderPath, Resolve: func(config *document, resolution *configkit.Resolution) error { for _, resources := range resourceSets(config) { - captureFields(resources.backends, resources.bases.backends, resources.namespace, backendFields, resolution, state.backends) - captureFields(resources.profiles, resources.bases.profiles, resources.namespace, profileFields, resolution, state.profiles) + captureFields(resources.backends, func(v backendConfig) string { return v.ID }, resources.bases.backends, resources.namespace, backendFields, resolution, state.backends) + captureFields(resources.profiles, func(v profileConfig) string { return v.ID }, resources.bases.profiles, resources.namespace, profileFields, resolution, state.profiles) capturePromptMetadata(resources.prompts, resources.bases.prompts, resources.namespace, resolution, state.promptMetadata) resolveBackends(resources.backends, resources.bases.backends, resolution) resolveProfiles(resources.profiles, resources.bases.profiles, resolution) @@ -351,11 +351,11 @@ func captureFieldState(path configkit.Path, resolution *configkit.Resolution) fi } } -func captureFields[T any](entries map[string]T, basePath configkit.Path, namespace string, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) { - for id := range entries { +func captureFields[T any](entries []T, identity func(T) string, basePath configkit.Path, namespace string, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) { + for index, entry := range entries { states := make(map[string]fieldState, len(fields)) for _, field := range fields { - info, exists := resolution.EntryInfo(basePath.Key(id).Key(field)) + info, exists := resolution.EntryInfo(basePath.Index(index).Key(field)) switch { case !exists || !info.Explicit: states[field] = fieldAbsent @@ -365,7 +365,7 @@ func captureFields[T any](entries map[string]T, basePath configkit.Path, namespa states[field] = fieldValue } } - destination[indexedResourceID(namespace, id)] = states + destination[indexedResourceID(namespace, identity(entry))] = states } } @@ -386,10 +386,10 @@ func normalizeString(value **string, path configkit.Path, resolution *configkit. return false } -func resolveBackends(backends map[string]backendConfig, basePath configkit.Path, resolution *configkit.Resolution) { - for _, id := range sortedKeys(backends) { - backend := backends[id] - base := basePath.Key(id) +func resolveBackends(backends []backendConfig, basePath configkit.Path, resolution *configkit.Resolution) { + for index := range backends { + backend := &backends[index] + base := basePath.Index(index) if backend.Endpoint != nil { endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*backend.Endpoint) if err != nil { @@ -424,7 +424,6 @@ func resolveBackends(backends map[string]backendConfig, basePath configkit.Path, configkit.Default(resolution, base.Key("queue_capacity"), &backend.QueueCapacity, &capacity, "select default queue capacity") } } - backends[id] = backend } } @@ -443,10 +442,10 @@ func normalizeDiagnosticEndpoint(value **string, path configkit.Path, resolution normalizeString(value, path, resolution, normalized) } -func resolveProfiles(profiles map[string]profileConfig, basePath configkit.Path, resolution *configkit.Resolution) { - for _, id := range sortedKeys(profiles) { - profile := profiles[id] - base := basePath.Key(id) +func resolveProfiles(profiles []profileConfig, basePath configkit.Path, resolution *configkit.Resolution) { + for index := range profiles { + profile := &profiles[index] + base := basePath.Index(index) if profile.Endpoint != nil && *profile.Endpoint != "" { endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint) if err != nil { @@ -467,7 +466,6 @@ func resolveProfiles(profiles map[string]profileConfig, basePath configkit.Path, normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value)) } } - profiles[id] = profile } } diff --git a/internal/configuration/load_test.go b/internal/configuration/load_test.go index 382a58c..e7e3edb 100644 --- a/internal/configuration/load_test.go +++ b/internal/configuration/load_test.go @@ -230,14 +230,14 @@ func TestLoadRejectsInvalidCollectionAndResourceKeys(t *testing.T) { } func TestLoadRecordsBackendNormalizationAndNullDefault(t *testing.T) { - loaded, err := load(mapping("backend", map[string]any{"backends": map[string]any{ - "local": map[string]any{"endpoint": " HTTP://localhost:8080/v1 ", "api_key_env": " TOKEN_NAME ", + loaded, err := load(mapping("backend", map[string]any{"backends": []any{ + map[string]any{"id": "local", "endpoint": " HTTP://localhost:8080/v1 ", "api_key_env": " TOKEN_NAME ", "concurrency_limit": 2, "queue_capacity": nil}, }}), nil) if err != nil { t.Fatal(err) } - backend := loaded.result.Config.Backends["local"] + backend := loaded.result.Config.Backends[0] if backend.Endpoint == nil || *backend.Endpoint != "http://localhost:8080/v1" || backend.APIKeyEnv == nil || *backend.APIKeyEnv != "TOKEN_NAME" || backend.QueueCapacity == nil || *backend.QueueCapacity != 1024 { @@ -246,6 +246,10 @@ func TestLoadRecordsBackendNormalizationAndNullDefault(t *testing.T) { if loaded.backends["local"]["queue_capacity"] != fieldNull { t.Fatalf("explicit null was not retained: %+v", loaded.backends["local"]) } + field, found := loaded.result.Metadata().Lookup(configkit.Keys("backends").Index(0).Key("endpoint")) + if !found || field.Origin.Operation != configkit.ChangeNormalization { + t.Fatalf("endpoint normalization was not recorded at the list entry: %+v", field) + } } func TestLoadBackendSmokeTestsOmissionAndNull(t *testing.T) { @@ -402,15 +406,35 @@ func TestLoadRejectsInvalidFinalBackendSmokeTests(t *testing.T) { } func TestLoadDefaultsAbsentQueueCapacity(t *testing.T) { - loaded, err := load(mapping("backend", map[string]any{"backends": map[string]any{ - "local": map[string]any{"endpoint": "http://localhost:8080/v1", "concurrency_limit": 1}, + underlay := mapping("defaults", map[string]any{"backends": []any{ + map[string]any{"id": "local", "endpoint": "http://localhost:8080/v1", "api_key_env": "OLD_KEY"}, + }}) + loaded, err := load(mapping("backend", map[string]any{"backends": []any{ + map[string]any{"id": "local", "endpoint": "http://localhost:8080/v1", "concurrency_limit": 1}, + }}), []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}) + if err != nil { + t.Fatal(err) + } + backend := loaded.result.Config.Backends[0] + if backend.APIKeyEnv != nil || backend.QueueCapacity == nil || *backend.QueueCapacity != 1024 || loaded.backends["local"]["queue_capacity"] != fieldAbsent { + t.Fatalf("absent queue capacity did not receive its default: %+v", backend) + } +} + +func TestLoadCapturesProfilePresence(t *testing.T) { + loaded, err := load(mapping("profile", map[string]any{"profiles": []any{ + map[string]any{"id": "local", "temperature": 0, "top_p": nil, "api_key_required": false}, }}), nil) if err != nil { t.Fatal(err) } - backend := loaded.result.Config.Backends["local"] - if backend.QueueCapacity == nil || *backend.QueueCapacity != 1024 || loaded.backends["local"]["queue_capacity"] != fieldAbsent { - t.Fatalf("absent queue capacity did not receive its default: %+v", backend) + profile := loaded.result.Config.Profiles[0] + if profile.Temperature == nil || *profile.Temperature != 0 || profile.TopP != nil || profile.APIKeyRequired == nil || *profile.APIKeyRequired { + t.Fatalf("profile values were not preserved: %+v", profile) + } + fields := loaded.profiles["local"] + if fields["temperature"] != fieldValue || fields["top_p"] != fieldNull || fields["api_key_required"] != fieldValue || fields["model"] != fieldAbsent { + t.Fatalf("profile presence was not preserved: %+v", fields) } }