// Package config is executus's runtime-configuration seam. // // A host supplies a Source so the harness can read tunable knobs (model tiers, // caps, thresholds, lane widths) without depending on any particular config // backend. Mort adapts its DB-backed convar.Manager; Gadfly adapts environment // variables; a brand-new project can use Env (or pass a nil Source and rely on // the code defaults every reader provides). // // Design rules: // - Every accessor takes a code default. A Source is NEVER required to know a // key — readers degrade to the default, so the harness runs with zero config. // - Reads are LIVE: callers read on every use so a host whose backend mutates // at runtime (e.g. convar) propagates without a restart. Sources that cache // (mort's 5-minute convar cache) may additionally implement Reloader to // signal invalidation. package config import ( "os" "strconv" "strings" ) // Source is the host configuration seam. All methods take a default and must be // safe for concurrent use. type Source interface { String(key, def string) string Int(key string, def int) int Float(key string, def float64) float64 Bool(key string, def bool) bool } // Reloader is an optional capability for Sources whose values can change at // runtime and that can notify watchers (e.g. a tier-reload or cache // invalidation). Sources that do not implement it are simply read live on every // access. Watch returns a cancel func; a nil-safe no-op is acceptable. type Reloader interface { Watch(prefix string, fn func(key string)) (cancel func()) } // Nil-safe package helpers: callers that may hold a nil Source use these instead // of dereferencing. They let every battery treat config as optional. func String(s Source, key, def string) string { if s == nil { return def } return s.String(key, def) } func Int(s Source, key string, def int) int { if s == nil { return def } return s.Int(key, def) } func Float(s Source, key string, def float64) float64 { if s == nil { return def } return s.Float(key, def) } func Bool(s Source, key string, def bool) bool { if s == nil { return def } return s.Bool(key, def) } // Env is the default Source: it reads process environment variables. A key is // mapped to an env var name by uppercasing it and replacing every rune outside // [A-Za-z0-9] with '_', then prefixing. So Env("GADFLY_").String("models", "") // reads GADFLY_MODELS, and Env("").Int("model.tier.fast.max_steps", 8) reads // MODEL_TIER_FAST_MAX_STEPS. An unset or unparseable value yields the default. func Env(prefix string) Source { return envSource{prefix: prefix} } type envSource struct{ prefix string } func (e envSource) envName(key string) string { var b strings.Builder b.WriteString(e.prefix) for _, r := range key { switch { case r >= 'a' && r <= 'z': b.WriteRune(r - 32) case (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9'): b.WriteRune(r) default: b.WriteByte('_') } } return b.String() } func (e envSource) raw(key string) (string, bool) { v, ok := os.LookupEnv(e.envName(key)) if !ok { return "", false } return strings.TrimSpace(v), true } func (e envSource) String(key, def string) string { if v, ok := e.raw(key); ok && v != "" { return v } return def } func (e envSource) Int(key string, def int) int { if v, ok := e.raw(key); ok { if n, err := strconv.Atoi(v); err == nil { return n } } return def } func (e envSource) Float(key string, def float64) float64 { if v, ok := e.raw(key); ok { if f, err := strconv.ParseFloat(v, 64); err == nil { return f } } return def } func (e envSource) Bool(key string, def bool) bool { if v, ok := e.raw(key); ok { if b, err := strconv.ParseBool(v); err == nil { return b } } return def }