Files
suaveplan/openspec/archive/2026-04-19-enterprise-env/tasks.md
2026-05-27 23:29:53 +01:00

11 KiB
Raw Blame History

Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/config/env.

Tasks: enterprise-env

1. Scaffold

  • 1.1 Create packages/universal/env/ with package.json, tsconfig.json, tsconfig.lib.json, vite.config.ts, vitest.config.ts, README.md skeleton
  • 1.2 Register coverage thresholds at 100/100/100/100 in vitest.config.ts; provider = istanbul
  • 1.3 Add cross-cutting guard tests in tests/: dep-graph, no-disallowed, tree-shake
  • 1.4 Add package to root CLAUDE.md, README.md, and STRUCTURE.yml

2. Constants + types + errors

  • 2.1 Implement src/constants/index.ts — DEFAULT_ARRAY_SEPARATOR, EMAIL_PATTERN, FALSY_VALUES, INTERPOLATION_PATTERN, PORT_MAX, PORT_MIN, TRUTHY_VALUES
  • 2.2 Implement src/types/common.ts — AnyField, CreateEnvOptions, EnvError, EnvSchema, ErrorMode, InferEnv, InferFieldType, Loader, LoaderResult, OptionalKeys, RequiredKeys, Simplify, ValidationRule
  • 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
  • 2.4 Implement src/errors/error-codes.ts and auto-register via side-effect import in src/index.ts
  • 2.5 Zod schemas: FieldConfigSchema, ValidationResultSchema, CreateEnvOptionsSchema, DotenvLoaderOptionsSchema, ObjectLoaderOptionsSchema — exported from *.schemas.ts siblings
  • 2.6 Tests: error construction, code narrowing, { key, raw?, source? } context fields on each error subclass

3. Core validator (validate + maskSensitive)

  • 3.1 Implement src/core/validator/validator.types.ts — ValidateInput, ValidationResult
  • 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 ***)
  • 3.3 Unit tests: all rules passing, partial failure accumulation, all rules failing, sensitive masking
  • 3.4 Property-based tests (fast-check): if N rules fail, result.errors.length === N; maskSensitive never leaks a sensitive raw value
  • 3.5 Co-located src/core/validator/validator.md

4. BaseField abstract class

  • 4.1 Implement src/core/field/field.types.ts — FieldConfig<T>, RuleOptions
  • 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
  • 4.3 Unit tests: builder method chaining, FieldConfig snapshot, abstract coerce enforcement
  • 4.4 Co-located src/core/field/field.md

5. Fields

  • 5.1 src/fields/string-field/string-field.ts — StringField extends BaseField<string>; rules: minLength, maxLength, pattern, nonempty; factory str()
  • 5.2 src/fields/number-field/number-field.ts — NumberField extends BaseField<number>; coerces via coerceNumber; rules: min, max, integer; factory num()
  • 5.3 src/fields/boolean-field/boolean-field.ts — BooleanField extends BaseField<boolean>; coerces via coerceBoolean; factory bool()
  • 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)
  • 5.5 src/fields/url-field/url-field.ts — UrlField extends BaseField<string>; validates via new URL(); optional protocols allowlist rule; factory url()
  • 5.6 src/fields/email-field/email-field.ts — EmailField extends BaseField<string>; validates against EMAIL_PATTERN; factory email()
  • 5.7 src/fields/port-field/port-field.ts — PortField extends BaseField<number>; validates integer in [PORT_MIN, PORT_MAX]; factory port()
  • 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?)
  • 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?)
  • 5.10 Unit tests for each field: valid raw, invalid raw → CoercionError, all custom rules, default / optional / sensitive combinations
  • 5.11 Co-located .md for each field file

6. Transforms (coerce utilities)

  • 6.1 Implement src/transforms/coerce/coerce.types.ts — CoerceFn<T>, CoerceEntry<T>
  • 6.2 Implement src/transforms/coerce/coerce.ts — coerceString, coerceNumber, coerceInteger, coerceBoolean, coerceArray, coerceJson; each throws CoercionError with ENV.INVALID_FORMAT on failure
  • 6.3 Unit tests: happy-path and all rejection branches for each coerce function
  • 6.4 Property-based tests (fast-check): coerceNumber(raw) round-trips for all finite float strings; coerceBoolean is total over TRUTHY_VALUES ∪ FALSY_VALUES
  • 6.5 Co-located src/transforms/coerce/coerce.md

