@kb-labs/core-config 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,338 @@
1
+ # @kb-labs/core-config
2
+
3
+ > **Core configuration system for KB Labs products with 6-layer merging, YAML/JSON support, and comprehensive tracing.** Provides deterministic configuration resolution with detailed merge traces for debugging.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-18.18.0+-green.svg)](https://nodejs.org/)
7
+ [![pnpm](https://img.shields.io/badge/pnpm-9.0.0+-orange.svg)](https://pnpm.io/)
8
+
9
+ ## 🎯 Vision & Purpose
10
+
11
+ **@kb-labs/core-config** is the foundation of KB Labs configuration system. It implements a sophisticated 6-layer configuration merging strategy that allows products to have defaults, profile overrides, preset configurations, workspace settings, local overrides, and CLI arguments - all merged deterministically with full traceability.
12
+
13
+ ## 🏗️ Architecture
14
+
15
+ ### Core Components
16
+
17
+ #### `getProductConfig()` - Main Configuration Resolver
18
+
19
+ - **Purpose**: Resolves product configuration by merging 6 layers
20
+ - **Responsibilities**:
21
+ - Find workspace root
22
+ - Load workspace config
23
+ - Resolve profile defaults
24
+ - Load preset defaults
25
+ - Load local config
26
+ - Merge with CLI overrides
27
+ - Return merged config with trace
28
+ - **Dependencies**: `core-types`, `yaml`, `zod`, `ajv`
29
+ - **Exports**: `ProductConfigResult<T>` with config and trace
30
+
31
+ #### `layeredMergeWithTrace()` - Merge Engine
32
+
33
+ - **Purpose**: Merges configuration layers with detailed tracing
34
+ - **Responsibilities**:
35
+ - Deep merge objects
36
+ - Track merge operations
37
+ - Generate merge trace
38
+ - **Dependencies**: None (pure function)
39
+ - **Exports**: `LayeredMergeResult` with merged config and trace
40
+
41
+ #### Profile Resolution System
42
+
43
+ - **Purpose**: Resolves Profiles v2 with scope selection
44
+ - **Responsibilities**:
45
+ - Load profile manifests
46
+ - Resolve profile extends
47
+ - Select scopes
48
+ - Extract profile defaults
49
+ - **Dependencies**: `zod` for validation
50
+ - **Exports**: `resolveProfile()`, `selectProfileScope()`
51
+
52
+ ### Design Patterns
53
+
54
+ - **Layered Architecture**: 6-layer configuration system
55
+ - **Strategy Pattern**: Different merge strategies for different layer types
56
+ - **Cache Pattern**: LRU cache for file system reads
57
+ - **Builder Pattern**: Configuration built layer by layer
58
+
59
+ ### Data Flow
60
+
61
+ ```
62
+ getProductConfig({ product, ... })
63
+
64
+ ├──► findNearestConfig() → workspace root
65
+ ├──► readWorkspaceConfig() → workspace config
66
+ ├──► resolveProfile() → profile defaults
67
+ ├──► resolvePreset() → preset defaults
68
+ ├──► readLocalConfig() → local config
69
+ ├──► layeredMergeWithTrace() → merged config + trace
70
+ └──► return { config, trace }
71
+ ```
72
+
73
+ ### State Management
74
+
75
+ - **State Type**: Local (per function call) + cached (LRU)
76
+ - **State Storage**: Memory (LRU cache for file reads)
77
+ - **State Lifecycle**: Created per call, cached for performance
78
+ - **State Persistence**: No persistence (stateless API)
79
+
80
+ ### Concurrency Model
81
+
82
+ - **Single-threaded**: All operations are async but single-threaded
83
+ - **Thread Safety**: N/A (Node.js single-threaded event loop)
84
+ - **Race Conditions**: None (stateless API)
85
+ - **Deadlocks**: None
86
+
87
+ ### Error Handling Strategy
88
+
89
+ - **Error Types**: `KbError` with error codes and hints
90
+ - **Error Propagation**: Errors thrown and caught by caller
91
+ - **Error Recovery**: No automatic recovery (caller must handle)
92
+ - **Error Logging**: Diagnostics array for non-fatal issues
93
+
94
+ ## 🚀 Quick Start
95
+
96
+ ### Installation
97
+
98
+ ```bash
99
+ pnpm add @kb-labs/core-config
100
+ ```
101
+
102
+ ### Basic Usage
103
+
104
+ ```typescript
105
+ import { getProductConfig } from '@kb-labs/core-config';
106
+
107
+ const result = await getProductConfig({
108
+ cwd: process.cwd(),
109
+ product: 'aiReview',
110
+ cli: { debug: true }
111
+ }, schema);
112
+
113
+ console.log(result.config); // Merged configuration
114
+ console.log(result.trace); // Detailed merge trace
115
+ ```
116
+
117
+ ## ✨ Features
118
+
119
+ - **YAML/JSON Support**: Automatic format detection and parsing
120
+ - **Find-up Resolution**: Walks up directory tree to find config files
121
+ - **Layered Merge**: Deterministic configuration merging with detailed trace
122
+ - **Product Normalization**: Consistent kebab-case (FS) ↔ camelCase (code) mapping
123
+ - **LRU Caching**: Filesystem read caching with invalidation
124
+ - **Error Handling**: Standardized errors with helpful hints
125
+ - **Schema Versioning**: All configs include `schemaVersion: "1.0"`
126
+
127
+ ## API
128
+
129
+ ### Core Functions
130
+
131
+ ```typescript
132
+ import {
133
+ getProductConfig,
134
+ explainProductConfig,
135
+ readConfigFile,
136
+ findNearestConfig,
137
+ clearCaches
138
+ } from '@kb-labs/core-config';
139
+
140
+ // Get product configuration with trace
141
+ const result = await getProductConfig({
142
+ cwd: '/path/to/project',
143
+ product: 'aiReview',
144
+ cli: { debug: true }
145
+ }, schema);
146
+
147
+ console.log(result.config); // Merged configuration
148
+ console.log(result.trace); // Detailed merge trace
149
+
150
+ // Explain configuration without resolving
151
+ const explanation = await explainProductConfig({
152
+ cwd: '/path/to/project',
153
+ product: 'aiReview'
154
+ }, schema);
155
+
156
+ // Read config file with format detection
157
+ const config = await readConfigFile('kb-labs.config.yaml');
158
+
159
+ // Find nearest config file
160
+ const { path, tried } = await findNearestConfig('/path/to/project');
161
+
162
+ // Clear caches (useful for tests)
163
+ clearCaches();
164
+ ```
165
+
166
+ ### Product Normalization
167
+
168
+ ```typescript
169
+ import { toFsProduct, toConfigProduct, ProductId } from '@kb-labs/core-config';
170
+
171
+ // Convert to filesystem format
172
+ toFsProduct('aiReview'); // 'ai-review'
173
+ toFsProduct('aiDocs'); // 'ai-docs'
174
+
175
+ // Convert to code format
176
+ toConfigProduct('ai-review'); // 'aiReview'
177
+ toConfigProduct('ai-docs'); // 'aiDocs'
178
+ ```
179
+
180
+ ### Error Handling
181
+
182
+ ```typescript
183
+ import { KbError, getExitCode } from '@kb-labs/core-config';
184
+
185
+ try {
186
+ const config = await getProductConfig(opts, schema);
187
+ } catch (error) {
188
+ if (error instanceof KbError) {
189
+ console.error(error.message);
190
+ console.error('Hint:', error.hint);
191
+ process.exit(getExitCode(error));
192
+ }
193
+ }
194
+ ```
195
+
196
+ ## Configuration Layers
197
+
198
+ The system merges configuration in this order (later layers override earlier ones):
199
+
200
+ 1. **Runtime defaults** - Built-in defaults for each product
201
+ 2. **Profile defaults** - From profile manifest defaults
202
+ 3. **Preset defaults** - From org preset package
203
+ 4. **Workspace config** - From `kb-labs.config.(json|yaml)`
204
+ 5. **Local config** - From `.kb/<product>/<product>.config.json`
205
+ 6. **CLI overrides** - From command line arguments
206
+
207
+ ## Configuration Files
208
+
209
+ ### Workspace Config (`kb-labs.config.yaml`)
210
+
211
+ ```yaml
212
+ schemaVersion: "1.0"
213
+ profiles:
214
+ default: "node-ts-lib@1.2.0"
215
+ backend: "node-ts-lib@1.2.0"
216
+ products:
217
+ ai-review:
218
+ enabled: true
219
+ rules: ["security", "performance"]
220
+ devlink:
221
+ watch: true
222
+ build: true
223
+ ```
224
+
225
+ ### Local Config (`.kb/ai-review/ai-review.config.json`)
226
+
227
+ ```json
228
+ {
229
+ "$schema": "https://schemas.kb-labs.dev/config.schema.json",
230
+ "schemaVersion": "1.0",
231
+ "enabled": true,
232
+ "rules": ["custom-rule"],
233
+ "settings": {
234
+ "debug": true
235
+ }
236
+ }
237
+ ```
238
+
239
+ ## Error Codes
240
+
241
+ | Code | Exit Code | Description |
242
+ |------|-----------|-------------|
243
+ | `ERR_CONFIG_NOT_FOUND` | 2 | Config file not found |
244
+ | `ERR_CONFIG_INVALID` | 1 | Invalid config syntax |
245
+ | `ERR_PRESET_NOT_RESOLVED` | 1 | Preset package not found |
246
+ | `ERR_PROFILE_INCOMPATIBLE` | 1 | Profile version incompatible |
247
+
248
+ ## Caching
249
+
250
+ The system uses LRU caching for filesystem reads:
251
+
252
+ - **Key**: `absPath|mtime|size`
253
+ - **Max Size**: 100 entries
254
+ - **Invalidation**: Automatic on file change
255
+ - **Clear**: Use `clearCaches()` for tests
256
+
257
+ ## Examples
258
+
259
+ ### Basic Usage
260
+
261
+ ```typescript
262
+ import { getProductConfig } from '@kb-labs/core-config';
263
+
264
+ const config = await getProductConfig({
265
+ cwd: process.cwd(),
266
+ product: 'aiReview',
267
+ cli: { debug: true }
268
+ }, aiReviewSchema);
269
+
270
+ console.log('AI Review config:', config.config);
271
+ console.log('Merge trace:', config.trace);
272
+ ```
273
+
274
+ ### CLI Integration
275
+
276
+ ```typescript
277
+ import { getProductConfig, KbError, getExitCode } from '@kb-labs/core-config';
278
+
279
+ try {
280
+ const result = await getProductConfig({
281
+ cwd: process.cwd(),
282
+ product: 'aiReview',
283
+ cli: cliArgs
284
+ }, schema);
285
+
286
+ console.log(JSON.stringify(result.config, null, 2));
287
+ } catch (error) {
288
+ if (error instanceof KbError) {
289
+ console.error(`Error: ${error.message}`);
290
+ if (error.hint) {
291
+ console.error(`Hint: ${error.hint}`);
292
+ }
293
+ process.exit(getExitCode(error));
294
+ }
295
+ throw error;
296
+ }
297
+ ```
298
+
299
+ ### Testing
300
+
301
+ ```typescript
302
+ import { clearCaches } from '@kb-labs/core-config';
303
+
304
+ beforeEach(() => {
305
+ clearCaches(); // Clear caches between tests
306
+ });
307
+ ```
308
+
309
+ ## 🔧 Configuration
310
+
311
+ ### Configuration Options
312
+
313
+ The 6-layer configuration system:
314
+
315
+ 1. **Runtime defaults** - Built-in defaults for each product
316
+ 2. **Profile defaults** - From profile manifest defaults
317
+ 3. **Preset defaults** - From org preset package (`$extends`)
318
+ 4. **Workspace config** - From `kb-labs.config.(json|yaml)`
319
+ 5. **Local config** - From `.kb/<product>/<product>.config.json`
320
+ 6. **CLI overrides** - From command line arguments
321
+
322
+ ### Environment Variables
323
+
324
+ - None (all configuration via files and options)
325
+
326
+ ### Default Values
327
+
328
+ - **Cache Size**: 100 entries (LRU)
329
+ - **Config Format**: Auto-detected (YAML/JSON)
330
+ - **Schema Version**: "1.0"
331
+
332
+ ## 🤝 Contributing
333
+
334
+ See [CONTRIBUTING.md](../../CONTRIBUTING.md) for development guidelines.
335
+
336
+ ## 📄 License
337
+
338
+ KB Public License v1.1 © KB Labs
@@ -0,0 +1,80 @@
1
+ import { promises } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+
4
+ // src/utils/fs-atomic.ts
5
+
6
+ // src/errors/kb-error.ts
7
+ var KbError = class extends Error {
8
+ constructor(code, message, hint, meta) {
9
+ super(message);
10
+ this.code = code;
11
+ this.hint = hint;
12
+ this.meta = meta;
13
+ this.name = "KbError";
14
+ }
15
+ };
16
+ function getExitCode(err) {
17
+ if (err.code === "ERR_FORBIDDEN") {
18
+ return 3;
19
+ }
20
+ if (err.code === "ERR_CONFIG_NOT_FOUND") {
21
+ return 2;
22
+ }
23
+ if (err.code === "ERR_CONFIG_EXISTS_CONFLICT") {
24
+ return 2;
25
+ }
26
+ if (err.code === "ERR_PATH_OUTSIDE_WORKSPACE") {
27
+ return 2;
28
+ }
29
+ if (err.code.startsWith("ERR_")) {
30
+ return 1;
31
+ }
32
+ return 1;
33
+ }
34
+ var ERROR_HINTS = {
35
+ ERR_CONFIG_NOT_FOUND: "Create .kb/kb-labs.config.yaml or run: kb init",
36
+ ERR_CONFIG_INVALID: "Check configuration syntax and required fields",
37
+ ERR_CONFIG_EXISTS_CONFLICT: "Use --force to overwrite existing configuration",
38
+ ERR_PATH_OUTSIDE_WORKSPACE: "Check cwd or use a relative path within the workspace",
39
+ ERR_PRESET_NOT_RESOLVED: "Install the required preset package or check network connectivity",
40
+ ERR_PROFILE_INCOMPATIBLE: "Profile version is incompatible with current system",
41
+ ERR_PROFILE_NOT_DEFINED: "Add profiles[] to kb.config.json or pass --profile=<id>",
42
+ ERR_PROFILE_RESOLVE_FAILED: "Profile registry unavailable. Define profiles[] inside kb.config.json or use workspace presets.",
43
+ ERR_PROFILE_EXTENDS_FAILED: "Profile extends failed. Check extends reference or install required preset/package.",
44
+ ERR_PROFILE_SCOPE_NOT_FOUND: "Scope not found. Use --scope=<id> with one of the configured scopes.",
45
+ ERR_PROFILE_SCOPE_CONFLICT: "Multiple scopes matched. Specify --scope=<id> explicitly.",
46
+ ERR_PROFILE_INVALID_FORMAT: "Update profiles[] in kb.config.json (Profiles v2) or rerun your plugin setup command.",
47
+ ERR_ARTIFACT_LIMIT_EXCEEDED: "Remove or split large artifacts, or reduce artifact count",
48
+ ERR_FORBIDDEN: "Add role in policy.overrides or use preset..."
49
+ };
50
+ async function writeFileAtomic(path, data) {
51
+ const tmp = `${path}.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
52
+ try {
53
+ await promises.mkdir(dirname(path), { recursive: true });
54
+ await promises.mkdir(dirname(tmp), { recursive: true });
55
+ await promises.writeFile(tmp, data);
56
+ await promises.rename(tmp, path);
57
+ } catch (error) {
58
+ try {
59
+ await promises.unlink(tmp);
60
+ } catch {
61
+ }
62
+ throw error;
63
+ }
64
+ }
65
+ function ensureWithinWorkspace(targetPath, workspaceRoot) {
66
+ const absTarget = resolve(targetPath);
67
+ const absRoot = resolve(workspaceRoot);
68
+ if (!absTarget.startsWith(absRoot)) {
69
+ throw new KbError(
70
+ "ERR_PATH_OUTSIDE_WORKSPACE",
71
+ `Refusing to write outside workspace: ${targetPath}`,
72
+ "Check cwd or use a relative path within the workspace",
73
+ { targetPath, workspaceRoot }
74
+ );
75
+ }
76
+ }
77
+
78
+ export { ERROR_HINTS, KbError, ensureWithinWorkspace, getExitCode, writeFileAtomic };
79
+ //# sourceMappingURL=chunk-A2WE2JZW.js.map
80
+ //# sourceMappingURL=chunk-A2WE2JZW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors/kb-error.ts","../src/utils/fs-atomic.ts"],"names":["fs"],"mappings":"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","file":"chunk-A2WE2JZW.js","sourcesContent":["/**\n * @module @kb-labs/core/config/errors\n * Standardized error class for KB Labs with exit code mapping\n */\n\nexport class KbError extends Error {\n constructor(\n public code: string,\n message: string,\n public hint?: string,\n public meta?: any\n ) {\n super(message);\n this.name = 'KbError';\n }\n}\n\n/**\n * Maps KbError codes to CLI exit codes\n */\nexport function getExitCode(err: KbError): number {\n if (err.code === 'ERR_FORBIDDEN') {return 3;}\n if (err.code === 'ERR_CONFIG_NOT_FOUND') {return 2;}\n if (err.code === 'ERR_CONFIG_EXISTS_CONFLICT') {return 2;}\n if (err.code === 'ERR_PATH_OUTSIDE_WORKSPACE') {return 2;}\n if (err.code.startsWith('ERR_')) {return 1;}\n return 1;\n}\n\n/**\n * Error codes with their standard hints\n */\nexport const ERROR_HINTS = {\n ERR_CONFIG_NOT_FOUND: 'Create .kb/kb-labs.config.yaml or run: kb init',\n ERR_CONFIG_INVALID: 'Check configuration syntax and required fields',\n ERR_CONFIG_EXISTS_CONFLICT: 'Use --force to overwrite existing configuration',\n ERR_PATH_OUTSIDE_WORKSPACE: 'Check cwd or use a relative path within the workspace',\n ERR_PRESET_NOT_RESOLVED: 'Install the required preset package or check network connectivity',\n ERR_PROFILE_INCOMPATIBLE: 'Profile version is incompatible with current system',\n ERR_PROFILE_NOT_DEFINED: 'Add profiles[] to kb.config.json or pass --profile=<id>',\n ERR_PROFILE_RESOLVE_FAILED: 'Profile registry unavailable. Define profiles[] inside kb.config.json or use workspace presets.',\n ERR_PROFILE_EXTENDS_FAILED: 'Profile extends failed. Check extends reference or install required preset/package.',\n ERR_PROFILE_SCOPE_NOT_FOUND: 'Scope not found. Use --scope=<id> with one of the configured scopes.',\n ERR_PROFILE_SCOPE_CONFLICT: 'Multiple scopes matched. Specify --scope=<id> explicitly.',\n ERR_PROFILE_INVALID_FORMAT: 'Update profiles[] in kb.config.json (Profiles v2) or rerun your plugin setup command.',\n ERR_ARTIFACT_LIMIT_EXCEEDED: 'Remove or split large artifacts, or reduce artifact count',\n ERR_FORBIDDEN: 'Add role in policy.overrides or use preset...',\n} as const;\n\nexport type ErrorCode = keyof typeof ERROR_HINTS;\n","/**\n * @module @kb-labs/core/config/utils/fs-atomic\n * Atomic file write operations using tmp+rename pattern\n */\n\nimport { promises as fs } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { KbError } from '../errors/kb-error';\nimport { dirname } from 'node:path';\n\n/**\n * Write file atomically using tmp+rename pattern\n * This ensures that the file is never partially written in case of crashes\n */\nexport async function writeFileAtomic(\n path: string,\n data: string | Uint8Array\n): Promise<void> {\n const tmp = `${path}.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n \n try {\n // Ensure parent directory exists\n await fs.mkdir(dirname(path), { recursive: true });\n await fs.mkdir(dirname(tmp), { recursive: true });\n \n // Write to temp file\n await fs.writeFile(tmp, data);\n \n // Atomic rename\n await fs.rename(tmp, path);\n } catch (error) {\n // Clean up temp file if it exists\n try {\n await fs.unlink(tmp);\n } catch {\n // Ignore cleanup errors\n }\n throw error;\n }\n}\n\n/**\n * Ensure a path is within the workspace root\n * Throws ERR_PATH_OUTSIDE_WORKSPACE if the path escapes the workspace\n */\nexport function ensureWithinWorkspace(targetPath: string, workspaceRoot: string): void {\n const absTarget = resolve(targetPath);\n const absRoot = resolve(workspaceRoot);\n \n if (!absTarget.startsWith(absRoot)) {\n throw new KbError(\n 'ERR_PATH_OUTSIDE_WORKSPACE',\n `Refusing to write outside workspace: ${targetPath}`,\n 'Check cwd or use a relative path within the workspace',\n { targetPath, workspaceRoot }\n );\n }\n}\n\n"]}
@@ -0,0 +1,3 @@
1
+ export { ensureWithinWorkspace, writeFileAtomic } from './chunk-A2WE2JZW.js';
2
+ //# sourceMappingURL=fs-atomic-EKSZ6G5A.js.map
3
+ //# sourceMappingURL=fs-atomic-EKSZ6G5A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"fs-atomic-EKSZ6G5A.js"}