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
|
||||
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
|
||||
|
||||
Scope: prompt storage format, materialization, and fixture-format migration.
|
||||
|
||||
@@ -53,7 +53,7 @@ func secretProviderPath(path configkit.Path) bool {
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 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.
|
||||
func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) {
|
||||
bases := rootResourceBases()
|
||||
state := &loaded{
|
||||
backends: 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]{
|
||||
Secret: secretProviderPath,
|
||||
Resolve: func(config *document, resolution *configkit.Resolution) error {
|
||||
captureFields(config.Backends, "backends", backendFields, resolution, state.backends)
|
||||
captureFields(config.Profiles, "profiles", profileFields, resolution, state.profiles)
|
||||
capturePromptMetadata(config.Prompts, resolution, state.promptMetadata)
|
||||
resolveBackends(config.Backends, resolution)
|
||||
resolveProfiles(config.Profiles, resolution)
|
||||
resolvePrompts(config.Prompts, resolution)
|
||||
captureFields(config.Backends, bases.backends, backendFields, resolution, state.backends)
|
||||
captureFields(config.Profiles, bases.profiles, profileFields, resolution, state.profiles)
|
||||
capturePromptMetadata(config.Prompts, bases.prompts, resolution, state.promptMetadata)
|
||||
resolveBackends(config.Backends, bases.backends, resolution)
|
||||
resolveProfiles(config.Profiles, bases.profiles, resolution)
|
||||
resolvePrompts(config.Prompts, bases.prompts, resolution)
|
||||
return nil
|
||||
},
|
||||
Validate: validateEffective,
|
||||
Validate: func(config *document, validation *configkit.Validation) {
|
||||
validateEffective(config, bases, validation)
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load configuration: %w", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if err := state.buildExecutionProfiles(); err != nil {
|
||||
@@ -176,7 +193,7 @@ type promptInputPresence struct {
|
||||
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) {
|
||||
if destination[id] == nil {
|
||||
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]) {
|
||||
inputs := prompts[id][version].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 {
|
||||
path := base.Index(index)
|
||||
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 {
|
||||
states := make(map[string]fieldState, len(fields))
|
||||
for _, field := range fields {
|
||||
info, exists := resolution.InputInfo(configkit.Keys(collection, id, field))
|
||||
info, exists := resolution.InputInfo(basePath.Key(id).Key(field))
|
||||
switch {
|
||||
case !exists || !info.Explicit:
|
||||
states[field] = fieldAbsent
|
||||
@@ -244,10 +261,10 @@ func normalizeString(value **string, path configkit.Path, resolution *configkit.
|
||||
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) {
|
||||
backend := backends[id]
|
||||
base := configkit.Keys("backends", id)
|
||||
base := basePath.Key(id)
|
||||
if backend.Endpoint != nil {
|
||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*backend.Endpoint)
|
||||
if err != nil {
|
||||
@@ -301,10 +318,10 @@ func normalizeDiagnosticEndpoint(value **string, path configkit.Path, resolution
|
||||
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) {
|
||||
profile := profiles[id]
|
||||
base := configkit.Keys("profiles", id)
|
||||
base := basePath.Key(id)
|
||||
if profile.Endpoint != nil && *profile.Endpoint != "" {
|
||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint)
|
||||
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) {
|
||||
backend := config.Backends[id]
|
||||
base := configkit.Keys("backends", id)
|
||||
base := bases.backends.Key(id)
|
||||
if strings.TrimSpace(id) == "" {
|
||||
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) {
|
||||
profile := config.Profiles[id]
|
||||
base := configkit.Keys("profiles", id)
|
||||
base := bases.profiles.Key(id)
|
||||
if strings.TrimSpace(id) == "" {
|
||||
validation.Error(base, "profile ID must not be blank")
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ type promptEntry struct {
|
||||
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 _, version := range sortedKeys(prompts[id]) {
|
||||
definition := prompts[id][version]
|
||||
base := configkit.Keys("prompts", id, version)
|
||||
base := basePath.Key(id).Key(version)
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
value **string
|
||||
@@ -84,8 +84,8 @@ func resolvePrompts(prompts map[string]map[string]promptConfig, resolution *conf
|
||||
}
|
||||
}
|
||||
|
||||
func (state *loaded) materializePrompts() error {
|
||||
state.prompts = make(map[string]map[string]*promptEntry, len(state.result.Config.Prompts))
|
||||
func (state *loaded) materializePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path) error {
|
||||
state.prompts = make(map[string]map[string]*promptEntry, len(prompts))
|
||||
state.ambiguous = make(map[string]bool)
|
||||
var metadata *configkit.Metadata
|
||||
lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) {
|
||||
@@ -95,21 +95,23 @@ func (state *loaded) materializePrompts() error {
|
||||
}
|
||||
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) == "" {
|
||||
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 {
|
||||
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.ambiguous[id] = len(versions) > 1
|
||||
for _, version := range sortedKeys(versions) {
|
||||
versionBase := promptBase.Key(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 {
|
||||
return err
|
||||
}
|
||||
@@ -119,8 +121,7 @@ func (state *loaded) materializePrompts() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (state *loaded) materializePrompt(id, version string, raw promptConfig, lookupOrigin func(configkit.Path) (configkit.FieldInfo, bool)) (*promptEntry, error) {
|
||||
base := configkit.Keys("prompts", id, version)
|
||||
func (state *loaded) materializePrompt(id, version string, raw promptConfig, base configkit.Path, lookupOrigin func(configkit.Path) (configkit.FieldInfo, bool)) (*promptEntry, error) {
|
||||
if len(raw.Messages) == 0 {
|
||||
return nil, fmt.Errorf("%s: at least one message is required", base.Key("messages"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user