7. Loaders

  • 7.1 src/loaders/object/object.ts — objectLoader(obj, options?) → Loader; ObjectLoaderOptions; source label defaults to 'object'
  • 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
  • 7.3 Unit tests for objectLoader: wraps object, respects source label
  • 7.4 Unit tests for parseDotenv: quoted values, comments, blank lines, = in value, malformed input
  • 7.5 Unit tests for dotenvLoader: reads real temp file, file-not-found returns empty gracefully (or throws — match main-branch behaviour), override flag
  • 7.6 Property-based tests (fast-check): parseDotenv is pure — same input always produces same output
  • 7.7 Co-located .md for each loader file

8. Utilities — interpolation + error factory

  • 8.1 src/utils/interpolation.ts — interpolate(template, values), hasInterpolation(value), extractVariables(template); uses INTERPOLATION_PATTERN
  • 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)
  • 8.3 Unit tests for all interpolation functions and the error factory
  • 8.4 Co-located .md for each utility file

9. createEnv orchestrator

  • 9.1 Implement src/core/create-env/create-env.types.ts — CollectedErrors, EnvResult<S>
  • 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)
  • 9.3 Unit tests: full schema resolution, missing required, coercion error, collect mode accumulation, throw mode, warn mode, multi-loader merge order, interpolation expansion
  • 9.4 Co-located src/core/create-env/create-env.md

10. Presets

  • 10.1 src/presets/common/common.ts — nodeEnvField, portField, hostField, logLevelField, debugField, appNameField, appVersionField, timezoneField, workersField, commonFields
  • 10.2 src/presets/server/server.ts — databaseUrlField, redisUrlField, sessionSecretField, corsOriginField, rateLimitMaxField, rateLimitWindowField, trustProxyField, smtpHostField, smtpPortField, smtpUserField, smtpPassField, smtpFromField, serverFields
  • 10.3 Unit tests: each preset field resolves correctly from a matching env object; commonFields and serverFields composites contain all expected keys
  • 10.4 Co-located .md for each preset file

11. OTel instrumentation

  • 11.1 Add @opentelemetry/api to dependencies in package.json
  • 11.2 Wire counter env.validation.total (label outcome: 'success' | 'error') in createEnv — incremented once per call
  • 11.3 Wire counter env.validation.errors (label field) in createEnv — incremented once per field that fails validation
  • 11.4 Wire histogram env.load.duration_ms — duration of the full createEnv call
  • 11.5 All meters obtained from @opentelemetry/api getMeter; no-op without exporter (verified in test with no SDK registered)
  • 11.6 Unit test: metrics instruments are created without error when no exporter is present

12. Property tests + benchmarks

  • 12.1 Property-based tests (fast-check) for field parsers round-trip (valid raw → coerce → stable value)
  • 12.2 Property test: maskSensitive never leaks raw values for sensitive fields
  • 12.3 Property test: validate accumulates all N failing rules
  • 12.4 Property test: parseDotenv is pure and idempotent
  • 12.5 src/__benchmarks__/create-env.bench.ts — createEnv with 20-field schema, objectLoader, no errors; throughput via mitata
  • 12.6 src/__benchmarks__/validate.bench.ts — validate hot path: single field with 5 passing rules; iterations via mitata
  • 12.7 Publish baseline numbers in README

13. Docs

  • 13.1 Co-located .md for every implementation file (≥200 words, simple + advanced examples, API reference, implementation notes)
  • 13.2 Rewrite README.md with public API table, loader usage, preset reference, error code table, OTel metric names, benchmark numbers, consumer quick-start
  • 13.3 bun run docs:check --filter=@suaveplan/env ≥ 90%

14. Verify and publish

  • 14.1 bunx turbo lint typecheck test --filter=@suaveplan/env green
  • 14.2 bunx turbo test:coverage --filter=@suaveplan/env at 100/100/100/100
  • 14.3 bunx turbo build --filter=@suaveplan/env green
  • 14.4 bun scripts/publish.ts --package @suaveplan/env to Verdaccio at 0.0.0
  • 14.5 Archive OpenSpec change to openspec/archive/2026-04-19-enterprise-env/