diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index f1f8ba3..9a8b33c 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -243,6 +243,14 @@ Validation: inspect the registration selectors and schema; when runnable, load one headerless root list and one version-2 collection. Resolution/validation callers are completed in the following stages. +Completion: the typed document now uses identified resource slices, registers +all six root and collection identity rules, and checks specification version 2. +Added a focused root/collection list fixture and reviewed the selectors and +schema. The focused test cannot yet compile because field capture, resolution, +validation, indexing, prompt materialization, and older fixtures still expect +maps; those migrations are assigned to the following stages. Formatting and +`git diff --check` passed; execution of the new fixture remains deferred. + ## Stage 4: Preserve Field Presence And Resolution At List Paths Scope: backend/profile field capture, normalization, and defaulting in diff --git a/internal/configuration/load.go b/internal/configuration/load.go index 6853fd8..aa9727d 100644 --- a/internal/configuration/load.go +++ b/internal/configuration/load.go @@ -18,20 +18,21 @@ import ( type document struct { Version *string `yaml:"version"` - Backends map[string]backendConfig `yaml:"backends"` - Profiles map[string]profileConfig `yaml:"profiles"` - Prompts map[string]promptConfig `yaml:"prompts"` + Backends []backendConfig `yaml:"backends"` + Profiles []profileConfig `yaml:"profiles"` + Prompts []promptConfig `yaml:"prompts"` Collections map[string]collectionConfig `yaml:"collections"` } type collectionConfig struct { - ReleaseVersion *string `yaml:"release_version"` - Backends map[string]backendConfig `yaml:"backends"` - Profiles map[string]profileConfig `yaml:"profiles"` - Prompts map[string]promptConfig `yaml:"prompts"` + ReleaseVersion *string `yaml:"release_version"` + Backends []backendConfig `yaml:"backends"` + Profiles []profileConfig `yaml:"profiles"` + Prompts []promptConfig `yaml:"prompts"` } type backendConfig struct { + ID string `yaml:"id"` Endpoint *string `yaml:"endpoint"` APIKeyEnv *string `yaml:"api_key_env"` ConcurrencyLimit *int `yaml:"concurrency_limit"` @@ -56,6 +57,7 @@ type authenticationProbeConfig struct { } type profileConfig struct { + ID string `yaml:"id"` Backend *string `yaml:"backend"` Endpoint *string `yaml:"endpoint"` Model *string `yaml:"model"` @@ -71,7 +73,7 @@ type profileConfig struct { } type promptConfig struct { - Version *string `yaml:"version"` + ID string `yaml:"id"` DefaultProfile *string `yaml:"default_profile"` Description *string `yaml:"description"` SessionID *string `yaml:"session_id"` @@ -141,9 +143,9 @@ type resourceSet struct { isCollection bool namespace string bases resourceBases - backends map[string]backendConfig - profiles map[string]profileConfig - prompts map[string]promptConfig + backends []backendConfig + profiles []profileConfig + prompts []promptConfig } func rootResourceBases() resourceBases { @@ -230,6 +232,14 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) Underlays: underlays, SourceVersionMode: configkit.SourceVersionModeIfPresent, Definition: configkit.Definition[document]{ + SequenceIdentities: []configkit.SequenceIdentity{ + configkit.SequenceIdentityAt(configkit.Keys("backends"), "id"), + configkit.SequenceIdentityAt(configkit.Keys("profiles"), "id"), + configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"), + configkit.SequenceIdentityMatching(configkit.SequenceSelector{configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey(), configkit.SequenceLiteralKey("backends")}, "id"), + configkit.SequenceIdentityMatching(configkit.SequenceSelector{configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey(), configkit.SequenceLiteralKey("profiles")}, "id"), + configkit.SequenceIdentityMatching(configkit.SequenceSelector{configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey(), configkit.SequenceLiteralKey("prompts")}, "id"), + }, Version: &configkit.VersionRule{ Path: configkit.Keys("version"), Check: checkConfigurationVersion, @@ -302,7 +312,7 @@ func checkConfigurationVersion(version configkit.Version, validation *configkit. if !version.Present { return nil } - if version.Null || version.Text != "1" { + if version.Null || version.Text != "2" { validation.Error(configkit.Keys("version"), "unsupported configuration specification version") } return nil diff --git a/internal/configuration/load_test.go b/internal/configuration/load_test.go index 6a6ef3f..382a58c 100644 --- a/internal/configuration/load_test.go +++ b/internal/configuration/load_test.go @@ -12,6 +12,27 @@ func mapping(label string, values map[string]any) configkit.Input { return configkit.Input{Source: configkit.Mapping(label, values)} } +func TestLoadIdentifiedResourceLists(t *testing.T) { + for _, header := range []bool{false, true} { + values := map[string]any{ + "backends": []any{map[string]any{"id": "local", "endpoint": "https://example.invalid/v1"}}, + "collections": map[string]any{"team": map[string]any{ + "backends": []any{map[string]any{"id": "local", "endpoint": "https://example.invalid/v1"}}, + }}, + } + if header { + values["version"] = "2" + } + loaded, err := load(mapping("identified resources", values), nil) + if err != nil { + t.Fatalf("load identified resources with header %t: %v", header, err) + } + if loaded.result.Config.Backends[0].ID != "local" || loaded.result.Config.Collections["team"].Backends[0].ID != "local" { + t.Fatal("identified root and collection resources were not retained") + } + } +} + func TestLoadChecksOptionalConfigurationVersion(t *testing.T) { for _, test := range []struct { name string