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

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.
This commit is contained in:
2026-08-07 16:58:24 -06:00
parent a0b803cb40
commit 8c6cf2c27a
14 changed files with 1003 additions and 50 deletions
+50
View File
@@ -6,6 +6,56 @@ This module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html
---
## [1.1.0] — 2026-08-07
Minor release. The env-var story is now **derived from the framework's real struct tags** end
to end: a tool to read the truth, `.env.example` generated from it, and a rule that rejects
invented names. Also folds in the scaffold-symbol fixes that were staged as v1.0.1 (never
tagged separately). The generated scaffold compiles clean against einherjar v1.0.0.
### Added
- **`get_config_env` tool.** Lists the real environment variables a component config reads —
name, declaring module/struct/field, `required`, and default — parsed from the indexed
struct tags (`internal/envspec`). The single source of truth for which `EINHERJAR_*` vars
exist; no hand-maintained list to drift.
- **`check_env` tool.** Checks a `.env` / `.env.example` against the framework: flags
`EINHERJAR_*` names that don't exist (e.g. `EINHERJAR_PG_DATABASE`), required vars missing
for the modules an app composes, and vars set for a module it does not compose (dead vars,
the way `EINHERJAR_SERVER_CORS_ORIGINS` is inert unless you compose `web.Config`).
- **`config.unknown-env-var` rule** (now twelve). `validate_snippet` rejects any struct field
tagged `env:"EINHERJAR_*"` whose name the framework does not declare — the exact class of
drift that shipped `EINHERJAR_PG_DATABASE` / `_SERVER_ADDR`. The valid-name set is injected
from the index at startup, so the rule can never drift from the real tags.
### Changed
- **`get_scaffold` now derives `.env.example` from the index** instead of a hand-written
string. Required vars (no default) are emitted uncommented with a runnable dev value;
defaulted vars are emitted commented, documenting the framework default. Names and defaults
can no longer drift from the modules the scaffold composes (`core/logz` + `web/server` +
`db-postgres`).
- **The scaffold composes `logz.Config`** instead of hard-coding the logger, so
`EINHERJAR_LOG_LEVEL` / `_JSON` are live and documented rather than silently ignored.
Verified end to end: `env.Parse` loads `slog.Level` from `EINHERJAR_LOG_LEVEL`. (Log format
is now env-driven; set `EINHERJAR_LOG_JSON=true` in production.)
- **The `wire` builtin routes the incremental flow to the new tools.** Composing a component
later now points at `get_config_env("<module>")` for its exact vars and `check_env` to
confirm `.env.example` is complete — and spells out the split between framework `EINHERJAR_*`
(discoverable, name-checked) and app-owned `APP_*` (your discipline).
### Fixed
- **`logz.Logger``logging.Logger`.** The logger interface is `contracts/logging.Logger`;
`logz.New` returns it. The scaffold's health hook and the `wire` builtin's `withUsers`
example typed loggers as the non-existent `logz.Logger`.
- **`postgres.Component``postgres.Provider`** in hook signatures. `postgres.New` returns a
`Component` (a lifecycle component that embeds `Provider`); hooks and `NewUnitOfWork` take a
`Provider`.
- **The real env tags throughout:** `EINHERJAR_SERVER_ADDR``EINHERJAR_SERVER_HOST` +
`_PORT`, and `EINHERJAR_PG_DATABASE``EINHERJAR_PG_NAME` (plus `EINHERJAR_PG_SSL_MODE`).
- The scaffold's minimal health hook no longer takes unused `logger`/`db` parameters.
## [1.0.0] — 2026-08-07
The MCP reaches **v1.0.0**, aligned with the v1.0.0 framework. The headline is the