elsabro 2.3.0 → 3.7.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.
Files changed (67) hide show
  1. package/README.md +668 -20
  2. package/bin/install.js +0 -0
  3. package/flows/development-flow.json +452 -0
  4. package/flows/quick-flow.json +118 -0
  5. package/package.json +3 -2
  6. package/references/SYSTEM_INDEX.md +379 -5
  7. package/references/agent-marketplace.md +2274 -0
  8. package/references/agent-protocol.md +1126 -0
  9. package/references/ai-code-suggestions.md +2413 -0
  10. package/references/checkpointing.md +595 -0
  11. package/references/collaboration-patterns.md +851 -0
  12. package/references/collaborative-sessions.md +1081 -0
  13. package/references/configuration-management.md +1810 -0
  14. package/references/cost-tracking.md +1095 -0
  15. package/references/enterprise-sso.md +2001 -0
  16. package/references/error-contracts-v2.md +968 -0
  17. package/references/event-driven.md +1031 -0
  18. package/references/flow-orchestration.md +940 -0
  19. package/references/flow-visualization.md +1557 -0
  20. package/references/ide-integrations.md +3513 -0
  21. package/references/interrupt-system.md +681 -0
  22. package/references/kubernetes-deployment.md +3099 -0
  23. package/references/memory-system.md +683 -0
  24. package/references/mobile-companion.md +3236 -0
  25. package/references/multi-llm-providers.md +2494 -0
  26. package/references/multi-project-memory.md +1182 -0
  27. package/references/observability.md +793 -0
  28. package/references/output-schemas.md +858 -0
  29. package/references/performance-profiler.md +955 -0
  30. package/references/plugin-system.md +1526 -0
  31. package/references/prompt-management.md +292 -0
  32. package/references/sandbox-execution.md +303 -0
  33. package/references/security-system.md +1253 -0
  34. package/references/streaming.md +696 -0
  35. package/references/testing-framework.md +1151 -0
  36. package/references/time-travel.md +802 -0
  37. package/references/tool-registry.md +886 -0
  38. package/references/voice-commands.md +3296 -0
  39. package/templates/agent-marketplace-config.json +220 -0
  40. package/templates/agent-protocol-config.json +136 -0
  41. package/templates/ai-suggestions-config.json +100 -0
  42. package/templates/checkpoint-state.json +61 -0
  43. package/templates/collaboration-config.json +157 -0
  44. package/templates/collaborative-sessions-config.json +153 -0
  45. package/templates/configuration-config.json +245 -0
  46. package/templates/cost-tracking-config.json +148 -0
  47. package/templates/enterprise-sso-config.json +438 -0
  48. package/templates/events-config.json +148 -0
  49. package/templates/flow-visualization-config.json +196 -0
  50. package/templates/ide-integrations-config.json +442 -0
  51. package/templates/kubernetes-config.json +764 -0
  52. package/templates/memory-state.json +84 -0
  53. package/templates/mobile-companion-config.json +600 -0
  54. package/templates/multi-llm-config.json +544 -0
  55. package/templates/multi-project-memory-config.json +145 -0
  56. package/templates/observability-config.json +109 -0
  57. package/templates/performance-profiler-config.json +125 -0
  58. package/templates/plugin-config.json +170 -0
  59. package/templates/prompt-management-config.json +86 -0
  60. package/templates/sandbox-config.json +185 -0
  61. package/templates/schemas-config.json +65 -0
  62. package/templates/security-config.json +120 -0
  63. package/templates/streaming-config.json +72 -0
  64. package/templates/testing-config.json +81 -0
  65. package/templates/timetravel-config.json +62 -0
  66. package/templates/tool-registry-config.json +109 -0
  67. package/templates/voice-commands-config.json +658 -0
