feat(mcp): derive env vars from the framework's real struct tags #3

Merged
Rene Nochebuena merged 1 commits from feat/env-reconciliation into main 2026-08-07 17:00:02 -06:00
Owner

Summary

Make the environment-variable surface derive from the framework's real struct tags instead of anyone maintaining it by hand. Adds get_config_env (the truth per module), check_env (drift detection for a .env/.env.example), a config.unknown-env-var rule, and switches get_scaffold to generate .env.example from the index. Also folds in the scaffold-symbol fixes staged as v1.0.1 (never tagged), so the generated scaffold compiles against einherjar v1.0.0.


Type of change

  • New feature — non-breaking addition of functionality
  • Bug fix — non-breaking change that resolves an issue
  • Documentation update

Description

Problem. The v1.0.0 .env.example and the scaffold symbols were hand-authored and drifted from the framework: it shipped EINHERJAR_PG_DATABASE (real: _PG_NAME), EINHERJAR_SERVER_ADDR (real: _HOST/_PORT), and typed loggers as the non-existent logz.Logger. The reconciliation showed the real cause — an env var is only live if its struct is composed and parsed — and that the index already carries every real tag (index.Field.Tag), unused.

Solution.

  • internal/envspec — parses the indexed struct tags into {name, module, struct, field, required, default}. One source of truth for the three consumers below.
  • get_config_env — lists the real env vars for a module (or all).
  • check_env — flags unknown EINHERJAR_* names, required vars missing for the composed modules, and dead vars (set for an uncomposed module).
  • config.unknown-env-var — a validate_snippet rule that rejects an env:"EINHERJAR_*" tag the framework doesn't declare; the valid-name set is injected from the index at startup, so the rule can't drift.
  • get_scaffold.env.example is now derived (required vars uncommented with a dev value; defaulted vars commented with their default), and the scaffold composes logz.Config so EINHERJAR_LOG_LEVEL/_JSON are live and documented instead of silently ignored (log format is now env-driven).
  • wire builtin — routes the incremental "compose a component later" flow to get_config_env/check_env and spells out framework EINHERJAR_* vs app-owned APP_* (JWT is the app-owned example).
  • Fixes (was v1.0.1): logging.Logger, postgres.Provider, the real env tags, and the trimmed health hook.

Version → v1.1.0.


Test plan

  • go build ./... and go test ./... (mcp) — new suites for envspec, the env tools (against the real data/index.json), and the rule
  • Generated the scaffold to real files and go build ./... against local einherjar v1.0.0 modules → clean (exit 0)
  • Runtime-verified config.Load() parses the composed logz.Config (EINHERJAR_LOG_LEVEL=DEBUGslog.LevelDebug, EINHERJAR_LOG_JSON=true)
  • Rendered .env.example inspected: correct names, required uncommented with dev values, defaults commented, no EINHERJAR_SERVER_CORS_ORIGINS (uncomposed) and no _PG_DATABASE/_SERVER_ADDR
  • Redeploy and exercise get_config_env / check_env / get_scaffold from a live MCP client
## Summary Make the environment-variable surface **derive from the framework's real struct tags** instead of anyone maintaining it by hand. Adds `get_config_env` (the truth per module), `check_env` (drift detection for a `.env`/`.env.example`), a `config.unknown-env-var` rule, and switches `get_scaffold` to generate `.env.example` from the index. Also folds in the scaffold-symbol fixes staged as v1.0.1 (never tagged), so the generated scaffold compiles against einherjar v1.0.0. --- ## Type of change - [x] New feature — non-breaking addition of functionality - [x] Bug fix — non-breaking change that resolves an issue - [x] Documentation update --- ## Description **Problem.** The v1.0.0 `.env.example` and the scaffold symbols were hand-authored and drifted from the framework: it shipped `EINHERJAR_PG_DATABASE` (real: `_PG_NAME`), `EINHERJAR_SERVER_ADDR` (real: `_HOST`/`_PORT`), and typed loggers as the non-existent `logz.Logger`. The reconciliation showed the real cause — an env var is only live if its struct is composed and parsed — and that the index already carries every real tag (`index.Field.Tag`), unused. **Solution.** - **`internal/envspec`** — parses the indexed struct tags into `{name, module, struct, field, required, default}`. One source of truth for the three consumers below. - **`get_config_env`** — lists the real env vars for a module (or all). - **`check_env`** — flags unknown `EINHERJAR_*` names, required vars missing for the composed modules, and dead vars (set for an uncomposed module). - **`config.unknown-env-var`** — a `validate_snippet` rule that rejects an `env:"EINHERJAR_*"` tag the framework doesn't declare; the valid-name set is injected from the index at startup, so the rule can't drift. - **`get_scaffold`** — `.env.example` is now derived (required vars uncommented with a dev value; defaulted vars commented with their default), and the scaffold **composes `logz.Config`** so `EINHERJAR_LOG_LEVEL`/`_JSON` are live and documented instead of silently ignored (log format is now env-driven). - **`wire` builtin** — routes the incremental "compose a component later" flow to `get_config_env`/`check_env` and spells out framework `EINHERJAR_*` vs app-owned `APP_*` (JWT is the app-owned example). - **Fixes (was v1.0.1):** `logging.Logger`, `postgres.Provider`, the real env tags, and the trimmed health hook. Version → v1.1.0. --- ## Test plan - [x] `go build ./...` and `go test ./...` (mcp) — new suites for `envspec`, the env tools (against the real `data/index.json`), and the rule - [x] Generated the scaffold to real files and `go build ./...` against local einherjar v1.0.0 modules → clean (exit 0) - [x] Runtime-verified `config.Load()` parses the composed `logz.Config` (`EINHERJAR_LOG_LEVEL=DEBUG` → `slog.LevelDebug`, `EINHERJAR_LOG_JSON=true`) - [x] Rendered `.env.example` inspected: correct names, required uncommented with dev values, defaults commented, no `EINHERJAR_SERVER_CORS_ORIGINS` (uncomposed) and no `_PG_DATABASE`/`_SERVER_ADDR` - [ ] Redeploy and exercise `get_config_env` / `check_env` / `get_scaffold` from a live MCP client
Rene Nochebuena added 1 commit 2026-08-07 16:59:55 -06:00
Minor to v1.1.0. Make the whole environment-variable surface derive from the
indexed component-config tags instead of a hand-maintained list that drifts.
Also folds in the scaffold-symbol fixes staged as v1.0.1 (never tagged); the
generated scaffold compiles clean against einherjar v1.0.0.

internal/envspec (new):
- Parse index struct tags into env vars {name, module, struct, field, required,
  default}. One source of truth: ParseTag, ForModule, FindStruct, All, KnownNames.

internal/tools:
- get_config_env: list the real env vars a component config reads (or all).
- check_env: flag unknown EINHERJAR_* names, required vars missing for the
  composed modules, and dead vars (set for an uncomposed module).
- get_scaffold: .env.example is now DERIVED from the index — required vars
  uncommented with a dev value, defaulted vars commented with their default.
  The scaffold now composes logz.Config (Log logz.Config) so EINHERJAR_LOG_*
  are live and documented, not hardcoded/ignored (log format is env-driven).
- validate_snippet: inject the real env-var name set into the rules package.

internal/index/builtins (wire conventions):
- Route the incremental "compose a component later" flow to get_config_env /
  check_env; distinguish framework EINHERJAR_* from app-owned APP_* (JWT).
- Compose logz.Config in the config + Run() examples, to match the scaffold.

internal/rules:
- config.unknown-env-var (twelfth rule): reject an env:"EINHERJAR_*" struct tag
  the framework doesn't declare. No-op until the server injects the name set, so
  it never fires on incomplete knowledge.

Fixed (was v1.0.1): scaffold health hook + wire builtin used logz.Logger (real:
contracts/logging.Logger) and postgres.Component (hooks take Provider); env tags
were EINHERJAR_SERVER_ADDR / EINHERJAR_PG_DATABASE (real: _HOST/_PORT / _PG_NAME).

Tests: envspec unit tests; env tools against the real data/index.json; the rule.
Verified by generating the scaffold, building it against local einherjar v1.0.0
(exit 0), and runtime-loading the composed logz.Config (EINHERJAR_LOG_LEVEL=DEBUG
-> slog.LevelDebug, EINHERJAR_LOG_JSON=true). Version bumped to v1.1.0 (badge +
serverVersion). No dependency changes.
Rene Nochebuena requested review from CoreDevelopers 2026-08-07 16:59:55 -06:00
Rene Nochebuena requested review from Agents 2026-08-07 16:59:55 -06:00
Rene Nochebuena merged commit 850b63607c into main 2026-08-07 17:00:02 -06:00
Rene Nochebuena deleted branch feat/env-reconciliation 2026-08-07 17:00:02 -06:00
Sign in to join this conversation.
No Reviewers
einherjar/CoreDevelopers
einherjar/Agents
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: einherjar/mcp#3