Skip to content

feat(nucleus): ADR-010 Phase 2a — FromConfigFile single-file loader#73

Merged
jcsvwinston merged 1 commit into
mainfrom
feat/adr-010-phase2a-fromconfigfile
May 16, 2026
Merged

feat(nucleus): ADR-010 Phase 2a — FromConfigFile single-file loader#73
jcsvwinston merged 1 commit into
mainfrom
feat/adr-010-phase2a-fromconfigfile

Conversation

@jcsvwinston
Copy link
Copy Markdown
Owner

Summary

Replaces the Phase 1 ErrConfigLoaderNotImplemented stub 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

Sub-PR Scope
2a (this PR) Single-file FromConfigFile + 1 MiB cap + strict schema + did-you-mean hints
2b (next) Multi-file merge + _append/_remove operators + TOML/JSON parsers + non-nullable security keys
2c WithUnknownFields("warn") + NUCLEUS_ENV=production strict override + startup WARN
2d Migration namespacing (<module_name>/<filename> checksum key in pkg/db/migrate.go)

Loader guards (ADR-010 §17 + §2 validation layers 1-2)

  • 1 MiB per-file size cap (MaxConfigFileBytes) enforced before the YAML parser. Eliminates anchor-expansion / deep-nesting DoS against gopkg.in/yaml.v3. Via io.LimitReader(path, cap+1) to detect overshoot.
  • Extension-based parser inference: .yaml/.yml today, .toml/.jsonErrUnsupportedConfigFormat with Phase 2b reference.
  • Strict-unknown-fields schema validation against app.ContractConfigKeyPatterns(). Unknown keys → ErrUnknownConfigKeys with did-you-mean hints (Levenshtein ≤3 on the final segment).
  • Wildcard pattern matcher for map-typed slots like databases.*.url and jwt_keys.*.kid.

Builder integration

  • AppBuilder.FromConfigFile(path) invokes the real loader; multi-path fails fast referencing Phase 2b.
  • Modules/Middleware/Services/Lifecycle registered before FromConfigFile are preserved — only the embedded app.Config slot is replaced (regression test included).
  • ErrConfigLoaderNotImplemented removed. Pre-v1.0 clean break per ADR-006/ADR-008 precedent.

Freeze baseline delta

Direction Symbol
+ const:MaxConfigFileBytes
+ var:ErrConfigFileTooLarge
+ var:ErrUnsupportedConfigFormat
+ var:ErrUnknownConfigKeys
var: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)

  • Happy path YAML load + defaults preserved for unset keys
  • Unsupported extension rejected
  • TOML/JSON paths return Phase-2b sentinel
  • File over cap → ErrConfigFileTooLarge; file exactly at boundary → accepted
  • Unknown key → ErrUnknownConfigKeys with did-you-mean hint
  • Missing file → file-system error (not content error)
  • Malformed YAML → parse error
  • Empty path rejected
  • AppBuilder FromConfigFile end-to-end happy path + preserves prior Mount
  • Levenshtein basics
  • Wildcard pattern matcher (databases.*.url, jwt_keys.*.kid)

All 38 pkg/nucleus tests pass locally (24 pre-existing + 13 config + the 1 multi-path replacement test).

Local validation

  • go build ./... — clean
  • go vet ./... — clean
  • go test ./... — green
  • bash 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 // indirect annotation on AWS SDK config/secretsmanager modules (they are direct deps of pkg/auth/secrets) and on the Prometheus + OpenTelemetry/Prometheus exporter modules (used by pkg/observability). No functional change — just go.mod correctness. Resolves the long-standing housekeeping note about go mod tidy not running cleanly.

NOT in this PR

Test plan

  • Test And Smoke — full pkg/nucleus suite + 13 new config tests
  • Contract Freeze — confirms the +4/-1 baseline delta
  • Compatibility Harness — no contract regressions on the still-frozen surface
  • All 4 DB matrix lanes (unchanged path; should pass)

🤖 Generated with Claude Code

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>
@jcsvwinston jcsvwinston merged commit 2b650f3 into main May 16, 2026
9 checks passed
@jcsvwinston jcsvwinston deleted the feat/adr-010-phase2a-fromconfigfile branch May 16, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant