Initial Commit
This commit is contained in:
180
openspec/specs/config/spec.md
Normal file
180
openspec/specs/config/spec.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# config
|
||||
|
||||
**Category**: universal
|
||||
**Path**: `universal/config/config`
|
||||
**Archived from**: `openspec/archive/2026-04-24-enterprise-config/`
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Layered configuration management for the Suaveplan monorepo. Five 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` with hot-reload and subscription support.
|
||||
|
||||
## Requirements
|
||||
|
||||
## Constants
|
||||
|
||||
```ts
|
||||
LAYER_PRIORITIES: { DEFAULTS: 0; FILE: 10; OBJECT: 20; ENV: 30; CLI: 40 }
|
||||
MASK_STRING: "****"
|
||||
TRUTHY_STRINGS: string[]
|
||||
FALSY_STRINGS: string[]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Code | Severity | Retryable | Trigger |
|
||||
|---|---|---|---|
|
||||
| `CONFIG_MISSING` | `error` | `false` | Required config key not present |
|
||||
| `CONFIG_INVALID_TYPE` | `error` | `false` | Value has wrong type |
|
||||
| `CONFIG_PARSE_FAILED` | `error` | `false` | Layer resolution threw |
|
||||
| `CONFIG_FILE_NOT_FOUND` | `error` | `false` | File layer content unavailable |
|
||||
| `CONFIG_ENV_NOT_SET` | `error` | `false` | Required env var absent |
|
||||
| `CONFIG_RELOAD_FAILED` | `error` | `true` | Reload aborted or layer re-resolution threw |
|
||||
| `CONFIG_VALIDATION_FAILED` | `fatal` | `false` | Validator threw |
|
||||
|
||||
---
|
||||
|
||||
## Core API
|
||||
|
||||
```ts
|
||||
class ConfigError extends SuaveplanError<ConfigPackageErrorCode>
|
||||
|
||||
function createConfig<T extends ConfigData>(options: CreateConfigOptions<T>): ConfigStore<T>
|
||||
|
||||
interface ConfigErrorDetail { path?: string; message: string }
|
||||
interface MergeResult { data: ConfigData; layers: ResolvedLayer[] }
|
||||
interface ResolvedLayer { name: string; priority: number; data: ConfigData }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Store
|
||||
|
||||
```ts
|
||||
function createConfigStore<T extends ConfigData>(options: ConfigStoreOptions<T>): ConfigStore<T>
|
||||
|
||||
interface ConfigStoreOptions<T extends ConfigData = ConfigData> {
|
||||
data: T
|
||||
sensitive: string[]
|
||||
}
|
||||
|
||||
interface ConfigStore<T extends ConfigData = ConfigData> {
|
||||
get<K extends string>(key: K): unknown
|
||||
has(key: string): boolean
|
||||
toJSON(): Readonly<T>
|
||||
toMasked(): Readonly<T>
|
||||
reload(signal?: AbortSignal): Promise<void>
|
||||
subscribe(listener: (next: Readonly<T>) => void): () => void
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layers
|
||||
|
||||
```ts
|
||||
function defaultsLayer<T extends ConfigData>(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
|
||||
function parseArgs(args: string[], mapping: Record<string, string>): Record<string, string>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Types
|
||||
|
||||
```ts
|
||||
type ConfigData = Record<string, unknown>
|
||||
|
||||
interface ConfigLayer {
|
||||
readonly name: string
|
||||
readonly priority: number
|
||||
resolve(): ConfigData
|
||||
teardown?(): void
|
||||
}
|
||||
|
||||
type ConfigValidator<T extends ConfigData = ConfigData> = (data: ConfigData) => T
|
||||
|
||||
type EnvironmentOverrides<T extends ConfigData = ConfigData> = Record<string, Partial<T>>
|
||||
|
||||
interface CreateConfigOptions<T extends ConfigData = ConfigData> {
|
||||
layers: ConfigLayer[]
|
||||
validate?: ConfigValidator<T>
|
||||
sensitive?: string[]
|
||||
environments?: EnvironmentOverrides<T>
|
||||
environment?: string
|
||||
logger?: LoggerLike
|
||||
}
|
||||
|
||||
interface EnvLayerOptions {
|
||||
prefix?: string
|
||||
mapping: Record<string, string>
|
||||
source?: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
interface CliLayerOptions {
|
||||
args: string[]
|
||||
mapping: Record<string, string>
|
||||
}
|
||||
|
||||
interface FileLayerOptions {
|
||||
content: ConfigData
|
||||
watchBus?: EventBusLike
|
||||
watchEvent?: string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Utils
|
||||
|
||||
```ts
|
||||
function deepMerge<T extends ConfigData>(base: T, override: Partial<T>): 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zod Schemas
|
||||
|
||||
- `EnvLayerOptionsSchema`
|
||||
- `CliLayerOptionsSchema`
|
||||
- `FileLayerOptionsSchema`
|
||||
- `ConfigStoreOptionsSchema`
|
||||
- `CreateConfigOptionsSchema`
|
||||
|
||||
---
|
||||
|
||||
## OTel Instrumentation
|
||||
|
||||
| Signal | Name | Attributes |
|
||||
|---|---|---|
|
||||
| Span | `config.create` | `config.layer_count`, `config.environment` |
|
||||
| Counter | `config.get_total` | `config.hit` (bool) |
|
||||
| Counter | `config.reload_total` | — |
|
||||
| Histogram | `config.reload_duration_ms` | — |
|
||||
| Histogram | `config.merge_duration_ms` | `config.layer_count` |
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` | `SuaveplanError` base class, `defineCodes` |
|
||||
| `@suaveplan/types` | `LoggerLike`, `EventBusLike` |
|
||||
| `@suaveplan/events` | `EventBus` for file-layer watch mode |
|
||||
| `@suaveplan/env` | `coerceValue` boolean/numeric token reference |
|
||||
| `@opentelemetry/api` | OTel spans, counters, histograms |
|
||||
| `zod` | Zod schemas for all public option types |
|
||||
| `tslib` | TS runtime helpers |
|
||||
Reference in New Issue
Block a user