RUL-019 — Config modules: Pydantic BaseSettings + auto-loaded .env

Ruling

Central config modules (core/infra_config.py) use Pydantic BaseSettings, and env files must be auto-loaded (_load_dotenv()) by every entrypoint — never left to direnv or manual sourcing. _load_dotenv() loads the secrets-only .env first, then overlays the active ORCHESTRATA_ENV_PROFILE (env/{dev,test,prod}.env, endpoint-only) with override=True. This is distinct from RUL-003: RUL-003 governs call-time reads for override-sensitive code paths; this ruling governs the shape of the config module itself.

Evidence

Enforcement

Verification

The loading clause is mechanized; the BaseSettings clause stays judgment. Two bullets, deliberately — they cover different halves of this ruling.

scripts/check_env_contract.py (invoked from scripts/health_check.py as "Env contract (RUL-019)") is a static AST gate with two rules:

The Rule-B trigger search is bounded (--max-depth 1 default) after measuring that an unbounded closure produced 16 findings, 14 of them traceable to one lazily-executed call site (core/graph/adapters/bolt_async.py:88) — day-one false positives are how a gate gets switched off. The bound errs toward false negatives and is therefore never invisible: every run prints the deep_scope_only entrypoints that would be in scope at unlimited depth, and --max-depth -1 audits that region.

The gate landed RED against the live repo — 4 genuine violations, which were then fixed rather than suppressed:

Resolution (2026-08-05): src/orchestrata_ultimate/core/env_bootstrap.py::load_env_contract now holds the single canonical two-phase implementation (lifted from scripts/start_test_services.py::_load_dotenv, the only correct one that existed); agents/runtime_main.py and knowledge_graph/server.py call it, and gateway/cli.py / kernel/cli_client.py are folded onto it. No baseline, no allowlist, no ratchet — the four violations were fixed in the code, not suppressed in the gate. Re-measured: [env-contract] OK — RUL-019 satisfied, 900 modules, 29 entrypoints, 5 in scope at depth 1, 4 ExecStart targets resolved.

False-positive hardening, same day — two classes found by adversarial review, both fixed, both recall-neutral (the red-first regression of the kernel fix still fails, and a second hand-rolled mutating loader is still caught):

The remaining bullet is honest: no lint can enforce "new config modules must use Pydantic BaseSettings" — infra_config.py is a reference implementation to imitate, not a pattern a gate can check for absence elsewhere. That clause stays judgment.