[agent/codex:gpt-5.4-mini] Update the code against the AX design principles in ~/spec/r... #3

Merged
Virgil merged 1 commit from agent/update-the-code-against-the-ax-design-pr into dev 2026-03-31 18:00:26 +00:00
3 changed files with 72 additions and 12 deletions

View file

@ -1,6 +1,6 @@
// Package config provides layered configuration management for the Core framework.
//
// Configuration values are resolved in priority order: defaults -> file -> env -> flags.
// Configuration values are resolved in priority order: defaults -> file -> env -> Set().
// Values are stored in a YAML file at ~/.core/config.yaml by default.
//
// Keys use dot notation for nested access:
@ -11,6 +11,7 @@
package config
import (
"errors"
"iter"
"maps"
"os"
@ -101,24 +102,21 @@ func New(opts ...Option) (*Config, error) {
}
// LoadFile reads a configuration file from the given medium and path and merges it into the current config.
// It supports YAML and environment files (.env).
// It supports YAML, JSON, TOML, and environment files (.env).
func (c *Config) LoadFile(m coreio.Medium, path string) error {
c.mu.Lock()
defer c.mu.Unlock()
configType, err := configTypeForPath(path)
if err != nil {
return coreerr.E("config.LoadFile", "unsupported config file type: "+path, err)
}
content, err := m.Read(path)
if err != nil {
return coreerr.E("config.LoadFile", "failed to read config file: "+path, err)
}
ext := filepath.Ext(path)
configType := "yaml"
if ext == "" && filepath.Base(path) == ".env" {
configType = "env"
} else if ext != "" {
configType = strings.TrimPrefix(ext, ".")
}
// Load into file-backed viper
c.f.SetConfigType(configType)
if err := c.f.MergeConfig(strings.NewReader(content)); err != nil {
@ -134,6 +132,24 @@ func (c *Config) LoadFile(m coreio.Medium, path string) error {
return nil
}
func configTypeForPath(path string) (string, error) {
ext := filepath.Ext(path)
switch {
case ext == "" && filepath.Base(path) == ".env":
return "env", nil
case ext == "":
return "yaml", nil
}
configType := strings.TrimPrefix(ext, ".")
switch configType {
case "yaml", "yml", "json", "toml", "env":
return configType, nil
default:
return "", errors.New("unsupported config file type")
}
}
// Get retrieves a configuration value by dot-notation key and stores it in out.
// If key is empty, it unmarshals the entire configuration into out.
// The out parameter must be a pointer to the target type.

View file

@ -246,6 +246,50 @@ func TestConfig_LoadFile_Env(t *testing.T) {
assert.Equal(t, "bar", foo)
}
func TestConfig_LoadFile_JSON_Good(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/tmp/test/config.json"] = `{"service":{"name":"core"}}`
cfg, err := New(WithMedium(m), WithPath("/config.yaml"))
assert.NoError(t, err)
err = cfg.LoadFile(m, "/tmp/test/config.json")
assert.NoError(t, err)
var name string
err = cfg.Get("service.name", &name)
assert.NoError(t, err)
assert.Equal(t, "core", name)
}
func TestConfig_LoadFile_TOML_Good(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/tmp/test/config.toml"] = "service = { name = \"core\" }\n"
cfg, err := New(WithMedium(m), WithPath("/config.yaml"))
assert.NoError(t, err)
err = cfg.LoadFile(m, "/tmp/test/config.toml")
assert.NoError(t, err)
var name string
err = cfg.Get("service.name", &name)
assert.NoError(t, err)
assert.Equal(t, "core", name)
}
func TestConfig_LoadFile_Unsupported_Bad(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/tmp/test/config.txt"] = "service = core\n"
cfg, err := New(WithMedium(m), WithPath("/config.yaml"))
assert.NoError(t, err)
err = cfg.LoadFile(m, "/tmp/test/config.txt")
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported config file type")
}
func TestConfig_WithEnvPrefix(t *testing.T) {
t.Setenv("MYAPP_SETTING", "secret")

View file

@ -5,7 +5,7 @@ description: Layered configuration management for the Core framework with file,
# config
`forge.lthn.ai/core/config` provides layered configuration management for applications built on the Core framework. It resolves values through a priority chain -- defaults, file, environment variables, flags -- so that the same codebase works identically across local development, CI, and production without code changes.
`forge.lthn.ai/core/config` provides layered configuration management for applications built on the Core framework. It resolves values through a priority chain -- defaults, file, environment variables, and explicit `Set()` calls -- so that the same codebase works identically across local development, CI, and production without code changes.
## Module Path
@ -85,7 +85,7 @@ Values are resolved in ascending priority order:
1. **Defaults** -- hardcoded fallbacks (via `Set()` before any file load)
2. **File** -- YAML loaded from `~/.core/config.yaml` (or a custom path)
3. **Environment variables** -- prefixed with `CORE_CONFIG_` by default
4. **Explicit Set()** -- in-memory overrides applied at runtime
4. **Explicit `Set()`** -- in-memory overrides applied at runtime
Environment variables always override file values. An explicit `Set()` call overrides everything.