Pass effective paths through configuration loading
This commit is contained in:
@@ -359,6 +359,12 @@ Validation: configuration load, target, prompt-origin, inheritance, and inspecti
|
|||||||
tests; `GOWORK=off go test ./internal/configuration .`. A behavior-preserving
|
tests; `GOWORK=off go test ./internal/configuration .`. A behavior-preserving
|
||||||
refactor should not rewrite unrelated expectations.
|
refactor should not rewrite unrelated expectations.
|
||||||
|
|
||||||
|
Completion: configuration capture, normalization, effective validation, and
|
||||||
|
prompt materialization now receive explicit ConfigKit base paths. Existing
|
||||||
|
resource IDs remain independent from those paths; source-origin reads and the
|
||||||
|
protected provider-key policy are preserved. Validation passed:
|
||||||
|
`GOWORK=off go test ./internal/configuration .`.
|
||||||
|
|
||||||
## Stage 4: Install One Prompt Definition Per ID
|
## Stage 4: Install One Prompt Definition Per ID
|
||||||
|
|
||||||
Scope: prompt storage format, materialization, and fixture-format migration.
|
Scope: prompt storage format, materialization, and fixture-format migration.
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func secretProviderPath(path configkit.Path) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
parts := strings.Split(strings.TrimPrefix(pointer, "/"), "/")
|
parts := strings.Split(strings.TrimPrefix(pointer, "/"), "/")
|
||||||
if len(parts) < 4 || (parts[0] != "backends" && parts[0] != "profiles") || parts[2] != "extra_params" {
|
if !providerExtraParamsPath(parts) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
key := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(parts[len(parts)-1], "~1", "/"), "~0", "~"))
|
key := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(parts[len(parts)-1], "~1", "/"), "~0", "~"))
|
||||||
@@ -64,3 +64,8 @@ func secretProviderPath(path configkit.Path) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func providerExtraParamsPath(parts []string) bool {
|
||||||
|
return len(parts) >= 4 && parts[1] != "" &&
|
||||||
|
(parts[0] == "backends" || parts[0] == "profiles") && parts[2] == "extra_params"
|
||||||
|
}
|
||||||
|
|||||||
29
internal/configuration/inspection_test.go
Normal file
29
internal/configuration/inspection_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecretProviderPathRecognizesResourceExtraParams(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path configkit.Path
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "backend nested secret", path: configkit.Keys("backends", "provider", "extra_params", "nested", "Token"), want: true},
|
||||||
|
{name: "profile nested secret", path: configkit.Keys("profiles", "assistant", "extra_params", "authorization"), want: true},
|
||||||
|
{name: "unrelated nested mapping", path: configkit.Keys("application", "backends", "provider", "extra_params", "token"), want: false},
|
||||||
|
{name: "extra params outside resource map", path: configkit.Keys("backends", "provider", "options", "extra_params", "token"), want: false},
|
||||||
|
{name: "missing resource key", path: configkit.Keys("backends", "", "extra_params", "token"), want: false},
|
||||||
|
{name: "ordinary provider value", path: configkit.Keys("profiles", "assistant", "extra_params", "provider", "mode"), want: false},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := secretProviderPath(test.path); got != test.want {
|
||||||
|
t.Fatalf("secretProviderPath(%q) = %t, want %t", test.path, got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,6 +119,20 @@ type loaded struct {
|
|||||||
promptIDs []domain.PromptIdentity
|
promptIDs []domain.PromptIdentity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type resourceBases struct {
|
||||||
|
backends configkit.Path
|
||||||
|
profiles configkit.Path
|
||||||
|
prompts configkit.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func rootResourceBases() resourceBases {
|
||||||
|
return resourceBases{
|
||||||
|
backends: configkit.Keys("backends"),
|
||||||
|
profiles: configkit.Keys("profiles"),
|
||||||
|
prompts: configkit.Keys("prompts"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||||
|
|
||||||
var backendFields = []string{"endpoint", "api_key_env", "concurrency_limit", "queue_capacity", "extra_params", "smoke_tests"}
|
var backendFields = []string{"endpoint", "api_key_env", "concurrency_limit", "queue_capacity", "extra_params", "smoke_tests"}
|
||||||
@@ -131,6 +145,7 @@ func Load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
|||||||
|
|
||||||
// load performs one ConfigKit lifecycle over the selected source tiers.
|
// load performs one ConfigKit lifecycle over the selected source tiers.
|
||||||
func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) {
|
func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) {
|
||||||
|
bases := rootResourceBases()
|
||||||
state := &loaded{
|
state := &loaded{
|
||||||
backends: make(map[string]map[string]fieldState),
|
backends: make(map[string]map[string]fieldState),
|
||||||
profiles: make(map[string]map[string]fieldState),
|
profiles: make(map[string]map[string]fieldState),
|
||||||
@@ -141,22 +156,24 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
|||||||
Definition: configkit.Definition[document]{
|
Definition: configkit.Definition[document]{
|
||||||
Secret: secretProviderPath,
|
Secret: secretProviderPath,
|
||||||
Resolve: func(config *document, resolution *configkit.Resolution) error {
|
Resolve: func(config *document, resolution *configkit.Resolution) error {
|
||||||
captureFields(config.Backends, "backends", backendFields, resolution, state.backends)
|
captureFields(config.Backends, bases.backends, backendFields, resolution, state.backends)
|
||||||
captureFields(config.Profiles, "profiles", profileFields, resolution, state.profiles)
|
captureFields(config.Profiles, bases.profiles, profileFields, resolution, state.profiles)
|
||||||
capturePromptMetadata(config.Prompts, resolution, state.promptMetadata)
|
capturePromptMetadata(config.Prompts, bases.prompts, resolution, state.promptMetadata)
|
||||||
resolveBackends(config.Backends, resolution)
|
resolveBackends(config.Backends, bases.backends, resolution)
|
||||||
resolveProfiles(config.Profiles, resolution)
|
resolveProfiles(config.Profiles, bases.profiles, resolution)
|
||||||
resolvePrompts(config.Prompts, resolution)
|
resolvePrompts(config.Prompts, bases.prompts, resolution)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Validate: validateEffective,
|
Validate: func(config *document, validation *configkit.Validation) {
|
||||||
|
validateEffective(config, bases, validation)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("load configuration: %w", err)
|
return nil, fmt.Errorf("load configuration: %w", err)
|
||||||
}
|
}
|
||||||
state.result = result
|
state.result = result
|
||||||
if err := state.materializePrompts(); err != nil {
|
if err := state.materializePrompts(result.Config.Prompts, bases.prompts); err != nil {
|
||||||
return nil, fmt.Errorf("load configuration: %w", err)
|
return nil, fmt.Errorf("load configuration: %w", err)
|
||||||
}
|
}
|
||||||
if err := state.buildExecutionProfiles(); err != nil {
|
if err := state.buildExecutionProfiles(); err != nil {
|
||||||
@@ -176,7 +193,7 @@ type promptInputPresence struct {
|
|||||||
description fieldState
|
description fieldState
|
||||||
}
|
}
|
||||||
|
|
||||||
func capturePromptMetadata(prompts map[string]map[string]promptConfig, resolution *configkit.Resolution, destination map[string]map[string][]promptInputPresence) {
|
func capturePromptMetadata(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string]map[string][]promptInputPresence) {
|
||||||
for _, id := range sortedKeys(prompts) {
|
for _, id := range sortedKeys(prompts) {
|
||||||
if destination[id] == nil {
|
if destination[id] == nil {
|
||||||
destination[id] = make(map[string][]promptInputPresence, len(prompts[id]))
|
destination[id] = make(map[string][]promptInputPresence, len(prompts[id]))
|
||||||
@@ -184,7 +201,7 @@ func capturePromptMetadata(prompts map[string]map[string]promptConfig, resolutio
|
|||||||
for _, version := range sortedKeys(prompts[id]) {
|
for _, version := range sortedKeys(prompts[id]) {
|
||||||
inputs := prompts[id][version].Inputs
|
inputs := prompts[id][version].Inputs
|
||||||
states := make([]promptInputPresence, len(inputs))
|
states := make([]promptInputPresence, len(inputs))
|
||||||
base := configkit.Keys("prompts", id, version, "inputs")
|
base := basePath.Key(id).Key(version).Key("inputs")
|
||||||
for index := range inputs {
|
for index := range inputs {
|
||||||
path := base.Index(index)
|
path := base.Index(index)
|
||||||
states[index] = promptInputPresence{
|
states[index] = promptInputPresence{
|
||||||
@@ -209,11 +226,11 @@ func captureFieldState(path configkit.Path, resolution *configkit.Resolution) fi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func captureFields[T any](entries map[string]T, collection string, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) {
|
func captureFields[T any](entries map[string]T, basePath configkit.Path, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) {
|
||||||
for id := range entries {
|
for id := range entries {
|
||||||
states := make(map[string]fieldState, len(fields))
|
states := make(map[string]fieldState, len(fields))
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
info, exists := resolution.InputInfo(configkit.Keys(collection, id, field))
|
info, exists := resolution.InputInfo(basePath.Key(id).Key(field))
|
||||||
switch {
|
switch {
|
||||||
case !exists || !info.Explicit:
|
case !exists || !info.Explicit:
|
||||||
states[field] = fieldAbsent
|
states[field] = fieldAbsent
|
||||||
@@ -244,10 +261,10 @@ func normalizeString(value **string, path configkit.Path, resolution *configkit.
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveBackends(backends map[string]backendConfig, resolution *configkit.Resolution) {
|
func resolveBackends(backends map[string]backendConfig, basePath configkit.Path, resolution *configkit.Resolution) {
|
||||||
for _, id := range sortedKeys(backends) {
|
for _, id := range sortedKeys(backends) {
|
||||||
backend := backends[id]
|
backend := backends[id]
|
||||||
base := configkit.Keys("backends", id)
|
base := basePath.Key(id)
|
||||||
if backend.Endpoint != nil {
|
if backend.Endpoint != nil {
|
||||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*backend.Endpoint)
|
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*backend.Endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -301,10 +318,10 @@ func normalizeDiagnosticEndpoint(value **string, path configkit.Path, resolution
|
|||||||
normalizeString(value, path, resolution, normalized)
|
normalizeString(value, path, resolution, normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveProfiles(profiles map[string]profileConfig, resolution *configkit.Resolution) {
|
func resolveProfiles(profiles map[string]profileConfig, basePath configkit.Path, resolution *configkit.Resolution) {
|
||||||
for _, id := range sortedKeys(profiles) {
|
for _, id := range sortedKeys(profiles) {
|
||||||
profile := profiles[id]
|
profile := profiles[id]
|
||||||
base := configkit.Keys("profiles", id)
|
base := basePath.Key(id)
|
||||||
if profile.Endpoint != nil && *profile.Endpoint != "" {
|
if profile.Endpoint != nil && *profile.Endpoint != "" {
|
||||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint)
|
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -329,10 +346,10 @@ func resolveProfiles(profiles map[string]profileConfig, resolution *configkit.Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateEffective(config *document, validation *configkit.Validation) {
|
func validateEffective(config *document, bases resourceBases, validation *configkit.Validation) {
|
||||||
for _, id := range sortedKeys(config.Backends) {
|
for _, id := range sortedKeys(config.Backends) {
|
||||||
backend := config.Backends[id]
|
backend := config.Backends[id]
|
||||||
base := configkit.Keys("backends", id)
|
base := bases.backends.Key(id)
|
||||||
if strings.TrimSpace(id) == "" {
|
if strings.TrimSpace(id) == "" {
|
||||||
validation.Error(base, "backend ID must not be blank")
|
validation.Error(base, "backend ID must not be blank")
|
||||||
}
|
}
|
||||||
@@ -373,7 +390,7 @@ func validateEffective(config *document, validation *configkit.Validation) {
|
|||||||
}
|
}
|
||||||
for _, id := range sortedKeys(config.Profiles) {
|
for _, id := range sortedKeys(config.Profiles) {
|
||||||
profile := config.Profiles[id]
|
profile := config.Profiles[id]
|
||||||
base := configkit.Keys("profiles", id)
|
base := bases.profiles.Key(id)
|
||||||
if strings.TrimSpace(id) == "" {
|
if strings.TrimSpace(id) == "" {
|
||||||
validation.Error(base, "profile ID must not be blank")
|
validation.Error(base, "profile ID must not be blank")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ type promptEntry struct {
|
|||||||
messages []*template.Template
|
messages []*template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolvePrompts(prompts map[string]map[string]promptConfig, resolution *configkit.Resolution) {
|
func resolvePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution) {
|
||||||
for _, id := range sortedKeys(prompts) {
|
for _, id := range sortedKeys(prompts) {
|
||||||
for _, version := range sortedKeys(prompts[id]) {
|
for _, version := range sortedKeys(prompts[id]) {
|
||||||
definition := prompts[id][version]
|
definition := prompts[id][version]
|
||||||
base := configkit.Keys("prompts", id, version)
|
base := basePath.Key(id).Key(version)
|
||||||
for _, field := range []struct {
|
for _, field := range []struct {
|
||||||
name string
|
name string
|
||||||
value **string
|
value **string
|
||||||
@@ -84,8 +84,8 @@ func resolvePrompts(prompts map[string]map[string]promptConfig, resolution *conf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *loaded) materializePrompts() error {
|
func (state *loaded) materializePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path) error {
|
||||||
state.prompts = make(map[string]map[string]*promptEntry, len(state.result.Config.Prompts))
|
state.prompts = make(map[string]map[string]*promptEntry, len(prompts))
|
||||||
state.ambiguous = make(map[string]bool)
|
state.ambiguous = make(map[string]bool)
|
||||||
var metadata *configkit.Metadata
|
var metadata *configkit.Metadata
|
||||||
lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) {
|
lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) {
|
||||||
@@ -95,21 +95,23 @@ func (state *loaded) materializePrompts() error {
|
|||||||
}
|
}
|
||||||
return metadata.Lookup(path)
|
return metadata.Lookup(path)
|
||||||
}
|
}
|
||||||
for _, id := range sortedKeys(state.result.Config.Prompts) {
|
for _, id := range sortedKeys(prompts) {
|
||||||
|
promptBase := basePath.Key(id)
|
||||||
if strings.TrimSpace(id) == "" {
|
if strings.TrimSpace(id) == "" {
|
||||||
return fmt.Errorf("%s: prompt ID must not be blank", configkit.Keys("prompts", id))
|
return fmt.Errorf("%s: prompt ID must not be blank", promptBase)
|
||||||
}
|
}
|
||||||
versions := state.result.Config.Prompts[id]
|
versions := prompts[id]
|
||||||
if versions == nil {
|
if versions == nil {
|
||||||
return fmt.Errorf("%s: prompt collection must be a mapping", configkit.Keys("prompts", id))
|
return fmt.Errorf("%s: prompt collection must be a mapping", promptBase)
|
||||||
}
|
}
|
||||||
state.prompts[id] = make(map[string]*promptEntry, len(versions))
|
state.prompts[id] = make(map[string]*promptEntry, len(versions))
|
||||||
state.ambiguous[id] = len(versions) > 1
|
state.ambiguous[id] = len(versions) > 1
|
||||||
for _, version := range sortedKeys(versions) {
|
for _, version := range sortedKeys(versions) {
|
||||||
|
versionBase := promptBase.Key(version)
|
||||||
if strings.TrimSpace(version) == "" {
|
if strings.TrimSpace(version) == "" {
|
||||||
return fmt.Errorf("%s: prompt version must not be blank", configkit.Keys("prompts", id, version))
|
return fmt.Errorf("%s: prompt version must not be blank", versionBase)
|
||||||
}
|
}
|
||||||
entry, err := state.materializePrompt(id, version, versions[version], lookupOrigin)
|
entry, err := state.materializePrompt(id, version, versions[version], versionBase, lookupOrigin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -119,8 +121,7 @@ func (state *loaded) materializePrompts() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *loaded) materializePrompt(id, version string, raw promptConfig, lookupOrigin func(configkit.Path) (configkit.FieldInfo, bool)) (*promptEntry, error) {
|
func (state *loaded) materializePrompt(id, version string, raw promptConfig, base configkit.Path, lookupOrigin func(configkit.Path) (configkit.FieldInfo, bool)) (*promptEntry, error) {
|
||||||
base := configkit.Keys("prompts", id, version)
|
|
||||||
if len(raw.Messages) == 0 {
|
if len(raw.Messages) == 0 {
|
||||||
return nil, fmt.Errorf("%s: at least one message is required", base.Key("messages"))
|
return nil, fmt.Errorf("%s: at least one message is required", base.Key("messages"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user