residual/config: standalone config library (v1) #21
Labels
No labels
harness
proj-core-console
proj-core-demo
proj-core-edit
proj-core-init
proj-core-lib
proj-core-login
proj-core-res
proj-core-sh
proj-core-theme
proj-docs
proj-extra-calc
proj-extra-code
proj-extra-playbook
proj-extra-sheets
proj-harness
proj-os
residual
tier-0-trivial
tier-1-easy
tier-2-medium
tier-3-hard
tier-4-major
tier-5-epic
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
residual/.agent#21
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Config Library v2 — Revised Design
Overview
Major redesign of the config library based on user feedback. Key changes:
resolidtoconfPREFIX__KEY__SUBKEY=VAL)residualconfigsection in config filesloadRaw(no circular dependencies on BurntSushi/toml)Design Principles
Revised API
App Configuration
Load Functions
Merge Functions
Write Functions
Per-Field Merge Policies
Users define merge rules in the config file's reserved
residualconfigsection:The library wraps the user's struct internally:
Internal Parsing (loadRaw)
To avoid circular dependencies, the library parses config files using raw byte/map approach:
map[string]interface{}using format-specific decoder (TOML/YAML/JSON)residualconfigsection → merge rulesThis means the library uses BurntSushi/toml for TOML decoding (external dependency), but NOT for its own internal struct parsing.
Struct Tags
conf:"name"default:"value"optional:"true"envsep:"X"\n)raw:"-"Env Var Format
Double underscores separate segments:
Load Cascade (Default Order)
Implementation Plan
Phase 1: Restructure (tag.go, handlers.go)
resolidtoconfrawtag for internal fieldsinspectFieldsto use new tag namesPhase 2: App struct (app.go)
Appstruct with Name, Group, SystemDir, UserDirNew(name, group)with XDG defaultsNewWithOptionsfor custom dirsSystemPath(ext)andUserPath(ext)SystemConfigDirandUserConfigDirPhase 3: Load functions (load.go)
loadRawfor raw byte/map parsingDefaults[T]()— tag defaults onlySystem[T]()— load from system dirUser[T]()— load from user dirReadFile[T](path)— load from explicit pathEnv[T](prefix)— load from env vars (double underscore)Phase 4: Merge functions (merge.go)
UseConfig[T](base, overlay)— cascade mergeOverwrite[T](base, overlay)— full overwriteOverwriteEnv[T](base, overlay)— env always winsResidualConfigfor per-field merge rulesPhase 5: Write functions (write.go)
WriteUserto useApp.UserPath(ext)WriteSystemto useApp.SystemPath(ext)WriteFilefor explicit pathsPhase 6: Tests (config_test.go)
Phase 7: Documentation
Verification
Risk Assessment
v1 Implementation Complete
All 7 phases done. Summary:
Files created
config.go— path helpers (SystemConfigDir, UserConfigDir, RuntimeDir, etc.)template.go— RenderTemplate with ${CONF__...} placeholder expansiontag.go— struct tag parsing (resolid, default, optional, envsep)handlers.go— format dispatch table (TOML implemented, YAML/JSON stubbed)env.go— reflection-based env overlay with type coercionload.go— Load[T] with full cascade (6 steps)write.go— WriteUser, WriteSystem, WriteFile with marshal rulesREADME.md— usage examples, tag reference, cascade explanationTest coverage
24 tests passing:
API surface
What's NOT done (explicit non-goals)
core/migration (separate session)Next steps
core/lib/configcallersWildcard Support Added to Plan
Per-field merge rules now support wildcards via
path.Match:Matching rules
*→ matches everything (blanket)section.*→ matches all fields under section (one level)section.*.key→ matches nested wildcardshostmatches onlyhostConflict resolution
If field matches both
cascadeandoverwrite,overwritewins (consistent with env always-overwrites principle).Implementation
Uses Go's
path.Matchwith.→/substitution. No external dependencies needed.v2 Implementation Complete
All 8 phases done. Commit
6f2dfc7.Files created/modified
config.goapp.gotag.gohandlers.goenv.goload.gomerge.gowrite.gocmd/res-validate-config/main.goKey changes from v1
app := config.New("myapp", "residual")MYAPP__PORT=3000app.Defaults[T](),app.System[T](), etc.config.UseConfig(base, overlay),config.Overwrite(base, overlay)residualconfigsection in config filesdatabase.*matches all database fieldsTest utility
What's NOT done
core/migration (separate session)Next steps
core/lib/configcallersAudit fixes applied (commit 65997f8)
All audit findings addressed:
Violations:
RuntimeDir()/IPCSocketPath()fromapp.go— not config concernsResidualConfignaming — approved (it IS the residual config library)Issues:
homeDir()now usesos.UserHomeDir()first, falls back to$HOME, then/OverwriteEnv→CascadeEnv(matches actual cascade merge behavior)Smells:
handlers.gores-validate-configto import config package directlySkipped (cosmetic):
fieldMeta.RawExcludevisible in godoc — unexported struct, low priorityVerification:
go build,go test,go vetall pass.Directory restructure (commit d338232)
Cleaned up root directory:
Before: 23 entries (9 source, 5 test, 4 docs, 5 other)
After: 17 entries (7 source, 5 test, 2 dirs, 3 other)
Changes:
AUDIT.md,handoff.md,residual-overview.md→docs/tag.go→load.go(tag inspection is core to loading)handlers.go→write.go(handlers shared between read/write)Root now:
Sub-package restructure (commit 52a0b2e)
Split config library into
core/sub-package with stub passthrough at root.Structure:
Import path:
git.merith.xyz/residual/config— easy API via stubs.Implementation:
git.merith.xyz/residual/config/core— full implementation.Users get the clean API:
README and godoc fixed (Phase 7 — documentation)
Stale documentation corrected to match the actual v2 API. Changes:
config.go (godoc):
config.Merge()functionapp.Defaults[T]()method call (not a method on App)conf:"-"tag to struct tag listDefaults → System → User → EnvREADME.md (full rewrite of stale sections):
UseConfig/CascadeEnvconf:"-"row; documentedoptionalas reserved for future write optimizationMergeWithRulesandCascadeEnvwith descriptionsSystem/Userreturn zero value on missing fileRenderTemplateand${CONF__SECTION__KEY}formatEnv()semantics: returns zero struct with only defined vars populatedVerification:
go build,go test,go vet,go docall pass.