Files
2026-05-27 23:29:53 +01:00

72 lines
5.6 KiB
Markdown

# OpenSpec Change: env-spec-conformance
- **Change ID**: env-spec-conformance
- **Package**: `@suaveplan/env`
- **Status**: PROPOSED
- **Date**: 2026-04-26
- **Reference**: `openspec/archive/2026-04-19-enterprise-env/specs/env/spec.md`
## Summary
Close the loop on `@suaveplan/env` spec conformance: every requirement in `openspec/specs/env/spec.md` now carries a real `**Verified by:**` line, the implementation has been realigned with the spec where the spec was the contract, and the spec has been realigned with the implementation where the implementation was the intended design. The `bun scripts/openspec-conformance.ts @suaveplan/env` gate exits 0 with no `deferred` annotations and no `retroactively ticked` lies.
This change folds the previously-deferred `env-implementation-realignment` work in scope. Every divergence the original audit flagged is closed below.
## Current-state audit (post-change)
| Spec ID | Requirement | Status |
|---------|-------------|--------|
| ENV-01 | `createEnv` schema-first, type-inferred result | shipped + literal-type narrowing pinned |
| ENV-02 | Three error modes (`throw` / `collect` / `warn`) | shipped |
| ENV-03 | Loader contract; ordered merge | shipped |
| ENV-04 | `objectLoader` shape | shipped (single-options form, Zod boundary check) |
| ENV-05 | `dotenvLoader` + `parseDotenv` purity | shipped (silent-skip for malformed lines, `override` defaults `false`, Zod boundary check) |
| ENV-06 | `interpolate` / `hasInterpolation` / `extractVariables` + `createEnv` interpolation pass | shipped (fixpoint pass over merged values pre-coercion) |
| ENV-07 | `BaseField` builder contract | shipped (`addRule` on base, `optional()`/`default()` per subclass) |
| ENV-08 | Field-type coercion + built-in rules | shipped |
| ENV-09 | `validate` accumulates all errors | shipped (`{success, data, errors}` shape) |
| ENV-10 | `maskSensitive(schema, values)` never leaks | shipped (`****` glyph, schema-first arg order) |
| ENV-11 | `ENV.*` error codes via `defineCodes` | shipped (`CoercionError` now uses `ENV.INVALID_FORMAT`) |
| ENV-12 | Zod schemas at boundaries | shipped (`createEnv` / `dotenvLoader` / `objectLoader` invoke their schemas; remaining schemas marked `boundary-only`) |
| ENV-13 | Presets (common + server) | shipped |
| ENV-14 | OTel meters / counters / histogram | shipped (`@opentelemetry/api` runtime dep + `getMeter('suaveplan.env')` instruments in `createEnv`) |
| ENV-15 | `sideEffects: false` + 100% coverage gate | shipped |
| ENV-16 | Property-based tests | shipped |
| ENV-17 | No Node/browser-only built-ins outside `dotenvLoader` | shipped |
## What this change does
1. **Annotates** every requirement in `openspec/specs/env/spec.md` with a real `**Verified by:**` line. No `deferred` markers.
2. **Aligns implementation to spec** for the contracts where the spec was the source of truth:
- `CoercionError` now throws code `ENV.INVALID_FORMAT` (not `ENV.COERCE_ERROR`); the dead `COERCE_ERROR` registry entry is removed.
- `createEnv` performs a fixpoint `${VAR}` interpolation pass over the merged values map before coercion, so cross-loader chains resolve.
- `createEnv`, `dotenvLoader`, and `objectLoader` invoke their respective Zod schemas at the public boundary; invalid options surface as `ENV.VALIDATION_FAILED`.
- `createEnv` records OTel instruments (`env.validation.total`, `env.validation.errors`, `env.load.duration_ms`) via `metrics.getMeter('suaveplan.env')`. `@opentelemetry/api` is now a runtime dependency.
3. **Aligns spec to implementation** for design choices the implementation had already made, where the implementation was clearly the intended design and the original spec wording was accidentally divergent:
- `objectLoader({ source, sourceName })` single-options form.
- `parseDotenv` silently skips malformed lines (matches dotenv-ecosystem convention).
- `DotenvLoaderOptions.override` defaults to `false` (safer; explicit `source` wins).
- `BaseField.addRule()` on the base class; `optional()`/`default()` lifted to each concrete subclass because their return type narrows.
- `ValidationResult` shape is `{ success, data, errors: ReadonlyArray<EnvError> }`.
- `maskSensitive(schema, values)` argument order; mask glyph is `'****'`.
4. **Adds** boundary tests in `src/core/create-env/create-env.spec-conformance.test.ts` covering: cross-loader interpolation, unresolved-token default, Zod boundary failures for all three public APIs, and OTel non-throwing instrumentation.
5. **Records** `retroactively ticked` rectification — the historical hazard in `openspec/archive/2026-04-19-enterprise-env/tasks.md` is recognised as resolved by this change.
## Available workspace deps
| Package | Available |
|---------|-----------|
| `@suaveplan/error` | yes |
| `@suaveplan/types` | yes |
| `@opentelemetry/api` (catalog: `observability`) | yes — now a runtime dep |
| `fast-check` (devDep) | yes |
## Impact
- `@opentelemetry/api` added as a runtime dep (catalog: `observability`).
- `CoercionError.code` changed from `ENV.COERCE_ERROR` → `ENV.INVALID_FORMAT`. `ENV.COERCE_ERROR` is removed from the registry. Genesis pre-1.0 — no external consumers depend on the old code.
- `createEnv` now performs interpolation across the merged map; values that previously contained literal `${X}` text will now expand if `X` is present. Documented as ENV-06 behaviour.
- New Zod boundary checks turn invalid runtime options into `EnvValidationError` instead of `TypeError` / silent failure.
- Public API surface preserved: function signatures unchanged, only behaviour and error codes tightened to match the spec.
- Version stays at `0.1.0` (genesis pre-1.0).