# OpenSpec Change: enterprise-config - **Change ID**: enterprise-config - **Package**: `@suaveplan/config` - **Category**: universal - **Target path**: `universal/config/config` - **Status**: IN PROGRESS - **Date**: 2026-04-23 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/config.md` --- ## Summary Deliver `@suaveplan/config` on the genesis branch from scratch at enterprise quality. The package provides layered configuration management: a set of typed layers (defaults, file, object, env, CLI) are resolved in priority order, deep-merged, optionally validated via a caller-supplied Zod-compatible schema, and exposed through an immutable `ConfigStore`. Genesis adds structured errors via `@suaveplan/error`, Zod option schemas, OTel instrumentation, `AbortSignal` on async reload, event-bus change propagation via `@suaveplan/events`, delegation of env-var coercion to `@suaveplan/env`, property-based tests (fast-check), and a mitata benchmark suite. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Base class for `ConfigError`; `defineCodes` / `ErrorCodes.registerMany` for `CONFIG.*` codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `EventBusLike`, `Clock` (injectable clock for future reload scheduling) | | `@suaveplan/events` (`workspace:^`) | `EventBus` for file-layer watch-mode change notifications | | `@suaveplan/env` (`workspace:^`) | `createEnv` / field descriptors — used inside `envLayer` for enterprise-grade coercion and prefix stripping | | `@suaveplan/feature-flags` (`workspace:^`) | `FlagManager` — `ConfigStore` can optionally accept a flag manager to gate experimental read paths | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | Spans + counters on `createConfig`, `store.get`, `store.set`, `store.reload` | | `tslib` (`catalog:build`) | Runtime helpers for TS decorators (if needed) | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`). --- ## Preserved public API surface (from main branch) All symbols below are carried forward verbatim; signatures may gain optional genesis-only fields but the existing call sites remain valid without changes. ### Constants ```ts LAYER_PRIORITIES: { DEFAULTS: 0; FILE: 10; OBJECT: 20; ENV: 30; CLI: 40 } MASK_STRING: "****" ``` ### Core ```ts // Re-exported from @suaveplan/error class ConfigError extends SuaveplanError // Orchestrator — resolves layers, merges, applies env overrides, validates function createConfig(options: CreateConfigOptions): ConfigStore // Internal types surfaced for downstream consumers interface ConfigErrorDetail { path?: string; message: string } interface MergeResult { data: ConfigData; layers: ResolvedLayer[] } interface ResolvedLayer { name: string; priority: number; data: ConfigData } ``` ### Store ```ts function createConfigStore(options: ConfigStoreOptions): ConfigStore interface ConfigStoreOptions { data: T sensitive: string[] } ``` ### Layers ```ts function defaultsLayer(defaults: T): ConfigLayer function objectLayer(data: ConfigData, priority?: number): ConfigLayer function envLayer(options: EnvLayerOptions): ConfigLayer function fileLayer(options: FileLayerOptions): ConfigLayer function cliLayer(options: CliLayerOptions): ConfigLayer // wraps parseArgs internally function parseArgs(args: string[], mapping: Record): Record ``` ### Types ```ts type ConfigData = Record interface ConfigLayer { readonly name: string readonly priority: number resolve(): ConfigData } type ConfigValidator = (data: ConfigData) => T type EnvironmentOverrides = Record> interface CreateConfigOptions { layers: ConfigLayer[] validate?: ConfigValidator sensitive?: string[] environments?: EnvironmentOverrides environment?: string } interface ConfigStore { get(key: K): unknown has(key: string): boolean toJSON(): Readonly toMasked(): Readonly } interface EnvLayerOptions { prefix?: string mapping: Record source?: Record } interface CliLayerOptions { args: string[] mapping: Record } interface FileLayerOptions { content: ConfigData } ``` ### Utils ```ts function deepMerge(base: T, override: Partial): T function mergeLayers(layers: Array<{ name: string; data: ConfigData }>): ConfigData function getByPath(obj: ConfigData, path: string): unknown function hasByPath(obj: ConfigData, path: string): boolean function setByPath(obj: ConfigData, path: string, value: unknown): ConfigData function maskSensitive(data: ConfigData, sensitive: string[], mask?: string): ConfigData function coerceValue(raw: string): unknown ``` ### Error codes ```ts type ConfigPackageErrorCode = | "CONFIG_MISSING" | "CONFIG_INVALID_TYPE" | "CONFIG_PARSE_FAILED" | "CONFIG_FILE_NOT_FOUND" | "CONFIG_ENV_NOT_SET" const CONFIG_ERROR_METADATA: Readonly> function registerConfigErrorCodes(): void ``` --- ## New additions for genesis ### 1. Extended error codes Two additional codes beyond the main-branch set: | Code | Severity | Retryable | Trigger | |---|---|---|---| | `CONFIG_RELOAD_FAILED` | `error` | `true` | Reload aborted or layer re-resolution threw | | `CONFIG_VALIDATION_FAILED` | `fatal` | `false` | Validator threw (previously mapped to `CONFIG_INVALID` which does not exist in the registry) | ### 2. Zod option schemas Zod schemas (not just TS types) for every public options object so callers can validate config at the boundary: - `CreateConfigOptionsSchema` - `ConfigStoreOptionsSchema` - `EnvLayerOptionsSchema` - `CliLayerOptionsSchema` - `FileLayerOptionsSchema` ### 3. Structured `ConfigError` `ConfigError` extends `SuaveplanError` from `@suaveplan/error` (not a local sub-class), carrying a `ConfigPackageErrorCode` as its `.code` discriminant. All internal throw sites use named factory helpers so codes are never stringly-typed at the call site. ### 4. `envLayer` delegates to `@suaveplan/env` Genesis `envLayer` wraps `@suaveplan/env`'s `createEnv` internally for prefix stripping and type coercion instead of duplicating the logic. The public `EnvLayerOptions` interface remains stable; `coerceValue` is still exported from the package as a utility (it delegates to `@suaveplan/env`'s internals). ### 5. `ConfigStore` enhancements The `ConfigStore` interface gains two optional genesis additions (both present on the concrete returned object but typed as optional on the interface to remain backward-compatible): ```ts interface ConfigStore { // --- existing --- get(key: K): unknown has(key: string): boolean toJSON(): Readonly toMasked(): Readonly // --- genesis additions --- reload(signal?: AbortSignal): Promise // re-resolves all layers; no-op on static stores subscribe(listener: (next: Readonly) => void): () => void // returns unsubscribe fn } ``` `createConfigStore` is kept synchronous and static; `reload` / `subscribe` are only wired when `createConfig` constructs the store with a live layer set. ### 6. `fileLayer` watch mode `fileLayer` accepts an optional `EventBus` from `@suaveplan/events`. When provided and the layer is attached to a reloadable `ConfigStore`, a file-change signal from the bus triggers a reload cycle. This is opt-in; the sync path is unchanged. ```ts interface FileLayerOptions { content: ConfigData // genesis addition — optional watch support watchBus?: EventBusLike watchEvent?: string // event name to listen for; default: 'config:file:changed' } ``` ### 7. `LoggerLike` injection into `createConfig` `CreateConfigOptions` accepts an optional `logger?: LoggerLike` (from `@suaveplan/types`). When present, layer resolution counts, merge durations, and validation outcomes are written as structured log entries (not OTel — OTel is additive). ```ts interface CreateConfigOptions { // ... existing fields ... logger?: LoggerLike // genesis addition } ``` ### 8. OTel instrumentation - `createConfig` — span `config.create`, attributes: `config.layer_count`, `config.environment` - `store.get` — counter `config.get_total`, attribute `config.hit` (bool) - `store.reload` — span `config.reload`, counter `config.reload_total` - `mergeLayers` — histogram `config.merge_duration_ms` ### 9. Property-based tests (fast-check) - `deepMerge` associativity: `merge(merge(a, b), c) ≡ merge(a, merge(b, c))` for plain-object inputs - Layer priority ordering: result of `mergeLayers` with N random layers equals result when layers are sorted by priority then merged left-to-right - `setByPath` / `getByPath` round-trip: `getByPath(setByPath({}, path, v), path) === v` for any valid dot-path - `maskSensitive` idempotency: applying mask twice yields the same output as applying once - `coerceValue` stability: calling `coerceValue` on the string representation of a non-string output returns the same value ### 10. Benchmark suite (`src/__benchmarks__/`) - `createConfig` with 5 layers (cold path) - `store.get` (hot path, deeply nested key) - `mergeLayers` with 2, 5, 10, 20 layers --- ## File layout ```text universal/config/config/ package.json tsconfig.json vitest.config.ts src/ index.ts # Public barrel error-codes.ts # CONFIG.* codes + registerConfigErrorCodes constants/ index.ts # LAYER_PRIORITIES, MASK_STRING, TRUTHY_STRINGS, FALSY_STRINGS types/ common.ts # All public TS interfaces and type aliases common.md core/ config-builder/ config-builder.ts # createConfig orchestrator config-builder.md config-builder.types.ts # ResolvedLayer, MergeResult, ConfigErrorDetail config-builder.test.ts index.ts config-store/ config-store.ts # createConfigStore config-store.md config-store.types.ts # ConfigStoreOptions config-store.test.ts index.ts layers/ defaults-layer/ defaults-layer.ts defaults-layer.md defaults-layer.test.ts index.ts object-layer/ object-layer.ts object-layer.md object-layer.test.ts index.ts env-layer/ env-layer.ts # envLayer + coerceValue (delegates to @suaveplan/env) env-layer.md env-layer.test.ts index.ts file-layer/ file-layer.ts # fileLayer + optional watchBus support file-layer.md file-layer.test.ts index.ts cli-layer/ cli-layer.ts # cliLayer + parseArgs cli-layer.md cli-layer.test.ts index.ts utils/ merge.ts # deepMerge, mergeLayers merge.md merge.test.ts mask.ts # getByPath, hasByPath, setByPath, maskSensitive mask.md mask.test.ts __benchmarks__/ config.bench.ts tests/ integration.test.ts # cross-cutting: full createConfig → store round-trip property.test.ts # fast-check property tests ``` --- ## Impact **Downstream packages that will consume `@suaveplan/config`**: any server-side or CLI package that needs structured config (e.g. future `@suaveplan/server`, API plugins, CLI tools built on `@suaveplan/cli`). No existing genesis package currently depends on it — this is a forward dependency. **Breaking changes from main-branch API**: none. All main-branch call sites compile unchanged. New optional fields (`logger`, `watchBus`, `watchEvent`, `reload`, `subscribe`) are additive. **Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/env`, `@suaveplan/feature-flags` must all be published to Verdaccio before this package can be built or tested.