129 lines
11 KiB
Markdown
129 lines
11 KiB
Markdown
> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/config/env.
|
||
|
||
# Tasks: enterprise-env
|
||
|
||
## 1. Scaffold
|
||
|
||
- [x] 1.1 Create `packages/universal/env/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton
|
||
- [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul
|
||
- [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed, tree-shake
|
||
- [x] 1.4 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml`
|
||
|
||
## 2. Constants + types + errors
|
||
|
||
- [x] 2.1 Implement `src/constants/index.ts` — `DEFAULT_ARRAY_SEPARATOR`, `EMAIL_PATTERN`, `FALSY_VALUES`, `INTERPOLATION_PATTERN`, `PORT_MAX`, `PORT_MIN`, `TRUTHY_VALUES`
|
||
- [x] 2.2 Implement `src/types/common.ts` — `AnyField`, `CreateEnvOptions`, `EnvError`, `EnvSchema`, `ErrorMode`, `InferEnv`, `InferFieldType`, `Loader`, `LoaderResult`, `OptionalKeys`, `RequiredKeys`, `Simplify`, `ValidationRule`
|
||
- [x] 2.3 Implement `src/errors/env-error.ts` — `EnvError` base extends `SuaveplanError`; `EnvValidationError`, `MissingEnvError`, `CoercionError`, `DotenvParseError` extend `EnvError`; `defineCodes` for `ENV.VALIDATION_FAILED`, `ENV.MISSING_REQUIRED`, `ENV.INVALID_FORMAT`, `ENV.PARSE_ERROR`
|
||
- [x] 2.4 Implement `src/errors/error-codes.ts` and auto-register via side-effect import in `src/index.ts`
|
||
- [x] 2.5 Zod schemas: `FieldConfigSchema`, `ValidationResultSchema`, `CreateEnvOptionsSchema`, `DotenvLoaderOptionsSchema`, `ObjectLoaderOptionsSchema` — exported from `*.schemas.ts` siblings
|
||
- [x] 2.6 Tests: error construction, code narrowing, `{ key, raw?, source? }` context fields on each error subclass
|
||
|
||
## 3. Core validator (validate + maskSensitive)
|
||
|
||
- [x] 3.1 Implement `src/core/validator/validator.types.ts` — `ValidateInput`, `ValidationResult`
|
||
- [x] 3.2 Implement `src/core/validator/validator.ts` — `validate(input): ValidationResult` (runs all `field._rules`, accumulates failures); `maskSensitive(values, schema): Record<string, string>` (replaces sensitive values with `***`)
|
||
- [x] 3.3 Unit tests: all rules passing, partial failure accumulation, all rules failing, sensitive masking
|
||
- [x] 3.4 Property-based tests (fast-check): if N rules fail, `result.errors.length === N`; `maskSensitive` never leaks a sensitive raw value
|
||
- [x] 3.5 Co-located `src/core/validator/validator.md`
|
||
|
||
## 4. BaseField abstract class
|
||
|
||
- [x] 4.1 Implement `src/core/field/field.types.ts` — `FieldConfig<T>`, `RuleOptions`
|
||
- [x] 4.2 Implement `src/core/field/field.ts` — `BaseField<T>` abstract class with `_isRequired`, `_hasDefault`, `_defaultValue`, `_isSensitive`, `_description`, `_rules`; fluent builder methods: `optional()`, `default(v)`, `sensitive()`, `describe(s)`, `rules(opts)`; abstract `coerce(raw: string): T`
|
||
- [x] 4.3 Unit tests: builder method chaining, `FieldConfig` snapshot, abstract coerce enforcement
|
||
- [x] 4.4 Co-located `src/core/field/field.md`
|
||
|
||
## 5. Fields
|
||
|
||
- [x] 5.1 `src/fields/string-field/string-field.ts` — `StringField` extends `BaseField<string>`; rules: `minLength`, `maxLength`, `pattern`, `nonempty`; factory `str()`
|
||
- [x] 5.2 `src/fields/number-field/number-field.ts` — `NumberField` extends `BaseField<number>`; coerces via `coerceNumber`; rules: `min`, `max`, `integer`; factory `num()`
|
||
- [x] 5.3 `src/fields/boolean-field/boolean-field.ts` — `BooleanField` extends `BaseField<boolean>`; coerces via `coerceBoolean`; factory `bool()`
|
||
- [x] 5.4 `src/fields/enum-field/enum-field.types.ts` — `EnumValues`; `src/fields/enum-field/enum-field.ts` — `EnumField<V extends EnumValues>` extends `BaseField<V[number]>`; factory `enums(values)`
|
||
- [x] 5.5 `src/fields/url-field/url-field.ts` — `UrlField` extends `BaseField<string>`; validates via `new URL()`; optional `protocols` allowlist rule; factory `url()`
|
||
- [x] 5.6 `src/fields/email-field/email-field.ts` — `EmailField` extends `BaseField<string>`; validates against `EMAIL_PATTERN`; factory `email()`
|
||
- [x] 5.7 `src/fields/port-field/port-field.ts` — `PortField` extends `BaseField<number>`; validates integer in `[PORT_MIN, PORT_MAX]`; factory `port()`
|
||
- [x] 5.8 `src/fields/json-field/json-field.types.ts` — `JsonValidator<T>`; `src/fields/json-field/json-field.ts` — `JsonField<T>` extends `BaseField<T>`; coerces via `coerceJson`; accepts optional `JsonValidator<T>`; factory `json<T>(validator?)`
|
||
- [x] 5.9 `src/fields/array-field/array-field.types.ts` — `ItemField<T>`; `src/fields/array-field/array-field.ts` — `ArrayField<T>` extends `BaseField<T[]>`; splits on `separator` (default `DEFAULT_ARRAY_SEPARATOR`), maps each item through `itemField`; factory `array(itemField, separator?)`
|
||
- [x] 5.10 Unit tests for each field: valid raw, invalid raw → `CoercionError`, all custom rules, default / optional / sensitive combinations
|
||
- [x] 5.11 Co-located `.md` for each field file
|
||
|
||
## 6. Transforms (coerce utilities)
|
||
|
||
- [x] 6.1 Implement `src/transforms/coerce/coerce.types.ts` — `CoerceFn<T>`, `CoerceEntry<T>`
|
||
- [x] 6.2 Implement `src/transforms/coerce/coerce.ts` — `coerceString`, `coerceNumber`, `coerceInteger`, `coerceBoolean`, `coerceArray`, `coerceJson`; each throws `CoercionError` with `ENV.INVALID_FORMAT` on failure
|
||
- [x] 6.3 Unit tests: happy-path and all rejection branches for each coerce function
|
||
- [x] 6.4 Property-based tests (fast-check): `coerceNumber(raw)` round-trips for all finite float strings; `coerceBoolean` is total over TRUTHY_VALUES ∪ FALSY_VALUES
|
||
- [x] 6.5 Co-located `src/transforms/coerce/coerce.md`
|
||
|
||
## 7. Loaders
|
||
|
||
- [x] 7.1 `src/loaders/object/object.ts` — `objectLoader(obj, options?)` → `Loader`; `ObjectLoaderOptions`; source label defaults to `'object'`
|
||
- [x] 7.2 `src/loaders/dotenv/dotenv.ts` — `parseDotenv(content): Record<string, string>` (pure, throws `DotenvParseError` on malformed lines); `dotenvLoader(options?)` → `Loader` reads file from `options.path` (default `.env`), calls `parseDotenv`; `DotenvLoaderOptions`
|
||
- [x] 7.3 Unit tests for `objectLoader`: wraps object, respects source label
|
||
- [x] 7.4 Unit tests for `parseDotenv`: quoted values, comments, blank lines, `=` in value, malformed input
|
||
- [x] 7.5 Unit tests for `dotenvLoader`: reads real temp file, file-not-found returns empty gracefully (or throws — match main-branch behaviour), `override` flag
|
||
- [x] 7.6 Property-based tests (fast-check): `parseDotenv` is pure — same input always produces same output
|
||
- [x] 7.7 Co-located `.md` for each loader file
|
||
|
||
## 8. Utilities — interpolation + error factory
|
||
|
||
- [x] 8.1 `src/utils/interpolation.ts` — `interpolate(template, values)`, `hasInterpolation(value)`, `extractVariables(template)`; uses `INTERPOLATION_PATTERN`
|
||
- [x] 8.2 `src/utils/errors.ts` — `createEnvError(key, message, value?)` factory returning `EnvError` value object (not the class — the plain interface shape used by `CollectedErrors`)
|
||
- [x] 8.3 Unit tests for all interpolation functions and the error factory
|
||
- [x] 8.4 Co-located `.md` for each utility file
|
||
|
||
## 9. createEnv orchestrator
|
||
|
||
- [x] 9.1 Implement `src/core/create-env/create-env.types.ts` — `CollectedErrors`, `EnvResult<S>`
|
||
- [x] 9.2 Implement `src/core/create-env/create-env.ts` — `createEnv(schema, options?)`:
|
||
- Runs each `Loader` in `options.loaders` in order, merging `LoaderResult.values` (later loaders win unless `override: false`)
|
||
- Expands `${VAR}` interpolation tokens in raw values
|
||
- For each schema key: resolves raw string → calls `coerce` → runs `validate` → checks `isRequired`
|
||
- In `'throw'` mode: throws `EnvValidationError` or `MissingEnvError` on first failure
|
||
- In `'collect'` mode: accumulates all failures into `CollectedErrors`, returns partial result
|
||
- In `'warn'` mode: logs via `console.warn`, returns partial result
|
||
- Calls `maskSensitive` before returning
|
||
- Returns `EnvResult<S>` (typed record)
|
||
- [x] 9.3 Unit tests: full schema resolution, missing required, coercion error, collect mode accumulation, throw mode, warn mode, multi-loader merge order, interpolation expansion
|
||
- [x] 9.4 Co-located `src/core/create-env/create-env.md`
|
||
|
||
## 10. Presets
|
||
|
||
- [x] 10.1 `src/presets/common/common.ts` — `nodeEnvField`, `portField`, `hostField`, `logLevelField`, `debugField`, `appNameField`, `appVersionField`, `timezoneField`, `workersField`, `commonFields`
|
||
- [x] 10.2 `src/presets/server/server.ts` — `databaseUrlField`, `redisUrlField`, `sessionSecretField`, `corsOriginField`, `rateLimitMaxField`, `rateLimitWindowField`, `trustProxyField`, `smtpHostField`, `smtpPortField`, `smtpUserField`, `smtpPassField`, `smtpFromField`, `serverFields`
|
||
- [x] 10.3 Unit tests: each preset field resolves correctly from a matching env object; `commonFields` and `serverFields` composites contain all expected keys
|
||
- [x] 10.4 Co-located `.md` for each preset file
|
||
|
||
## 11. OTel instrumentation
|
||
|
||
- [x] 11.1 Add `@opentelemetry/api` to `dependencies` in `package.json`
|
||
- [x] 11.2 Wire counter `env.validation.total` (label `outcome: 'success' | 'error'`) in `createEnv` — incremented once per call
|
||
- [x] 11.3 Wire counter `env.validation.errors` (label `field`) in `createEnv` — incremented once per field that fails validation
|
||
- [x] 11.4 Wire histogram `env.load.duration_ms` — duration of the full `createEnv` call
|
||
- [x] 11.5 All meters obtained from `@opentelemetry/api` `getMeter`; no-op without exporter (verified in test with no SDK registered)
|
||
- [x] 11.6 Unit test: metrics instruments are created without error when no exporter is present
|
||
|
||
## 12. Property tests + benchmarks
|
||
|
||
- [x] 12.1 Property-based tests (fast-check) for field parsers round-trip (valid raw → coerce → stable value)
|
||
- [x] 12.2 Property test: `maskSensitive` never leaks raw values for sensitive fields
|
||
- [x] 12.3 Property test: `validate` accumulates all N failing rules
|
||
- [x] 12.4 Property test: `parseDotenv` is pure and idempotent
|
||
- [x] 12.5 `src/__benchmarks__/create-env.bench.ts` — `createEnv` with 20-field schema, `objectLoader`, no errors; throughput via mitata
|
||
- [x] 12.6 `src/__benchmarks__/validate.bench.ts` — `validate` hot path: single field with 5 passing rules; iterations via mitata
|
||
- [x] 12.7 Publish baseline numbers in README
|
||
|
||
## 13. Docs
|
||
|
||
- [x] 13.1 Co-located `.md` for every implementation file (≥200 words, simple + advanced examples, API reference, implementation notes)
|
||
- [x] 13.2 Rewrite `README.md` with public API table, loader usage, preset reference, error code table, OTel metric names, benchmark numbers, consumer quick-start
|
||
- [x] 13.3 `bun run docs:check --filter=@suaveplan/env` ≥ 90%
|
||
|
||
## 14. Verify and publish
|
||
|
||
- [x] 14.1 `bunx turbo lint typecheck test --filter=@suaveplan/env` green
|
||
- [x] 14.2 `bunx turbo test:coverage --filter=@suaveplan/env` at 100/100/100/100
|
||
- [x] 14.3 `bunx turbo build --filter=@suaveplan/env` green
|
||
- [x] 14.4 `bun scripts/publish.ts --package @suaveplan/env` to Verdaccio at 0.0.0
|
||
- [x] 14.5 Archive OpenSpec change to `openspec/archive/2026-04-19-enterprise-env/`
|