feat(nucleus): ADR-010 Phase 2a — FromConfigFile single-file loader#73
Merged
Merged
Conversation
Replaces the Phase 1 `ErrConfigLoaderNotImplemented` stub with a real single-file YAML loader, the first slice of ADR-010 §2 (Phase 2 work sliced into 2a / 2b / 2c / 2d sub-iterations). Loader guards (ADR-010 §17 compliance + §2 validation layers 1-2): - **1 MiB per-file size cap** (`MaxConfigFileBytes`) enforced before the YAML parser ever runs — eliminates the anchor-expansion / deep-nesting DoS classes against `gopkg.in/yaml.v3`. Implemented via `io.LimitReader(path, cap+1)`: the +1 detects overshoot so the structured `ErrConfigFileTooLarge` is returned rather than a parser surprise. `os.Stat` is NOT used as the only check (procfs / FUSE can lie about size); reading is the source of truth. - **Extension-based parser inference**. `.yaml` / `.yml` work today via the existing koanf YAML parser. `.toml` / `.json` return `ErrUnsupportedConfigFormat` with a Phase 2b reference; the parsers land alongside the multi-file merge engine. - **Strict-unknown-fields schema validation** against `app.ContractConfigKeyPatterns()`. Two koanf instances are used: one to enumerate the file's keys for the strict check, and a second one for the actual layered load (defaults < file). Unknown keys surface as `ErrUnknownConfigKeys` listing every offending key with did-you-mean hints (Levenshtein distance ≤3 on the final segment). - **Wildcard pattern matching**. `keyMatchesAny` matches a flat koanf key against schema patterns segment-by-segment, with `*` as a single-segment wildcard. Recognises map-typed schema slots like `databases.*.url` and `jwt_keys.*.kid`. Builder integration: - `AppBuilder.FromConfigFile(path)` now invokes the real loader. - Multi-path `FromConfigFile(a, b)` fails fast with a Phase 2b reference — the merge engine is the next sub-PR. - Modules / Middleware / Services / Lifecycle registered BEFORE the `FromConfigFile` call are preserved; only the embedded `app.Config` slot is replaced (regression test included). - `ErrConfigLoaderNotImplemented` is removed entirely — Phase 1 stub retired. Pre-`v1.0` clean break per the ADR-006 / ADR-008 precedent. New exported surface added to the freeze baseline: - const `MaxConfigFileBytes` - var `ErrConfigFileTooLarge` - var `ErrUnsupportedConfigFormat` - var `ErrUnknownConfigKeys` Removed from the freeze baseline: - var `ErrConfigLoaderNotImplemented` (Phase 1 stub) Net delta: +4 / -1 entries in `contracts/baseline/api_exported_symbols.txt`. New dependency: - `github.com/knadh/koanf/providers/rawbytes v1.0.0` — sibling of the YAML provider already in tree. Zero-go, used by the loader to feed the file bytes into koanf without going through the filesystem twice (the first read is constrained by the 1 MiB cap). Tests added in `pkg/nucleus/config_test.go` (13 cases): - Happy-path YAML load - Defaults preserved for unset keys - Unsupported extension rejected (.ini etc.) - TOML/JSON paths return Phase-2b sentinel - File over the cap → ErrConfigFileTooLarge - File exactly at the cap boundary → accepted - Unknown key → ErrUnknownConfigKeys - Did-you-mean hint surfaces for plausible typos - Missing file is reported as a file error, not a content error - Malformed YAML produces a parse error - Empty path rejected - `AppBuilder.FromConfigFile` end-to-end happy path - `AppBuilder.FromConfigFile` preserves Modules mounted earlier - Levenshtein basics - Wildcard pattern matcher (`databases.*.url`, `jwt_keys.*.kid`) Local validation: go build ./... clean; go vet ./... clean; go test ./... green (all 38 pkg/nucleus tests including the 13 new config tests); `bash scripts/ci/check_contract_freeze.sh` green. `go mod tidy` side effects: the AWS SDK config / secretsmanager modules previously annotated `// indirect` are now correctly `// direct` (they are direct deps of `pkg/auth/secrets`); the Prometheus and OpenTelemetry/Prometheus exporter modules used by `pkg/observability` similarly promoted. No functional change, just correctness in `go.mod`. State files and the Phase 1 iteration archive (`docs/iterations/2026-05-16-adr010-phase1-and-examples-purge.md`) are intentionally NOT staged here — they belong to the state-close PR following the convention established by #61 / #64 / #68. Phase 2 sub-PR map: - 2a (this PR) — single-file FromConfigFile + size cap + strict schema. - 2b — multi-file merge + `_append`/`_remove` suffix operators + TOML/JSON parsers + non-nullable security keys. - 2c — `WithUnknownFields("warn")` + `NUCLEUS_ENV=production` strict override + startup WARN. - 2d — migration namespacing in `pkg/db/migrate.go`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the Phase 1
ErrConfigLoaderNotImplementedstub with a real single-file YAML loader. First slice of ADR-010 §2 (Phase 2 work, sliced into 2a / 2b / 2c / 2d).Phase 2 slicing
FromConfigFile+ 1 MiB cap + strict schema + did-you-mean hints_append/_removeoperators + TOML/JSON parsers + non-nullable security keysWithUnknownFields("warn")+NUCLEUS_ENV=productionstrict override + startup WARN<module_name>/<filename>checksum key inpkg/db/migrate.go)Loader guards (ADR-010 §17 + §2 validation layers 1-2)
MaxConfigFileBytes) enforced before the YAML parser. Eliminates anchor-expansion / deep-nesting DoS againstgopkg.in/yaml.v3. Viaio.LimitReader(path, cap+1)to detect overshoot..yaml/.ymltoday,.toml/.json→ErrUnsupportedConfigFormatwith Phase 2b reference.app.ContractConfigKeyPatterns(). Unknown keys →ErrUnknownConfigKeyswith did-you-mean hints (Levenshtein ≤3 on the final segment).databases.*.urlandjwt_keys.*.kid.Builder integration
AppBuilder.FromConfigFile(path)invokes the real loader; multi-path fails fast referencing Phase 2b.Modules/Middleware/Services/Lifecycleregistered beforeFromConfigFileare preserved — only the embeddedapp.Configslot is replaced (regression test included).ErrConfigLoaderNotImplementedremoved. Pre-v1.0clean break per ADR-006/ADR-008 precedent.Freeze baseline delta
const:MaxConfigFileBytesvar:ErrConfigFileTooLargevar:ErrUnsupportedConfigFormatvar:ErrUnknownConfigKeysvar:ErrConfigLoaderNotImplemented(Phase 1 stub retired)New dependency
github.com/knadh/koanf/providers/rawbytes v1.0.0— sibling of the YAML provider already vendored. Zero-go, used to feed the cap-bounded file bytes into koanf without re-reading from disk.Tests (13 new in
config_test.go)ErrConfigFileTooLarge; file exactly at boundary → acceptedErrUnknownConfigKeyswith did-you-mean hintFromConfigFileend-to-end happy path + preserves priorMountdatabases.*.url,jwt_keys.*.kid)All 38
pkg/nucleustests pass locally (24 pre-existing + 13 config + the 1 multi-path replacement test).Local validation
go build ./...— cleango vet ./...— cleango test ./...— greenbash scripts/ci/check_contract_freeze.sh— green (new entries seeded; Phase 1 stub removal recorded)go.mod tidy side effects
go mod tidy(run because of the new dep) corrected the// indirectannotation on AWS SDK config/secretsmanager modules (they are direct deps ofpkg/auth/secrets) and on the Prometheus + OpenTelemetry/Prometheus exporter modules (used bypkg/observability). No functional change — justgo.modcorrectness. Resolves the long-standing housekeeping note aboutgo mod tidynot running cleanly.NOT in this PR
HANDOFF.md,CURRENT_ITERATION.md,docs/iterations/2026-05-16-adr010-phase1-and-examples-purge.md) — belongs to its own state-close PR following the convention from chore(state): close CSRF hardening iteration #61 / chore(state): close slog secret-redaction iteration #64 / chore(state): close 2026-05-15 MSSQL/Oracle SchemaDrift iteration #68. Owner has pre-prepared those files in the working tree.Test plan
Test And Smoke— full pkg/nucleus suite + 13 new config testsContract Freeze— confirms the +4/-1 baseline deltaCompatibility Harness— no contract regressions on the still-frozen surface🤖 Generated with Claude Code