@@ -0,0 +1,1810 @@
1
+ # ELSABRO Configuration Management System
2
+
3
+ > Sistema avanzado de configuración con environments, feature flags, validación y remote config.
4
+
5
+ ## Arquitectura General
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────────┐
9
+ │ Configuration Management │
10
+ ├─────────────────────────────────────────────────────────────────────┤
11
+ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
12
+ │ │ ConfigManager │ │ FeatureFlags │ │ EnvResolver │ │
13
+ │ │ ───────── │ │ ───────── │ │ ───────── │ │
14
+ │ │ • Load/Save │ │ • Targeting │ │ • Variables │ │
15
+ │ │ • Merge │ │ • Percentage │ │ • Inheritance │ │
16
+ │ │ • Watch │ │ • Schedules │ │ • Secrets │ │
17
+ │ └───────────────┘ └───────────────┘ └───────────────┘ │
18
+ │ │ │
19
+ │ ┌───────────────────────────┴───────────────────────────┐ │
20
+ │ │ ConfigValidator │ │
21
+ │ │ • JSON Schema • Type Coercion • Required Fields │ │
22
+ │ └────────────────────────────────────────────────────────┘ │
23
+ │ │ │
24
+ │ ┌───────────────────────────┴───────────────────────────┐ │
25
+ │ │ RemoteConfigSync │ │
26
+ │ │ • Polling • Webhooks • Caching • Fallback │ │
27
+ │ └────────────────────────────────────────────────────────┘ │
28
+ └─────────────────────────────────────────────────────────────────────┘
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 1. ConfigManager
34
+
35
+ ### Propósito
36
+ Gestiona la carga, fusión y persistencia de configuraciones con soporte para múltiples fuentes.
37
+
38
+ ### Interfaz
39
+
40
+ ```typescript
41
+ interface ConfigSource {
42
+ type: 'file' | 'env' | 'remote' | 'memory';
43
+ path?: string;
44
+ url?: string;
45
+ priority: number; // Higher = override lower
46
+ required?: boolean;
47
+ format?: 'json' | 'yaml' | 'toml' | 'env';
48
+ }
49
+
50
+ interface ConfigOptions {
51
+ sources: ConfigSource[];
52
+ environment: string;
53
+ watchForChanges: boolean;
54
+ reloadOnChange: boolean;
55
+ mergeStrategy: 'deep' | 'shallow' | 'replace';
56
+ encryptionKey?: string;
57
+ }
58
+
59
+ interface ConfigManager {
60
+ // Core operations
61
+ load(): Promise<void>;
62
+ get<T>(path: string, defaultValue?: T): T;
63
+ set(path: string, value: unknown): void;
64
+ has(path: string): boolean;
65
+ delete(path: string): void;
66
+
67
+ // Bulk operations
68
+ getAll(): Record<string, unknown>;
69
+ merge(config: Record<string, unknown>): void;
70
+ reset(): void;
71
+
72
+ // Environment
73
+ getEnvironment(): string;
74
+ setEnvironment(env: string): Promise<void>;
75
+
76
+ // Persistence
77
+ save(source?: string): Promise<void>;
78
+ export(format: 'json' | 'yaml' | 'env'): string;
79
+
80
+ // Events
81
+ onChange(path: string, callback: (newValue: unknown, oldValue: unknown) => void): () => void;
82
+ onReload(callback: (config: Record<string, unknown>) => void): () => void;
83
+ }
84
+ ```
85
+
86
+ ### Implementación
87
+
88
+ ```typescript
89
+ class ConfigManagerImpl implements ConfigManager {
90
+ private config: Map<string, unknown> = new Map();
91
+ private sources: ConfigSource[] = [];
92
+ private watchers: Map<string, Set<Function>> = new Map();
93
+ private environment: string;
94
+ private options: ConfigOptions;
95
+
96
+ constructor(options: ConfigOptions) {
97
+ this.options = options;
98
+ this.environment = options.environment;
99
+ this.sources = options.sources.sort((a, b) => a.priority - b.priority);
100
+ }
101
+
102
+ async load(): Promise<void> {
103
+ const configs: Record<string, unknown>[] = [];
104
+
105
+ for (const source of this.sources) {
106
+ try {
107
+ const config = await this.loadSource(source);
108
+ configs.push(config);
109
+ } catch (error) {
110
+ if (source.required) {
111
+ throw new ConfigLoadError(`Required config source failed: ${source.path || source.url}`, error);
112
+ }
113
+ console.warn(`Optional config source unavailable: ${source.path || source.url}`);
114
+ }
115
+ }
116
+
117
+ // Merge all configs in priority order
118
+ const merged = this.mergeConfigs(configs);
119
+
120
+ // Apply environment-specific overrides
121
+ const envConfig = merged.environments?.[this.environment];
122
+ if (envConfig) {
123
+ this.mergeConfigs([merged, envConfig as Record<string, unknown>]);
124
+ }
125
+
126
+ // Store flattened config
127
+ this.flattenAndStore(merged);
128
+
129
+ // Setup watchers if enabled
130
+ if (this.options.watchForChanges) {
131
+ this.setupWatchers();
132
+ }
133
+ }
134
+
135
+ private async loadSource(source: ConfigSource): Promise<Record<string, unknown>> {
136
+ switch (source.type) {
137
+ case 'file':
138
+ return this.loadFileConfig(source);
139
+ case 'env':
140
+ return this.loadEnvConfig(source);
141
+ case 'remote':
142
+ return this.loadRemoteConfig(source);
143
+ case 'memory':
144
+ return source.data || {};
145
+ default:
146
+ throw new Error(`Unknown config source type: ${source.type}`);
147
+ }
148
+ }
149
+
150
+ private async loadFileConfig(source: ConfigSource): Promise<Record<string, unknown>> {
151
+ const content = await fs.readFile(source.path!, 'utf-8');
152
+
153
+ switch (source.format || this.detectFormat(source.path!)) {
154
+ case 'json':
155
+ return JSON.parse(content);
156
+ case 'yaml':
157
+ return yaml.parse(content);
158
+ case 'toml':
159
+ return toml.parse(content);
160
+ case 'env':
161
+ return this.parseEnvFile(content);
162
+ default:
163
+ return JSON.parse(content);
164
+ }
165
+ }
166
+
167
+ private loadEnvConfig(source: ConfigSource): Record<string, unknown> {
168
+ const prefix = source.path || '';
169
+ const config: Record<string, unknown> = {};
170
+
171
+ for (const [key, value] of Object.entries(process.env)) {
172
+ if (prefix && !key.startsWith(prefix)) continue;
173
+
174
+ const configKey = prefix ? key.slice(prefix.length + 1) : key;
175
+ this.setNestedValue(config, configKey.toLowerCase().replace(/_/g, '.'), value);
176
+ }
177
+
178
+ return config;
179
+ }
180
+
181
+ private async loadRemoteConfig(source: ConfigSource): Promise<Record<string, unknown>> {
182
+ const response = await fetch(source.url!, {
183
+ headers: source.headers || {},
184
+ timeout: source.timeout || 5000
185
+ });
186
+
187
+ if (!response.ok) {
188
+ throw new Error(`Remote config fetch failed: ${response.status}`);
189
+ }
190
+
191
+ return response.json();
192
+ }
193
+
194
+ get<T>(path: string, defaultValue?: T): T {
195
+ const value = this.config.get(path);
196
+ return (value !== undefined ? value : defaultValue) as T;
197
+ }
198
+
199
+ set(path: string, value: unknown): void {
200
+ const oldValue = this.config.get(path);
201
+ this.config.set(path, value);
202
+ this.notifyWatchers(path, value, oldValue);
203
+ }
204
+
205
+ has(path: string): boolean {
206
+ return this.config.has(path);
207
+ }
208
+
209
+ onChange(path: string, callback: Function): () => void {
210
+ if (!this.watchers.has(path)) {
211
+ this.watchers.set(path, new Set());
212
+ }
213
+ this.watchers.get(path)!.add(callback);
214
+
215
+ return () => {
216
+ this.watchers.get(path)?.delete(callback);
217
+ };
218
+ }
219
+
220
+ private notifyWatchers(path: string, newValue: unknown, oldValue: unknown): void {
221
+ // Exact path watchers
222
+ this.watchers.get(path)?.forEach(cb => cb(newValue, oldValue));
223
+
224
+ // Wildcard watchers
225
+ this.watchers.forEach((callbacks, pattern) => {
226
+ if (pattern.includes('*') && this.matchesPattern(path, pattern)) {
227
+ callbacks.forEach(cb => cb(newValue, oldValue));
228
+ }
229
+ });
230
+ }
231
+
232
+ private mergeConfigs(configs: Record<string, unknown>[]): Record<string, unknown> {
233
+ switch (this.options.mergeStrategy) {
234
+ case 'shallow':
235
+ return Object.assign({}, ...configs);
236
+ case 'replace':
237
+ return configs[configs.length - 1] || {};
238
+ case 'deep':
239
+ default:
240
+ return this.deepMerge({}, ...configs);
241
+ }
242
+ }
243
+
244
+ private deepMerge(...objects: Record<string, unknown>[]): Record<string, unknown> {
245
+ const result: Record<string, unknown> = {};
246
+
247
+ for (const obj of objects) {
248
+ for (const [key, value] of Object.entries(obj)) {
249
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
250
+ result[key] = this.deepMerge(
251
+ (result[key] || {}) as Record<string, unknown>,
252
+ value as Record<string, unknown>
253
+ );
254
+ } else {
255
+ result[key] = value;
256
+ }
257
+ }
258
+ }
259
+
260
+ return result;
261
+ }
262
+ }
263
+ ```
264
+
265
+ ### Uso
266
+
267
+ ```typescript
268
+ const config = new ConfigManager({
269
+ environment: process.env.NODE_ENV || 'development',
270
+ sources: [
271
+ { type: 'file', path: '.elsabro/config.json', priority: 1 },
272
+ { type: 'file', path: '.elsabro/config.local.json', priority: 2, required: false },
273
+ { type: 'env', path: 'ELSABRO', priority: 3 },
274
+ { type: 'remote', url: 'https://config.example.com/elsabro', priority: 4, required: false }
275
+ ],
276
+ watchForChanges: true,
277
+ reloadOnChange: true,
278
+ mergeStrategy: 'deep'
279
+ });
280
+
281
+ await config.load();
282
+
283
+ // Get values
284
+ const llmModel = config.get('llm.model', 'claude-sonnet-4-20250514');
285
+ const maxTokens = config.get<number>('llm.maxTokens', 4096);
286
+
287
+ // Watch for changes
288
+ config.onChange('llm.*', (newValue, oldValue) => {
289
+ console.log('LLM config changed:', { newValue, oldValue });
290
+ });
291
+ ```
292
+
293
+ ---
294
+
295
+ ## 2. FeatureFlags
296
+
297
+ ### Propósito
298
+ Sistema de feature flags con targeting, rollout gradual y programación temporal.
299
+
300
+ ### Interfaz
301
+
302
+ ```typescript
303
+ interface FeatureFlag {
304
+ key: string;
305
+ name: string;
306
+ description?: string;
307
+ enabled: boolean;
308
+
309
+ // Targeting
310
+ targeting?: {
311
+ rules: TargetingRule[];
312
+ defaultVariant: string;
313
+ };
314
+
315
+ // Variants for A/B testing
316
+ variants?: {
317
+ [key: string]: {
318
+ value: unknown;
319
+ weight?: number; // For percentage rollout
320
+ };
321
+ };
322
+
323
+ // Schedule
324
+ schedule?: {
325
+ enableAt?: string; // ISO date
326
+ disableAt?: string; // ISO date
327
+ timezone?: string;
328
+ };
329
+
330
+ // Metadata
331
+ tags?: string[];
332
+ owner?: string;
333
+ createdAt: string;
334
+ updatedAt: string;
335
+ }
336
+
337
+ interface TargetingRule {
338
+ attribute: string;
339
+ operator: 'eq' | 'neq' | 'in' | 'nin' | 'gt' | 'lt' | 'contains' | 'regex';
340
+ value: unknown;
341
+ variant: string;
342
+ }
343
+
344
+ interface EvaluationContext {
345
+ userId?: string;
346
+ sessionId?: string;
347
+ environment?: string;
348
+ attributes?: Record<string, unknown>;
349
+ }
350
+
351
+ interface FeatureFlagService {
352
+ // Evaluation
353
+ isEnabled(key: string, context?: EvaluationContext): boolean;
354
+ getVariant(key: string, context?: EvaluationContext): string;
355
+ getValue<T>(key: string, context?: EvaluationContext): T | undefined;
356
+
357
+ // Management
358
+ getFlag(key: string): FeatureFlag | undefined;
359
+ getAllFlags(): FeatureFlag[];
360
+ createFlag(flag: Omit<FeatureFlag, 'createdAt' | 'updatedAt'>): FeatureFlag;
361
+ updateFlag(key: string, updates: Partial<FeatureFlag>): FeatureFlag;
362
+ deleteFlag(key: string): void;
363
+
364
+ // Bulk operations
365
+ enableAll(tags?: string[]): void;
366
+ disableAll(tags?: string[]): void;
367
+
368
+ // Events
369
+ onFlagChange(key: string, callback: (flag: FeatureFlag) => void): () => void;
370
+ }
371
+ ```
372
+
373
+ ### Implementación
374
+
375
+ ```typescript
376
+ class FeatureFlagServiceImpl implements FeatureFlagService {
377
+ private flags: Map<string, FeatureFlag> = new Map();
378
+ private listeners: Map<string, Set<Function>> = new Map();
379
+ private hashSeed: string;
380
+
381
+ constructor(private config: ConfigManager, seed?: string) {
382
+ this.hashSeed = seed || 'elsabro-feature-flags';
383
+ this.loadFlags();
384
+ }
385
+
386
+ private loadFlags(): void {
387
+ const flagsConfig = this.config.get<Record<string, FeatureFlag>>('featureFlags', {});
388
+ for (const [key, flag] of Object.entries(flagsConfig)) {
389
+ this.flags.set(key, { ...flag, key });
390
+ }
391
+ }
392
+
393
+ isEnabled(key: string, context: EvaluationContext = {}): boolean {
394
+ const flag = this.flags.get(key);
395
+ if (!flag) return false;
396
+
397
+ // Check schedule
398
+ if (!this.isWithinSchedule(flag)) return false;
399
+
400
+ // Check base enabled state
401
+ if (!flag.enabled) return false;
402
+
403
+ // Evaluate targeting rules
404
+ if (flag.targeting?.rules) {
405
+ for (const rule of flag.targeting.rules) {
406
+ if (this.evaluateRule(rule, context)) {
407
+ return rule.variant === 'enabled' || rule.variant === 'true';
408
+ }
409
+ }
410
+ }
411
+
412
+ // Percentage rollout
413
+ if (flag.variants) {
414
+ const variant = this.getVariantByPercentage(flag, context);
415
+ return variant === 'enabled' || variant === 'true';
416
+ }
417
+
418
+ return true;
419
+ }
420
+
421
+ getVariant(key: string, context: EvaluationContext = {}): string {
422
+ const flag = this.flags.get(key);
423
+ if (!flag) return 'control';
424
+
425
+ if (!this.isWithinSchedule(flag)) return 'control';
426
+ if (!flag.enabled) return 'control';
427
+
428
+ // Evaluate targeting rules
429
+ if (flag.targeting?.rules) {
430
+ for (const rule of flag.targeting.rules) {
431
+ if (this.evaluateRule(rule, context)) {
432
+ return rule.variant;
433
+ }
434
+ }
435
+ }
436
+
437
+ // Percentage-based variant selection
438
+ if (flag.variants) {
439
+ return this.getVariantByPercentage(flag, context);
440
+ }
441
+
442
+ return flag.targeting?.defaultVariant || 'control';
443
+ }
444
+
445
+ getValue<T>(key: string, context: EvaluationContext = {}): T | undefined {
446
+ const flag = this.flags.get(key);
447
+ if (!flag || !flag.variants) return undefined;
448
+
449
+ const variant = this.getVariant(key, context);
450
+ return flag.variants[variant]?.value as T;
451
+ }
452
+
453
+ private isWithinSchedule(flag: FeatureFlag): boolean {
454
+ if (!flag.schedule) return true;
455
+
456
+ const now = new Date();
457
+ const { enableAt, disableAt } = flag.schedule;
458
+
459
+ if (enableAt && new Date(enableAt) > now) return false;
460
+ if (disableAt && new Date(disableAt) < now) return false;
461
+
462
+ return true;
463
+ }
464
+
465
+ private evaluateRule(rule: TargetingRule, context: EvaluationContext): boolean {
466
+ const attributeValue = this.getAttributeValue(rule.attribute, context);
467
+
468
+ switch (rule.operator) {
469
+ case 'eq':
470
+ return attributeValue === rule.value;
471
+ case 'neq':
472
+ return attributeValue !== rule.value;
473
+ case 'in':
474
+ return Array.isArray(rule.value) && rule.value.includes(attributeValue);
475
+ case 'nin':
476
+ return Array.isArray(rule.value) && !rule.value.includes(attributeValue);
477
+ case 'gt':
478
+ return Number(attributeValue) > Number(rule.value);
479
+ case 'lt':
480
+ return Number(attributeValue) < Number(rule.value);
481
+ case 'contains':
482
+ return String(attributeValue).includes(String(rule.value));
483
+ case 'regex':
484
+ return new RegExp(String(rule.value)).test(String(attributeValue));
485
+ default:
486
+ return false;
487
+ }
488
+ }
489
+
490
+ private getAttributeValue(attribute: string, context: EvaluationContext): unknown {
491
+ // Built-in attributes
492
+ switch (attribute) {
493
+ case 'userId':
494
+ return context.userId;
495
+ case 'sessionId':
496
+ return context.sessionId;
497
+ case 'environment':
498
+ return context.environment;
499
+ default:
500
+ return context.attributes?.[attribute];
501
+ }
502
+ }
503
+
504
+ private getVariantByPercentage(flag: FeatureFlag, context: EvaluationContext): string {
505
+ if (!flag.variants) return 'control';
506
+
507
+ // Generate deterministic hash for consistent bucketing
508
+ const identifier = context.userId || context.sessionId || 'anonymous';
509
+ const hash = this.hashString(`${flag.key}:${identifier}:${this.hashSeed}`);
510
+ const bucket = hash % 100;
511
+
512
+ // Calculate cumulative weights
513
+ let cumulative = 0;
514
+ for (const [variant, config] of Object.entries(flag.variants)) {
515
+ cumulative += config.weight || 0;
516
+ if (bucket < cumulative) {
517
+ return variant;
518
+ }
519
+ }
520
+
521
+ return flag.targeting?.defaultVariant || Object.keys(flag.variants)[0] || 'control';
522
+ }
523
+
524
+ private hashString(str: string): number {
525
+ let hash = 0;
526
+ for (let i = 0; i < str.length; i++) {
527
+ const char = str.charCodeAt(i);
528
+ hash = ((hash << 5) - hash) + char;
529
+ hash = hash & hash;
530
+ }
531
+ return Math.abs(hash);
532
+ }
533
+
534
+ createFlag(flag: Omit<FeatureFlag, 'createdAt' | 'updatedAt'>): FeatureFlag {
535
+ const now = new Date().toISOString();
536
+ const newFlag: FeatureFlag = {
537
+ ...flag,
538
+ createdAt: now,
539
+ updatedAt: now
540
+ };
541
+
542
+ this.flags.set(flag.key, newFlag);
543
+ this.notifyListeners(flag.key, newFlag);
544
+ return newFlag;
545
+ }
546
+
547
+ updateFlag(key: string, updates: Partial<FeatureFlag>): FeatureFlag {
548
+ const existing = this.flags.get(key);
549
+ if (!existing) {
550
+ throw new Error(`Flag not found: ${key}`);
551
+ }
552
+
553
+ const updated: FeatureFlag = {
554
+ ...existing,
555
+ ...updates,
556
+ key, // Prevent key change
557
+ createdAt: existing.createdAt,
558
+ updatedAt: new Date().toISOString()
559
+ };
560
+
561
+ this.flags.set(key, updated);
562
+ this.notifyListeners(key, updated);
563
+ return updated;
564
+ }
565
+
566
+ onFlagChange(key: string, callback: Function): () => void {
567
+ if (!this.listeners.has(key)) {
568
+ this.listeners.set(key, new Set());
569
+ }
570
+ this.listeners.get(key)!.add(callback);
571
+
572
+ return () => {
573
+ this.listeners.get(key)?.delete(callback);
574
+ };
575
+ }
576
+
577
+ private notifyListeners(key: string, flag: FeatureFlag): void {
578
+ this.listeners.get(key)?.forEach(cb => cb(flag));
579
+ this.listeners.get('*')?.forEach(cb => cb(flag));
580
+ }
581
+ }
582
+ ```
583
+
584
+ ### Uso
585
+
586
+ ```typescript
587
+ const flags = new FeatureFlagService(config);
588
+
589
+ // Simple check
590
+ if (flags.isEnabled('new-agent-ui')) {
591
+ renderNewUI();
592
+ }
593
+
594
+ // With targeting context
595
+ const context = {
596
+ userId: 'user-123',
597
+ environment: 'production',
598
+ attributes: {
599
+ plan: 'enterprise',
600
+ region: 'us-west'
601
+ }
602
+ };
603
+
604
+ if (flags.isEnabled('beta-features', context)) {
605
+ enableBetaFeatures();
606
+ }
607
+
608
+ // A/B testing
609
+ const variant = flags.getVariant('checkout-flow', context);
610
+ switch (variant) {
611
+ case 'single-page':
612
+ renderSinglePageCheckout();
613
+ break;
614
+ case 'multi-step':
615
+ renderMultiStepCheckout();
616
+ break;
617
+ default:
618
+ renderDefaultCheckout();
619
+ }
620
+
621
+ // Get variant value
622
+ const buttonColor = flags.getValue<string>('button-color', context);
623
+ ```
624
+
625
+ ---
626
+
627
+ ## 3. EnvResolver
628
+
629
+ ### Propósito
630
+ Resuelve variables de entorno con herencia, interpolación y soporte para secretos.
631
+
632
+ ### Interfaz
633
+
634
+ ```typescript
635
+ interface EnvDefinition {
636
+ name: string;
637
+ inherits?: string; // Parent environment
638
+ variables: Record<string, EnvVariable>;
639
+ }
640
+
641
+ interface EnvVariable {
642
+ value?: string;
643
+ secret?: boolean;
644
+ required?: boolean;
645
+ default?: string;
646
+ description?: string;
647
+ validation?: {
648
+ type?: 'string' | 'number' | 'boolean' | 'url' | 'email';
649
+ pattern?: string;
650
+ min?: number;
651
+ max?: number;
652
+ enum?: string[];
653
+ };
654
+ }
655
+
656
+ interface ResolvedEnv {
657
+ [key: string]: string;
658
+ }
659
+
660
+ interface EnvResolver {
661
+ // Resolution
662
+ resolve(envName: string): Promise<ResolvedEnv>;
663
+ get(envName: string, key: string): Promise<string | undefined>;
664
+
665
+ // Interpolation
666
+ interpolate(template: string, env?: ResolvedEnv): Promise<string>;
667
+
668
+ // Validation
669
+ validate(envName: string): Promise<ValidationResult>;
670
+
671
+ // Environment management
672
+ getEnvironments(): string[];
673
+ getCurrentEnvironment(): string;
674
+ setCurrentEnvironment(name: string): void;
675
+ }
676
+
677
+ interface ValidationResult {
678
+ valid: boolean;
679
+ errors: ValidationError[];
680
+ warnings: ValidationWarning[];
681
+ }
682
+ ```
683
+
684
+ ### Implementación
685
+
686
+ ```typescript
687
+ class EnvResolverImpl implements EnvResolver {
688
+ private definitions: Map<string, EnvDefinition> = new Map();
689
+ private cache: Map<string, ResolvedEnv> = new Map();
690
+ private currentEnv: string = 'development';
691
+
692
+ constructor(
693
+ private config: ConfigManager,
694
+ private secretsVault?: SecretsVault
695
+ ) {
696
+ this.loadDefinitions();
697
+ }
698
+
699
+ private loadDefinitions(): void {
700
+ const envs = this.config.get<Record<string, EnvDefinition>>('environments', {});
701
+ for (const [name, def] of Object.entries(envs)) {
702
+ this.definitions.set(name, { ...def, name });
703
+ }
704
+ }
705
+
706
+ async resolve(envName: string): Promise<ResolvedEnv> {
707
+ // Check cache
708
+ if (this.cache.has(envName)) {
709
+ return this.cache.get(envName)!;
710
+ }
711
+
712
+ const definition = this.definitions.get(envName);
713
+ if (!definition) {
714
+ throw new Error(`Environment not found: ${envName}`);
715
+ }
716
+
717
+ // Build inheritance chain
718
+ const chain = this.buildInheritanceChain(envName);
719
+
720
+ // Merge variables from all environments in chain
721
+ let resolved: ResolvedEnv = {};
722
+ for (const env of chain) {
723
+ const envDef = this.definitions.get(env)!;
724
+ resolved = { ...resolved, ...await this.resolveVariables(envDef.variables) };
725
+ }
726
+
727
+ // Apply process.env overrides (highest priority)
728
+ for (const key of Object.keys(resolved)) {
729
+ if (process.env[key] !== undefined) {
730
+ resolved[key] = process.env[key]!;
731
+ }
732
+ }
733
+
734
+ // Interpolate variable references
735
+ resolved = await this.interpolateAll(resolved);
736
+
737
+ this.cache.set(envName, resolved);
738
+ return resolved;
739
+ }
740
+
741
+ private buildInheritanceChain(envName: string): string[] {
742
+ const chain: string[] = [];
743
+ let current = envName;
744
+ const visited = new Set<string>();
745
+
746
+ while (current) {
747
+ if (visited.has(current)) {
748
+ throw new Error(`Circular inheritance detected: ${current}`);
749
+ }
750
+ visited.add(current);
751
+ chain.unshift(current);
752
+
753
+ const def = this.definitions.get(current);
754
+ current = def?.inherits || '';
755
+ }
756
+
757
+ return chain;
758
+ }
759
+
760
+ private async resolveVariables(
761
+ variables: Record<string, EnvVariable>
762
+ ): Promise<ResolvedEnv> {
763
+ const resolved: ResolvedEnv = {};
764
+
765
+ for (const [key, variable] of Object.entries(variables)) {
766
+ let value: string | undefined;
767
+
768
+ if (variable.secret && this.secretsVault) {
769
+ // Retrieve from secrets vault
770
+ value = await this.secretsVault.get(key);
771
+ } else if (variable.value !== undefined) {
772
+ value = variable.value;
773
+ } else if (variable.default !== undefined) {
774
+ value = variable.default;
775
+ }
776
+
777
+ if (value === undefined && variable.required) {
778
+ throw new Error(`Required environment variable not set: ${key}`);
779
+ }
780
+
781
+ if (value !== undefined) {
782
+ resolved[key] = value;
783
+ }
784
+ }
785
+
786
+ return resolved;
787
+ }
788
+
789
+ private async interpolateAll(env: ResolvedEnv): Promise<ResolvedEnv> {
790
+ const result: ResolvedEnv = {};
791
+ let changed = true;
792
+ let iterations = 0;
793
+ const maxIterations = 10;
794
+
795
+ // Copy initial values
796
+ for (const [key, value] of Object.entries(env)) {
797
+ result[key] = value;
798
+ }
799
+
800
+ // Iteratively resolve references until stable
801
+ while (changed && iterations < maxIterations) {
802
+ changed = false;
803
+ iterations++;
804
+
805
+ for (const [key, value] of Object.entries(result)) {
806
+ const interpolated = await this.interpolate(value, result);
807
+ if (interpolated !== result[key]) {
808
+ result[key] = interpolated;
809
+ changed = true;
810
+ }
811
+ }
812
+ }
813
+
814
+ return result;
815
+ }
816
+
817
+ async interpolate(template: string, env?: ResolvedEnv): Promise<string> {
818
+ const resolvedEnv = env || await this.resolve(this.currentEnv);
819
+
820
+ // Pattern: ${VAR_NAME} or ${VAR_NAME:-default}
821
+ return template.replace(/\$\{([^}]+)\}/g, (match, expr) => {
822
+ const [key, defaultValue] = expr.split(':-');
823
+
824
+ if (key in resolvedEnv) {
825
+ return resolvedEnv[key];
826
+ }
827
+ if (process.env[key]) {
828
+ return process.env[key]!;
829
+ }
830
+ if (defaultValue !== undefined) {
831
+ return defaultValue;
832
+ }
833
+
834
+ return match; // Keep unresolved
835
+ });
836
+ }
837
+
838
+ async validate(envName: string): Promise<ValidationResult> {
839
+ const errors: ValidationError[] = [];
840
+ const warnings: ValidationWarning[] = [];
841
+
842
+ const definition = this.definitions.get(envName);
843
+ if (!definition) {
844
+ return {
845
+ valid: false,
846
+ errors: [{ key: '', message: `Environment not found: ${envName}` }],
847
+ warnings: []
848
+ };
849
+ }
850
+
851
+ const resolved = await this.resolve(envName);
852
+
853
+ for (const [key, variable] of Object.entries(definition.variables)) {
854
+ const value = resolved[key];
855
+
856
+ // Required check
857
+ if (variable.required && !value) {
858
+ errors.push({ key, message: 'Required variable is missing' });
859
+ continue;
860
+ }
861
+
862
+ if (!value) continue;
863
+
864
+ // Type validation
865
+ if (variable.validation) {
866
+ const v = variable.validation;
867
+
868
+ switch (v.type) {
869
+ case 'number':
870
+ if (isNaN(Number(value))) {
871
+ errors.push({ key, message: 'Must be a number' });
872
+ } else {
873
+ const num = Number(value);
874
+ if (v.min !== undefined && num < v.min) {
875
+ errors.push({ key, message: `Must be at least ${v.min}` });
876
+ }
877
+ if (v.max !== undefined && num > v.max) {
878
+ errors.push({ key, message: `Must be at most ${v.max}` });
879
+ }
880
+ }
881
+ break;
882
+
883
+ case 'boolean':
884
+ if (!['true', 'false', '1', '0'].includes(value.toLowerCase())) {
885
+ errors.push({ key, message: 'Must be a boolean' });
886
+ }
887
+ break;
888
+
889
+ case 'url':
890
+ try {
891
+ new URL(value);
892
+ } catch {
893
+ errors.push({ key, message: 'Must be a valid URL' });
894
+ }
895
+ break;
896
+
897
+ case 'email':
898
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
899
+ errors.push({ key, message: 'Must be a valid email' });
900
+ }
901
+ break;
902
+ }
903
+
904
+ // Pattern validation
905
+ if (v.pattern && !new RegExp(v.pattern).test(value)) {
906
+ errors.push({ key, message: `Must match pattern: ${v.pattern}` });
907
+ }
908
+
909
+ // Enum validation
910
+ if (v.enum && !v.enum.includes(value)) {
911
+ errors.push({ key, message: `Must be one of: ${v.enum.join(', ')}` });
912
+ }
913
+ }
914
+
915
+ // Warnings for potential issues
916
+ if (variable.secret && !this.secretsVault) {
917
+ warnings.push({ key, message: 'Secret variable without vault configured' });
918
+ }
919
+ }
920
+
921
+ return {
922
+ valid: errors.length === 0,
923
+ errors,
924
+ warnings
925
+ };
926
+ }
927
+
928
+ getEnvironments(): string[] {
929
+ return Array.from(this.definitions.keys());
930
+ }
931
+
932
+ getCurrentEnvironment(): string {
933
+ return this.currentEnv;
934
+ }
935
+
936
+ setCurrentEnvironment(name: string): void {
937
+ if (!this.definitions.has(name)) {
938
+ throw new Error(`Environment not found: ${name}`);
939
+ }
940
+ this.currentEnv = name;
941
+ this.cache.clear();
942
+ }
943
+ }
944
+ ```
945
+
946
+ ### Uso
947
+
948
+ ```typescript
949
+ const envResolver = new EnvResolver(config, secretsVault);
950
+
951
+ // Resolve all variables for an environment
952
+ const prodEnv = await envResolver.resolve('production');
953
+ console.log(prodEnv.DATABASE_URL);
954
+
955
+ // Interpolate templates
956
+ const connectionString = await envResolver.interpolate(
957
+ 'postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT:-5432}/${DB_NAME}'
958
+ );
959
+
960
+ // Validate environment
961
+ const validation = await envResolver.validate('production');
962
+ if (!validation.valid) {
963
+ console.error('Environment validation failed:', validation.errors);
964
+ }
965
+ ```
966
+
967
+ ---
968
+
969
+ ## 4. ConfigValidator
970
+
971
+ ### Propósito
972
+ Valida configuraciones contra JSON Schema con coerción de tipos y mensajes de error amigables.
973
+
974
+ ### Interfaz
975
+
976
+ ```typescript
977
+ interface SchemaDefinition {
978
+ $schema?: string;
979
+ type: string;
980
+ properties?: Record<string, SchemaDefinition>;
981
+ required?: string[];
982
+ items?: SchemaDefinition;
983
+ enum?: unknown[];
984
+ minimum?: number;
985
+ maximum?: number;
986
+ minLength?: number;
987
+ maxLength?: number;
988
+ pattern?: string;
989
+ format?: string;
990
+ default?: unknown;
991
+ description?: string;
992
+ additionalProperties?: boolean | SchemaDefinition;
993
+ }
994
+
995
+ interface ValidationOptions {
996
+ coerceTypes?: boolean;
997
+ removeAdditional?: boolean;
998
+ useDefaults?: boolean;
999
+ allErrors?: boolean;
1000
+ }
1001
+
1002
+ interface ConfigValidatorResult {
1003
+ valid: boolean;
1004
+ errors: ConfigValidationError[];
1005
+ coerced?: Record<string, unknown>;
1006
+ }
1007
+
1008
+ interface ConfigValidationError {
1009
+ path: string;
1010
+ message: string;
1011
+ keyword: string;
1012
+ params?: Record<string, unknown>;
1013
+ }
1014
+
1015
+ interface ConfigValidator {
1016
+ // Schema management
1017
+ addSchema(name: string, schema: SchemaDefinition): void;
1018
+ getSchema(name: string): SchemaDefinition | undefined;
1019
+
1020
+ // Validation
1021
+ validate(data: unknown, schemaName: string, options?: ValidationOptions): ConfigValidatorResult;
1022
+ validatePartial(data: unknown, schemaName: string, paths: string[]): ConfigValidatorResult;
1023
+
1024
+ // Utilities
1025
+ coerce(data: unknown, schemaName: string): unknown;
1026
+ getDefaults(schemaName: string): Record<string, unknown>;
1027
+ }
1028
+ ```
1029
+
1030
+ ### Implementación
1031
+
1032
+ ```typescript
1033
+ class ConfigValidatorImpl implements ConfigValidator {
1034
+ private schemas: Map<string, SchemaDefinition> = new Map();
1035
+
1036
+ constructor() {
1037
+ // Register built-in formats
1038
+ this.registerFormats();
1039
+ }
1040
+
1041
+ private formatValidators: Record<string, (value: string) => boolean> = {
1042
+ 'date-time': (v) => !isNaN(Date.parse(v)),
1043
+ 'date': (v) => /^\d{4}-\d{2}-\d{2}$/.test(v),
1044
+ 'time': (v) => /^\d{2}:\d{2}:\d{2}$/.test(v),
1045
+ 'email': (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
1046
+ 'uri': (v) => { try { new URL(v); return true; } catch { return false; } },
1047
+ 'uuid': (v) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v),
1048
+ 'hostname': (v) => /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(v),
1049
+ 'ipv4': (v) => /^(\d{1,3}\.){3}\d{1,3}$/.test(v) && v.split('.').every(n => parseInt(n) <= 255),
1050
+ };
1051
+
1052
+ addSchema(name: string, schema: SchemaDefinition): void {
1053
+ this.schemas.set(name, schema);
1054
+ }
1055
+
1056
+ getSchema(name: string): SchemaDefinition | undefined {
1057
+ return this.schemas.get(name);
1058
+ }
1059
+
1060
+ validate(
1061
+ data: unknown,
1062
+ schemaName: string,
1063
+ options: ValidationOptions = {}
1064
+ ): ConfigValidatorResult {
1065
+ const schema = this.schemas.get(schemaName);
1066
+ if (!schema) {
1067
+ return {
1068
+ valid: false,
1069
+ errors: [{ path: '', message: `Schema not found: ${schemaName}`, keyword: 'schema' }]
1070
+ };
1071
+ }
1072
+
1073
+ const errors: ConfigValidationError[] = [];
1074
+ let coerced = options.coerceTypes ? this.deepClone(data) : data;
1075
+
1076
+ if (options.useDefaults) {
1077
+ coerced = this.applyDefaults(coerced as Record<string, unknown>, schema);
1078
+ }
1079
+
1080
+ if (options.coerceTypes) {
1081
+ coerced = this.coerceTypes(coerced, schema);
1082
+ }
1083
+
1084
+ this.validateValue(coerced, schema, '', errors, options);
1085
+
1086
+ if (options.removeAdditional && schema.type === 'object') {
1087
+ coerced = this.removeAdditionalProps(coerced as Record<string, unknown>, schema);
1088
+ }
1089
+
1090
+ return {
1091
+ valid: errors.length === 0,
1092
+ errors,
1093
+ coerced: options.coerceTypes ? coerced as Record<string, unknown> : undefined
1094
+ };
1095
+ }
1096
+
1097
+ private validateValue(
1098
+ value: unknown,
1099
+ schema: SchemaDefinition,
1100
+ path: string,
1101
+ errors: ConfigValidationError[],
1102
+ options: ValidationOptions
1103
+ ): void {
1104
+ // Type validation
1105
+ if (!this.checkType(value, schema.type)) {
1106
+ errors.push({
1107
+ path,
1108
+ message: `Expected ${schema.type}, got ${typeof value}`,
1109
+ keyword: 'type',
1110
+ params: { expected: schema.type, actual: typeof value }
1111
+ });
1112
+ if (!options.allErrors) return;
1113
+ }
1114
+
1115
+ switch (schema.type) {
1116
+ case 'object':
1117
+ this.validateObject(value as Record<string, unknown>, schema, path, errors, options);
1118
+ break;
1119
+ case 'array':
1120
+ this.validateArray(value as unknown[], schema, path, errors, options);
1121
+ break;
1122
+ case 'string':
1123
+ this.validateString(value as string, schema, path, errors);
1124
+ break;
1125
+ case 'number':
1126
+ case 'integer':
1127
+ this.validateNumber(value as number, schema, path, errors);
1128
+ break;
1129
+ }
1130
+
1131
+ // Enum validation
1132
+ if (schema.enum && !schema.enum.includes(value)) {
1133
+ errors.push({
1134
+ path,
1135
+ message: `Must be one of: ${schema.enum.join(', ')}`,
1136
+ keyword: 'enum',
1137
+ params: { allowed: schema.enum }
1138
+ });
1139
+ }
1140
+ }
1141
+
1142
+ private validateObject(
1143
+ obj: Record<string, unknown>,
1144
+ schema: SchemaDefinition,
1145
+ path: string,
1146
+ errors: ConfigValidationError[],
1147
+ options: ValidationOptions
1148
+ ): void {
1149
+ // Required fields
1150
+ if (schema.required) {
1151
+ for (const field of schema.required) {
1152
+ if (!(field in obj)) {
1153
+ errors.push({
1154
+ path: this.joinPath(path, field),
1155
+ message: 'Required field is missing',
1156
+ keyword: 'required'
1157
+ });
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ // Property validation
1163
+ if (schema.properties) {
1164
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
1165
+ if (key in obj) {
1166
+ this.validateValue(obj[key], propSchema, this.joinPath(path, key), errors, options);
1167
+ }
1168
+ }
1169
+ }
1170
+
1171
+ // Additional properties
1172
+ if (schema.additionalProperties === false) {
1173
+ const allowed = new Set(Object.keys(schema.properties || {}));
1174
+ for (const key of Object.keys(obj)) {
1175
+ if (!allowed.has(key)) {
1176
+ errors.push({
1177
+ path: this.joinPath(path, key),
1178
+ message: 'Additional property not allowed',
1179
+ keyword: 'additionalProperties'
1180
+ });
1181
+ }
1182
+ }
1183
+ }
1184
+ }
1185
+
1186
+ private validateArray(
1187
+ arr: unknown[],
1188
+ schema: SchemaDefinition,
1189
+ path: string,
1190
+ errors: ConfigValidationError[],
1191
+ options: ValidationOptions
1192
+ ): void {
1193
+ if (schema.items) {
1194
+ arr.forEach((item, index) => {
1195
+ this.validateValue(item, schema.items!, `${path}[${index}]`, errors, options);
1196
+ });
1197
+ }
1198
+ }
1199
+
1200
+ private validateString(
1201
+ value: string,
1202
+ schema: SchemaDefinition,
1203
+ path: string,
1204
+ errors: ConfigValidationError[]
1205
+ ): void {
1206
+ if (schema.minLength !== undefined && value.length < schema.minLength) {
1207
+ errors.push({
1208
+ path,
1209
+ message: `Must be at least ${schema.minLength} characters`,
1210
+ keyword: 'minLength'
1211
+ });
1212
+ }
1213
+
1214
+ if (schema.maxLength !== undefined && value.length > schema.maxLength) {
1215
+ errors.push({
1216
+ path,
1217
+ message: `Must be at most ${schema.maxLength} characters`,
1218
+ keyword: 'maxLength'
1219
+ });
1220
+ }
1221
+
1222
+ if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
1223
+ errors.push({
1224
+ path,
1225
+ message: `Must match pattern: ${schema.pattern}`,
1226
+ keyword: 'pattern'
1227
+ });
1228
+ }
1229
+
1230
+ if (schema.format && this.formatValidators[schema.format]) {
1231
+ if (!this.formatValidators[schema.format](value)) {
1232
+ errors.push({
1233
+ path,
1234
+ message: `Invalid ${schema.format} format`,
1235
+ keyword: 'format'
1236
+ });
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ private validateNumber(
1242
+ value: number,
1243
+ schema: SchemaDefinition,
1244
+ path: string,
1245
+ errors: ConfigValidationError[]
1246
+ ): void {
1247
+ if (schema.type === 'integer' && !Number.isInteger(value)) {
1248
+ errors.push({
1249
+ path,
1250
+ message: 'Must be an integer',
1251
+ keyword: 'type'
1252
+ });
1253
+ }
1254
+
1255
+ if (schema.minimum !== undefined && value < schema.minimum) {
1256
+ errors.push({
1257
+ path,
1258
+ message: `Must be at least ${schema.minimum}`,
1259
+ keyword: 'minimum'
1260
+ });
1261
+ }
1262
+
1263
+ if (schema.maximum !== undefined && value > schema.maximum) {
1264
+ errors.push({
1265
+ path,
1266
+ message: `Must be at most ${schema.maximum}`,
1267
+ keyword: 'maximum'
1268
+ });
1269
+ }
1270
+ }
1271
+
1272
+ private checkType(value: unknown, type: string): boolean {
1273
+ switch (type) {
1274
+ case 'object':
1275
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1276
+ case 'array':
1277
+ return Array.isArray(value);
1278
+ case 'string':
1279
+ return typeof value === 'string';
1280
+ case 'number':
1281
+ return typeof value === 'number' && !isNaN(value);
1282
+ case 'integer':
1283
+ return typeof value === 'number' && Number.isInteger(value);
1284
+ case 'boolean':
1285
+ return typeof value === 'boolean';
1286
+ case 'null':
1287
+ return value === null;
1288
+ default:
1289
+ return true;
1290
+ }
1291
+ }
1292
+
1293
+ private coerceTypes(value: unknown, schema: SchemaDefinition): unknown {
1294
+ if (value === undefined || value === null) return value;
1295
+
1296
+ switch (schema.type) {
1297
+ case 'string':
1298
+ return String(value);
1299
+ case 'number':
1300
+ return Number(value);
1301
+ case 'integer':
1302
+ return Math.floor(Number(value));
1303
+ case 'boolean':
1304
+ if (typeof value === 'string') {
1305
+ return value.toLowerCase() === 'true' || value === '1';
1306
+ }
1307
+ return Boolean(value);
1308
+ case 'object':
1309
+ if (typeof value === 'object' && schema.properties) {
1310
+ const obj = value as Record<string, unknown>;
1311
+ const result: Record<string, unknown> = {};
1312
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
1313
+ if (key in obj) {
1314
+ result[key] = this.coerceTypes(obj[key], propSchema);
1315
+ }
1316
+ }
1317
+ return result;
1318
+ }
1319
+ return value;
1320
+ case 'array':
1321
+ if (Array.isArray(value) && schema.items) {
1322
+ return value.map(item => this.coerceTypes(item, schema.items!));
1323
+ }
1324
+ return value;
1325
+ default:
1326
+ return value;
1327
+ }
1328
+ }
1329
+
1330
+ private applyDefaults(
1331
+ obj: Record<string, unknown>,
1332
+ schema: SchemaDefinition
1333
+ ): Record<string, unknown> {
1334
+ const result = { ...obj };
1335
+
1336
+ if (schema.properties) {
1337
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
1338
+ if (!(key in result) && propSchema.default !== undefined) {
1339
+ result[key] = this.deepClone(propSchema.default);
1340
+ } else if (key in result && propSchema.type === 'object') {
1341
+ result[key] = this.applyDefaults(
1342
+ result[key] as Record<string, unknown>,
1343
+ propSchema
1344
+ );
1345
+ }
1346
+ }
1347
+ }
1348
+
1349
+ return result;
1350
+ }
1351
+
1352
+ getDefaults(schemaName: string): Record<string, unknown> {
1353
+ const schema = this.schemas.get(schemaName);
1354
+ if (!schema) return {};
1355
+ return this.extractDefaults(schema);
1356
+ }
1357
+
1358
+ private extractDefaults(schema: SchemaDefinition): Record<string, unknown> {
1359
+ const defaults: Record<string, unknown> = {};
1360
+
1361
+ if (schema.properties) {
1362
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
1363
+ if (propSchema.default !== undefined) {
1364
+ defaults[key] = propSchema.default;
1365
+ } else if (propSchema.type === 'object') {
1366
+ const nested = this.extractDefaults(propSchema);
1367
+ if (Object.keys(nested).length > 0) {
1368
+ defaults[key] = nested;
1369
+ }
1370
+ }
1371
+ }
1372
+ }
1373
+
1374
+ return defaults;
1375
+ }
1376
+
1377
+ private joinPath(base: string, key: string): string {
1378
+ return base ? `${base}.${key}` : key;
1379
+ }
1380
+
1381
+ private deepClone<T>(obj: T): T {
1382
+ return JSON.parse(JSON.stringify(obj));
1383
+ }
1384
+ }
1385
+ ```
1386
+
1387
+ ### Uso
1388
+
1389
+ ```typescript
1390
+ const validator = new ConfigValidator();
1391
+
1392
+ // Register schema
1393
+ validator.addSchema('elsabro-config', {
1394
+ type: 'object',
1395
+ required: ['llm', 'agents'],
1396
+ properties: {
1397
+ llm: {
1398
+ type: 'object',
1399
+ required: ['model'],
1400
+ properties: {
1401
+ model: {
1402
+ type: 'string',
1403
+ enum: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-3-5-haiku-20241022'],
1404
+ default: 'claude-sonnet-4-20250514'
1405
+ },
1406
+ maxTokens: {
1407
+ type: 'integer',
1408
+ minimum: 1,
1409
+ maximum: 200000,
1410
+ default: 4096
1411
+ },
1412
+ temperature: {
1413
+ type: 'number',
1414
+ minimum: 0,
1415
+ maximum: 1,
1416
+ default: 0.7
1417
+ }
1418
+ }
1419
+ },
1420
+ agents: {
1421
+ type: 'object',
1422
+ additionalProperties: {
1423
+ type: 'object',
1424
+ properties: {
1425
+ enabled: { type: 'boolean', default: true },
1426
+ model: { type: 'string' },
1427
+ maxIterations: { type: 'integer', minimum: 1 }
1428
+ }
1429
+ }
1430
+ }
1431
+ }
1432
+ });
1433
+
1434
+ // Validate config
1435
+ const result = validator.validate(userConfig, 'elsabro-config', {
1436
+ coerceTypes: true,
1437
+ useDefaults: true,
1438
+ allErrors: true
1439
+ });
1440
+
1441
+ if (!result.valid) {
1442
+ console.error('Config validation failed:');
1443
+ result.errors.forEach(err => {
1444
+ console.error(` ${err.path}: ${err.message}`);
1445
+ });
1446
+ } else {
1447
+ // Use coerced config with defaults applied
1448
+ const validConfig = result.coerced;
1449
+ }
1450
+ ```
1451
+
1452
+ ---
1453
+
1454
+ ## 5. RemoteConfigSync
1455
+
1456
+ ### Propósito
1457
+ Sincroniza configuración desde fuentes remotas con caching, fallback y actualización automática.
1458
+
1459
+ ### Interfaz
1460
+
1461
+ ```typescript
1462
+ interface RemoteConfigOptions {
1463
+ url: string;
1464
+ pollingInterval?: number; // ms, 0 = disabled
1465
+ cacheEnabled?: boolean;
1466
+ cacheTTL?: number; // ms
1467
+ fallbackToCache?: boolean;
1468
+ headers?: Record<string, string>;
1469
+ timeout?: number;
1470
+ retryAttempts?: number;
1471
+ retryDelay?: number;
1472
+ }
1473
+
1474
+ interface RemoteConfigSync {
1475
+ // Fetch operations
1476
+ fetch(): Promise<Record<string, unknown>>;
1477
+ fetchWithFallback(): Promise<Record<string, unknown>>;
1478
+
1479
+ // Polling
1480
+ startPolling(): void;
1481
+ stopPolling(): void;
1482
+
1483
+ // Cache
1484
+ getFromCache(): Record<string, unknown> | null;
1485
+ clearCache(): void;
1486
+
1487
+ // Events
1488
+ onUpdate(callback: (config: Record<string, unknown>) => void): () => void;
1489
+ onError(callback: (error: Error) => void): () => void;
1490
+
1491
+ // Status
1492
+ getStatus(): RemoteConfigStatus;
1493
+ }
1494
+
1495
+ interface RemoteConfigStatus {
1496
+ lastFetch: Date | null;
1497
+ lastSuccess: Date | null;
1498
+ lastError: Error | null;
1499
+ isPolling: boolean;
1500
+ cacheAge: number | null;
1501
+ }
1502
+ ```
1503
+
1504
+ ### Implementación
1505
+
1506
+ ```typescript
1507
+ class RemoteConfigSyncImpl implements RemoteConfigSync {
1508
+ private options: Required<RemoteConfigOptions>;
1509
+ private cache: { data: Record<string, unknown>; timestamp: number } | null = null;
1510
+ private pollingTimer: NodeJS.Timer | null = null;
1511
+ private updateCallbacks: Set<Function> = new Set();
1512
+ private errorCallbacks: Set<Function> = new Set();
1513
+ private status: RemoteConfigStatus = {
1514
+ lastFetch: null,
1515
+ lastSuccess: null,
1516
+ lastError: null,
1517
+ isPolling: false,
1518
+ cacheAge: null
1519
+ };
1520
+
1521
+ constructor(options: RemoteConfigOptions) {
1522
+ this.options = {
1523
+ pollingInterval: 0,
1524
+ cacheEnabled: true,
1525
+ cacheTTL: 300000, // 5 minutes
1526
+ fallbackToCache: true,
1527
+ headers: {},
1528
+ timeout: 10000,
1529
+ retryAttempts: 3,
1530
+ retryDelay: 1000,
1531
+ ...options
1532
+ };
1533
+ }
1534
+
1535
+ async fetch(): Promise<Record<string, unknown>> {
1536
+ this.status.lastFetch = new Date();
1537
+
1538
+ let lastError: Error | null = null;
1539
+
1540
+ for (let attempt = 1; attempt <= this.options.retryAttempts; attempt++) {
1541
+ try {
1542
+ const controller = new AbortController();
1543
+ const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);
1544
+
1545
+ const response = await fetch(this.options.url, {
1546
+ headers: this.options.headers,
1547
+ signal: controller.signal
1548
+ });
1549
+
1550
+ clearTimeout(timeoutId);
1551
+
1552
+ if (!response.ok) {
1553
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1554
+ }
1555
+
1556
+ const data = await response.json();
1557
+
1558
+ // Update cache
1559
+ if (this.options.cacheEnabled) {
1560
+ this.cache = {
1561
+ data,
1562
+ timestamp: Date.now()
1563
+ };
1564
+ }
1565
+
1566
+ this.status.lastSuccess = new Date();
1567
+ this.status.lastError = null;
1568
+
1569
+ // Notify listeners
1570
+ this.updateCallbacks.forEach(cb => cb(data));
1571
+
1572
+ return data;
1573
+ } catch (error) {
1574
+ lastError = error as Error;
1575
+
1576
+ if (attempt < this.options.retryAttempts) {
1577
+ await this.delay(this.options.retryDelay * attempt);
1578
+ }
1579
+ }
1580
+ }
1581
+
1582
+ this.status.lastError = lastError;
1583
+ this.errorCallbacks.forEach(cb => cb(lastError));
1584
+ throw lastError;
1585
+ }
1586
+
1587
+ async fetchWithFallback(): Promise<Record<string, unknown>> {
1588
+ try {
1589
+ return await this.fetch();
1590
+ } catch (error) {
1591
+ if (this.options.fallbackToCache && this.cache) {
1592
+ console.warn('Remote config fetch failed, using cached config');
1593
+ return this.cache.data;
1594
+ }
1595
+ throw error;
1596
+ }
1597
+ }
1598
+
1599
+ startPolling(): void {
1600
+ if (this.pollingTimer || this.options.pollingInterval <= 0) {
1601
+ return;
1602
+ }
1603
+
1604
+ this.status.isPolling = true;
1605
+
1606
+ this.pollingTimer = setInterval(async () => {
1607
+ try {
1608
+ await this.fetch();
1609
+ } catch (error) {
1610
+ // Error already handled in fetch()
1611
+ }
1612
+ }, this.options.pollingInterval);
1613
+
1614
+ // Initial fetch
1615
+ this.fetch().catch(() => {});
1616
+ }
1617
+
1618
+ stopPolling(): void {
1619
+ if (this.pollingTimer) {
1620
+ clearInterval(this.pollingTimer);
1621
+ this.pollingTimer = null;
1622
+ }
1623
+ this.status.isPolling = false;
1624
+ }
1625
+
1626
+ getFromCache(): Record<string, unknown> | null {
1627
+ if (!this.cache) return null;
1628
+
1629
+ const age = Date.now() - this.cache.timestamp;
1630
+ if (age > this.options.cacheTTL) {
1631
+ this.cache = null;
1632
+ return null;
1633
+ }
1634
+
1635
+ return this.cache.data;
1636
+ }
1637
+
1638
+ clearCache(): void {
1639
+ this.cache = null;
1640
+ }
1641
+
1642
+ onUpdate(callback: Function): () => void {
1643
+ this.updateCallbacks.add(callback);
1644
+ return () => this.updateCallbacks.delete(callback);
1645
+ }
1646
+
1647
+ onError(callback: Function): () => void {
1648
+ this.errorCallbacks.add(callback);
1649
+ return () => this.errorCallbacks.delete(callback);
1650
+ }
1651
+
1652
+ getStatus(): RemoteConfigStatus {
1653
+ return {
1654
+ ...this.status,
1655
+ cacheAge: this.cache ? Date.now() - this.cache.timestamp : null
1656
+ };
1657
+ }
1658
+
1659
+ private delay(ms: number): Promise<void> {
1660
+ return new Promise(resolve => setTimeout(resolve, ms));
1661
+ }
1662
+ }
1663
+ ```
1664
+
1665
+ ---
1666
+
1667
+ ## 6. Integración con ELSABRO
1668
+
1669
+ ### Inicialización
1670
+
1671
+ ```typescript
1672
+ // config/index.ts
1673
+ import { ConfigManager } from './ConfigManager';
1674
+ import { FeatureFlagService } from './FeatureFlags';
1675
+ import { EnvResolver } from './EnvResolver';
1676
+ import { ConfigValidator } from './ConfigValidator';
1677
+ import { RemoteConfigSync } from './RemoteConfigSync';
1678
+
1679
+ export async function initializeConfig(): Promise<ConfigurationSystem> {
1680
+ // 1. Create config manager
1681
+ const configManager = new ConfigManager({
1682
+ environment: process.env.NODE_ENV || 'development',
1683
+ sources: [
1684
+ { type: 'file', path: '.elsabro/config.json', priority: 1 },
1685
+ { type: 'file', path: '.elsabro/config.local.json', priority: 2, required: false },
1686
+ { type: 'env', path: 'ELSABRO', priority: 3 }
1687
+ ],
1688
+ watchForChanges: true,
1689
+ mergeStrategy: 'deep'
1690
+ });
1691
+
1692
+ await configManager.load();
1693
+
1694
+ // 2. Create validator and validate config
1695
+ const validator = new ConfigValidator();
1696
+ validator.addSchema('elsabro', elsabroConfigSchema);
1697
+
1698
+ const validation = validator.validate(configManager.getAll(), 'elsabro', {
1699
+ coerceTypes: true,
1700
+ useDefaults: true
1701
+ });
1702
+
1703
+ if (!validation.valid) {
1704
+ throw new ConfigValidationError(validation.errors);
1705
+ }
1706
+
1707
+ // 3. Create env resolver
1708
+ const envResolver = new EnvResolver(configManager);
1709
+ const envValidation = await envResolver.validate(configManager.getEnvironment());
1710
+
1711
+ if (!envValidation.valid) {
1712
+ console.warn('Environment validation warnings:', envValidation.warnings);
1713
+ }
1714
+
1715
+ // 4. Create feature flags
1716
+ const featureFlags = new FeatureFlagService(configManager);
1717
+
1718
+ // 5. Setup remote config if enabled
1719
+ let remoteConfig: RemoteConfigSync | null = null;
1720
+ if (configManager.get('remoteConfig.enabled')) {
1721
+ remoteConfig = new RemoteConfigSync({
1722
+ url: configManager.get('remoteConfig.url'),
1723
+ pollingInterval: configManager.get('remoteConfig.pollingInterval', 60000)
1724
+ });
1725
+
1726
+ remoteConfig.onUpdate((config) => {
1727
+ configManager.merge(config);
1728
+ });
1729
+
1730
+ remoteConfig.startPolling();
1731
+ }
1732
+
1733
+ return {
1734
+ config: configManager,
1735
+ flags: featureFlags,
1736
+ env: envResolver,
1737
+ validator,
1738
+ remoteConfig
1739
+ };
1740
+ }
1741
+ ```
1742
+
1743
+ ### Dashboard de Configuración
1744
+
1745
+ ```
1746
+ ┌─────────────────────────────────────────────────────────────────────────────┐
1747
+ │ ELSABRO Configuration Dashboard │
1748
+ ├─────────────────────────────────────────────────────────────────────────────┤
1749
+ │ Environment: production │
1750
+ │ Config Sources: 4 loaded, 0 failed │
1751
+ │ Last Validation: ✓ Valid (2024-01-15 10:30:45) │
1752
+ ├─────────────────────────────────────────────────────────────────────────────┤
1753
+ │ Feature Flags │
1754
+ ├─────────────────┬──────────┬─────────────┬──────────────────────────────────┤
1755
+ │ Flag │ Status │ Targeting │ Rollout │
1756
+ ├─────────────────┼──────────┼─────────────┼──────────────────────────────────┤
1757
+ │ new-agent-ui │ ✓ ON │ All Users │ ████████████████████ 100% │
1758
+ │ beta-tools │ ✓ ON │ Enterprise │ ████████░░░░░░░░░░░░ 40% │
1759
+ │ experimental │ ✗ OFF │ Internal │ ░░░░░░░░░░░░░░░░░░░░ 0% │
1760
+ │ cost-optimizer │ ⏰ SCHED │ All Users │ Starts: 2024-02-01 │
1761
+ └─────────────────┴──────────┴─────────────┴──────────────────────────────────┘
1762
+ │ Remote Config │
1763
+ ├─────────────────────────────────────────────────────────────────────────────┤
1764
+ │ Status: ● Connected │
1765
+ │ Last Sync: 2024-01-15 10:30:00 (45 seconds ago) │
1766
+ │ Polling: Every 60s │
1767
+ │ Cache: Valid (TTL: 4:15 remaining) │
1768
+ └─────────────────────────────────────────────────────────────────────────────┘
1769
+ ```
1770
+
1771
+ ---
1772
+
1773
+ ## 7. Best Practices
1774
+
1775
+ ### Diseño de Configuración
1776
+
1777
+ 1. **Estructura jerárquica clara**: Agrupa configs relacionadas
1778
+ 2. **Valores por defecto sensibles**: Funcionamiento out-of-the-box
1779
+ 3. **Documentación inline**: Descripciones en schema
1780
+ 4. **Validación estricta**: Falla temprano con errores claros
1781
+
1782
+ ### Feature Flags
1783
+
1784
+ 1. **Nombres descriptivos**: `enable-new-checkout` no `flag1`
1785
+ 2. **Kill switches**: Siempre poder desactivar features
1786
+ 3. **Cleanup regular**: Eliminar flags obsoletos
1787
+ 4. **Auditoría**: Log de cambios en flags
1788
+
1789
+ ### Environments
1790
+
1791
+ 1. **Herencia clara**: `prod > staging > development`
1792
+ 2. **Secretos separados**: Nunca en archivos de config
1793
+ 3. **Validación por environment**: Diferentes requerimientos
1794
+ 4. **Documentación de variables**: Qué hace cada una
1795
+
1796
+ ### Remote Config
1797
+
1798
+ 1. **Fallback robusto**: Cache local siempre disponible
1799
+ 2. **Timeouts cortos**: No bloquear startup
1800
+ 3. **Versionamiento**: Rollback fácil
1801
+ 4. **Monitoring**: Alertas de sync failures
1802
+
1803
+ ---
1804
+
1805
+ ## Referencias
1806
+
1807
+ - **REF-001**: Architecture Guide
1808
+ - **REF-006**: Observability & Metrics
1809
+ - **REF-022**: Security System (para integración con SecretsVault)
1810
+ - **REF-023**: Esta referencia (Configuration Management)