# @isa/core/config

A lightweight, type-safe configuration management system for Angular applications with runtime validation and nested object access.

## Overview

The Core Config library provides a simple yet powerful configuration service for Angular applications. It enables type-safe access to application configuration data with Zod schema validation, supports nested object navigation with dot notation, and integrates seamlessly with Angular's dependency injection system. The library is designed for runtime configuration that needs to be injected at application startup from external sources like environment files or remote configuration servers.

## Table of Contents

- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Configuration Patterns](#configuration-patterns)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)

## Features

- **Type-safe configuration** - Zod schema validation for runtime safety
- **Nested object access** - Dot notation for deep property paths
- **Dependency injection** - Angular injection token integration
- **Flexible data structure** - Support for primitives, arrays, and objects
- **Array coercion** - Automatic handling of string or array paths
- **Runtime validation** - Validate configuration at runtime with schemas
- **Lightweight** - Minimal dependencies, simple implementation
- **JSON-compatible** - Works with standard JSON configuration data

## Quick Start

### 1. Provide Configuration Data

```typescript
import { ApplicationConfig } from '@angular/core';
import { CONFIG_DATA } from '@isa/core/config';

const appConfig = {
  api: {
    baseUrl: 'https://api.example.com',
    timeout: 30000,
    retries: 3
  },
  features: {
    enableBeta: false,
    maxUploadSize: 10485760
  },
  branding: {
    appName: 'My Application',
    theme: 'light'
  }
};

export const config: ApplicationConfig = {
  providers: [
    {
      provide: CONFIG_DATA,
      useValue: appConfig
    }
  ]
};
```

### 2. Inject and Use Config Service

```typescript
import { Component, inject } from '@angular/core';
import { Config } from '@isa/core/config';
import { z } from 'zod';

@Component({
  selector: 'app-root',
  template: '...'
})
export class AppComponent {
  #config = inject(Config);

  // Access with validation
  apiConfig = this.#config.get(
    'api',
    z.object({
      baseUrl: z.string().url(),
      timeout: z.number().positive(),
      retries: z.number().int().min(0)
    })
  );

  // Access nested property
  appName = this.#config.get('branding.appName', z.string());

  ngOnInit() {
    console.log(`API URL: ${this.apiConfig.baseUrl}`);
    console.log(`App name: ${this.appName}`);
  }
}
```

### 3. Access Without Validation (Deprecated)

```typescript
// Not recommended - use Zod schema instead
const rawValue = this.#config.get('api.baseUrl');  // any type
```

## Core Concepts

### Configuration Data Structure

The configuration data must be JSON-compatible:

```typescript
type JsonPrimitive = string | number | boolean | null | undefined;

type JsonValue =
  | Array<JsonPrimitive>
  | Record<string, JsonPrimitive>
  | JsonPrimitive;
```

**Supported Types:**
- **Primitives**: string, number, boolean, null, undefined
- **Objects**: Records with primitive values
- **Arrays**: Arrays of primitives

**Example Configuration:**
```typescript
const config = {
  // Primitives
  appName: 'ISA Frontend',
  port: 8080,
  debugMode: true,

  // Nested objects
  database: {
    host: 'localhost',
    port: 5432,
    name: 'mydb'
  },

  // Arrays
  allowedOrigins: ['http://localhost:4200', 'https://example.com'],

  // Nested with arrays
  features: {
    enabled: ['auth', 'reporting', 'analytics']
  }
};
```

### Dot Notation Access

Access nested properties using dot notation:

```typescript
// Direct property
this.#config.get('appName', z.string());

// Nested property
this.#config.get('database.host', z.string());

// Deep nesting
this.#config.get('features.beta.settings.maxUsers', z.number());
```

**Path Resolution:**
```typescript
// String path
'database.host'  →  config.database.host

// Array path (equivalent)
['database', 'host']  →  config.database.host

// Mixed array path
['features', 'beta', 'settings']  →  config.features.beta.settings
```

### Zod Schema Validation

All configuration access should use Zod schemas for type safety:

```typescript
import { z } from 'zod';

// Simple primitive
const port = this.#config.get('port', z.number());

// Object with validation
const dbConfig = this.#config.get(
  'database',
  z.object({
    host: z.string().min(1),
    port: z.number().int().positive(),
    name: z.string()
  })
);

// Array with validation
const origins = this.#config.get(
  'allowedOrigins',
  z.array(z.string().url())
);

// Optional values
const optionalFeature = this.#config.get(
  'features.experimental',
  z.string().optional()
);

// Default values
const timeout = this.#config.get(
  'api.timeout',
  z.number().default(30000)
);
```

## API Reference

### CONFIG_DATA

Injection token for providing configuration data.

**Type:** `InjectionToken<JsonValue>`

**Usage:**
```typescript
import { CONFIG_DATA } from '@isa/core/config';

{
  provide: CONFIG_DATA,
  useValue: myConfigObject
}
```

### Config Service

Injectable service for accessing configuration data.

#### `get<TOut>(path: string | string[], zSchema: z.ZodSchema<TOut>): TOut`

Retrieves and validates configuration value at the specified path.

**Parameters:**
- `path: string | string[]` - Path to configuration value (dot notation or array)
- `zSchema: z.ZodSchema<TOut>` - Zod schema for validation and type inference

**Returns:** Validated value of type `TOut`

**Throws:** `ZodError` if validation fails

**Example:**
```typescript
const apiUrl = this.#config.get('api.baseUrl', z.string().url());
// apiUrl is typed as string and validated as URL

const config = this.#config.get(
  'database',
  z.object({
    host: z.string(),
    port: z.number()
  })
);
// config is typed as { host: string; port: number; }
```

#### `get(path: string | string[]): any` (Deprecated)

Retrieves configuration value without validation.

**Parameters:**
- `path: string | string[]` - Path to configuration value

**Returns:** Unvalidated value of type `any`

**Deprecated:** Use the version with Zod schema instead

**Example:**
```typescript
// Not recommended
const value = this.#config.get('some.path');  // Type: any
```

## Usage Examples

### Basic Configuration Access

```typescript
import { Component, inject } from '@angular/core';
import { Config } from '@isa/core/config';
import { z } from 'zod';

@Component({
  selector: 'app-api-client',
  template: '...'
})
export class ApiClientComponent {
  #config = inject(Config);

  // Get string value
  apiBaseUrl = this.#config.get('api.baseUrl', z.string());

  // Get number value
  timeout = this.#config.get('api.timeout', z.number());

  // Get boolean value
  debugMode = this.#config.get('debug', z.boolean());

  ngOnInit() {
    console.log(`Connecting to ${this.apiBaseUrl}`);
    console.log(`Timeout: ${this.timeout}ms`);
    console.log(`Debug: ${this.debugMode}`);
  }
}
```

### Complex Object Configuration

```typescript
import { Component, inject, OnInit } from '@angular/core';
import { Config } from '@isa/core/config';
import { z } from 'zod';

// Define schema
const DatabaseConfigSchema = z.object({
  host: z.string().min(1),
  port: z.number().int().positive(),
  database: z.string().min(1),
  username: z.string(),
  password: z.string(),
  ssl: z.boolean().default(false),
  poolSize: z.number().int().positive().default(10)
});

type DatabaseConfig = z.infer<typeof DatabaseConfigSchema>;

@Component({
  selector: 'app-database',
  template: '...'
})
export class DatabaseComponent implements OnInit {
  #config = inject(Config);

  dbConfig!: DatabaseConfig;

  ngOnInit() {
    this.dbConfig = this.#config.get('database', DatabaseConfigSchema);

    console.log(`Connecting to ${this.dbConfig.host}:${this.dbConfig.port}`);
    console.log(`Database: ${this.dbConfig.database}`);
    console.log(`SSL: ${this.dbConfig.ssl}`);
  }
}
```

### Array Configuration

```typescript
import { Component, inject } from '@angular/core';
import { Config } from '@isa/core/config';
import { z } from 'zod';

@Component({
  selector: 'app-cors-config',
  template: '...'
})
export class CorsConfigComponent {
  #config = inject(Config);

  // Array of strings
  allowedOrigins = this.#config.get(
    'cors.allowedOrigins',
    z.array(z.string().url())
  );

  // Array of numbers
  allowedPorts = this.#config.get(
    'security.allowedPorts',
    z.array(z.number().int().positive())
  );

  isOriginAllowed(origin: string): boolean {
    return this.allowedOrigins.includes(origin);
  }
}
```

### Optional and Default Values

```typescript
import { Component, inject } from '@angular/core';
import { Config } from '@isa/core/config';
import { z } from 'zod';

@Component({
  selector: 'app-features',
  template: '...'
})
export class FeaturesComponent {
  #config = inject(Config);

  // Optional value (may be undefined)
  betaFeature = this.#config.get(
    'features.beta.enabled',
    z.boolean().optional()
  );

  // Value with default
  maxUploadSize = this.#config.get(
    'upload.maxSize',
    z.number().default(10485760)  // 10MB default
  );

  // Optional object
  experimentalConfig = this.#config.get(
    'experimental',
    z.object({
      enabled: z.boolean(),
      features: z.array(z.string())
    }).optional()
  );

  isBetaEnabled(): boolean {
    return this.betaFeature ?? false;
  }
}
```

### Environment-Specific Configuration

```typescript
// environment.ts
export const environment = {
  production: false,
  api: {
    baseUrl: 'http://localhost:3000',
    timeout: 30000
  },
  features: {
    analytics: false,
    debugPanel: true
  }
};

// environment.prod.ts
export const environment = {
  production: true,
  api: {
    baseUrl: 'https://api.production.com',
    timeout: 10000
  },
  features: {
    analytics: true,
    debugPanel: false
  }
};

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { CONFIG_DATA } from '@isa/core/config';
import { environment } from './environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: CONFIG_DATA,
      useValue: environment
    }
  ]
};

// api.service.ts
import { Injectable, inject } from '@angular/core';
import { Config } from '@isa/core/config';
import { z } from 'zod';

@Injectable({ providedIn: 'root' })
export class ApiService {
  #config = inject(Config);

  #apiConfig = this.#config.get(
    'api',
    z.object({
      baseUrl: z.string().url(),
      timeout: z.number()
    })
  );

  get baseUrl(): string {
    return this.#apiConfig.baseUrl;
  }

  get timeout(): number {
    return this.#apiConfig.timeout;
  }
}
```

### Feature Flag Configuration

```typescript
import { Injectable, inject } from '@angular/core';
import { Config } from '@isa/core/config';
import { z } from 'zod';

const FeatureFlagsSchema = z.object({
  newDashboard: z.boolean().default(false),
  advancedSearch: z.boolean().default(false),
  exportToPdf: z.boolean().default(true),
  darkMode: z.boolean().default(false),
  experimentalFeatures: z.array(z.string()).default([])
});

type FeatureFlags = z.infer<typeof FeatureFlagsSchema>;

@Injectable({ providedIn: 'root' })
export class FeatureFlagService {
  #config = inject(Config);

  #flags: FeatureFlags = this.#config.get('features', FeatureFlagsSchema);

  isEnabled(feature: keyof Omit<FeatureFlags, 'experimentalFeatures'>): boolean {
    return this.#flags[feature];
  }

  isExperimentalEnabled(feature: string): boolean {
    return this.#flags.experimentalFeatures.includes(feature);
  }
}

// Usage in component
@Component({
  selector: 'app-dashboard',
  template: `
    @if (featureFlags.isEnabled('newDashboard')) {
      <app-new-dashboard />
    } @else {
      <app-legacy-dashboard />
    }
  `
})
export class DashboardComponent {
  featureFlags = inject(FeatureFlagService);
}
```

### Validation with Custom Error Handling

```typescript
import { Component, inject, OnInit } from '@angular/core';
import { Config } from '@isa/core/config';
import { z, ZodError } from 'zod';

@Component({
  selector: 'app-config-validator',
  template: '...'
})
export class ConfigValidatorComponent implements OnInit {
  #config = inject(Config);

  ngOnInit() {
    try {
      const apiConfig = this.#config.get(
        'api',
        z.object({
          baseUrl: z.string().url('Invalid API URL'),
          timeout: z.number().positive('Timeout must be positive'),
          retries: z.number().int().min(0, 'Retries cannot be negative')
        })
      );

      console.log('Configuration valid:', apiConfig);
    } catch (error) {
      if (error instanceof ZodError) {
        console.error('Configuration validation failed:');
        error.errors.forEach(err => {
          console.error(`  - ${err.path.join('.')}: ${err.message}`);
        });

        // Fallback to defaults
        this.useFallbackConfig();
      }
    }
  }

  private useFallbackConfig() {
    console.warn('Using fallback configuration');
    // Use hardcoded defaults
  }
}
```

## Configuration Patterns

### Typed Configuration Service

Create a dedicated service with typed configuration access:

```typescript
import { Injectable, inject } from '@angular/core';
import { Config } from '@isa/core/config';
import { z } from 'zod';

// Define all schemas
const ApiConfigSchema = z.object({
  baseUrl: z.string().url(),
  timeout: z.number().positive(),
  retries: z.number().int().min(0)
});

const AuthConfigSchema = z.object({
  clientId: z.string(),
  redirectUri: z.string().url(),
  scopes: z.array(z.string())
});

const AppConfigSchema = z.object({
  name: z.string(),
  version: z.string(),
  buildNumber: z.string()
});

// Type exports
export type ApiConfig = z.infer<typeof ApiConfigSchema>;
export type AuthConfig = z.infer<typeof AuthConfigSchema>;
export type AppConfig = z.infer<typeof AppConfigSchema>;

@Injectable({ providedIn: 'root' })
export class AppConfigService {
  #config = inject(Config);

  // Lazy-loaded, cached configuration
  #apiConfig?: ApiConfig;
  #authConfig?: AuthConfig;
  #appConfig?: AppConfig;

  get api(): ApiConfig {
    if (!this.#apiConfig) {
      this.#apiConfig = this.#config.get('api', ApiConfigSchema);
    }
    return this.#apiConfig;
  }

  get auth(): AuthConfig {
    if (!this.#authConfig) {
      this.#authConfig = this.#config.get('auth', AuthConfigSchema);
    }
    return this.#authConfig;
  }

  get app(): AppConfig {
    if (!this.#appConfig) {
      this.#appConfig = this.#config.get('app', AppConfigSchema);
    }
    return this.#appConfig;
  }
}

// Usage
@Component({
  selector: 'app-root',
  template: '...'
})
export class AppComponent {
  appConfig = inject(AppConfigService);

  ngOnInit() {
    console.log(`API: ${this.appConfig.api.baseUrl}`);
    console.log(`Auth: ${this.appConfig.auth.clientId}`);
    console.log(`Version: ${this.appConfig.app.version}`);
  }
}
```

### Dynamic Configuration Loading

Load configuration from external source at startup:

```typescript
import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { CONFIG_DATA } from '@isa/core/config';
import { inject } from '@angular/core';
import { lastValueFrom } from 'rxjs';

function loadConfiguration() {
  const http = inject(HttpClient);

  return async () => {
    try {
      const config = await lastValueFrom(
        http.get<JsonValue>('/assets/config.json')
      );
      return config;
    } catch (error) {
      console.error('Failed to load configuration', error);
      // Return fallback configuration
      return {
        api: { baseUrl: 'http://localhost:3000' }
      };
    }
  };
}

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: () => {
        const loadConfig = loadConfiguration();
        return () => loadConfig().then(config => {
          // Config will be available via CONFIG_DATA
        });
      },
      multi: true
    },
    {
      provide: CONFIG_DATA,
      useFactory: loadConfiguration()
    }
  ]
};
```

### Multi-Environment Configuration

Structured configuration for multiple environments:

```typescript
// config/base.config.ts
export const baseConfig = {
  app: {
    name: 'ISA Frontend',
    version: '1.0.0'
  },
  features: {
    reporting: true,
    analytics: true
  }
};

// config/development.config.ts
import { baseConfig } from './base.config';

export const developmentConfig = {
  ...baseConfig,
  api: {
    baseUrl: 'http://localhost:3000',
    timeout: 60000
  },
  features: {
    ...baseConfig.features,
    debugPanel: true,
    mockData: true
  }
};

// config/production.config.ts
import { baseConfig } from './base.config';

export const productionConfig = {
  ...baseConfig,
  api: {
    baseUrl: 'https://api.production.com',
    timeout: 30000
  },
  features: {
    ...baseConfig.features,
    debugPanel: false,
    mockData: false
  }
};

// app.config.ts
import { CONFIG_DATA } from '@isa/core/config';
import { developmentConfig } from './config/development.config';
import { productionConfig } from './config/production.config';
import { environment } from './environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: CONFIG_DATA,
      useValue: environment.production ? productionConfig : developmentConfig
    }
  ]
};
```

## Testing

The library uses **Jest** for testing.

### Running Tests

```bash
# Run tests for this library
npx nx test core-config --skip-nx-cache

# Run tests with coverage
npx nx test core-config --code-coverage --skip-nx-cache

# Run tests in watch mode
npx nx test core-config --watch
```

### Testing Components with Config

```typescript
import { TestBed } from '@angular/core/testing';
import { Config, CONFIG_DATA } from '@isa/core/config';
import { MyComponent } from './my.component';

describe('MyComponent', () => {
  const mockConfig = {
    api: {
      baseUrl: 'http://test.example.com',
      timeout: 5000
    },
    features: {
      enabled: true
    }
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: CONFIG_DATA,
          useValue: mockConfig
        }
      ]
    });
  });

  it('should access configuration', () => {
    const component = TestBed.createComponent(MyComponent).componentInstance;

    const config = component.getApiConfig();
    expect(config.baseUrl).toBe('http://test.example.com');
    expect(config.timeout).toBe(5000);
  });
});
```

### Testing Config Service Directly

```typescript
import { TestBed } from '@angular/core/testing';
import { Config, CONFIG_DATA } from '@isa/core/config';
import { z } from 'zod';

describe('Config Service', () => {
  let service: Config;

  const testConfig = {
    app: {
      name: 'Test App',
      version: '1.0.0'
    },
    features: {
      enabled: ['feature1', 'feature2']
    }
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: CONFIG_DATA,
          useValue: testConfig
        }
      ]
    });
    service = TestBed.inject(Config);
  });

  it('should retrieve string value', () => {
    const name = service.get('app.name', z.string());
    expect(name).toBe('Test App');
  });

  it('should retrieve nested object', () => {
    const app = service.get(
      'app',
      z.object({
        name: z.string(),
        version: z.string()
      })
    );

    expect(app.name).toBe('Test App');
    expect(app.version).toBe('1.0.0');
  });

  it('should retrieve array', () => {
    const features = service.get('features.enabled', z.array(z.string()));
    expect(features).toEqual(['feature1', 'feature2']);
  });

  it('should throw on validation failure', () => {
    expect(() => {
      service.get('app.version', z.number());  // version is string, not number
    }).toThrow();
  });

  it('should handle missing path gracefully', () => {
    const missing = service.get('nonexistent.path', z.string().optional());
    expect(missing).toBeUndefined();
  });
});
```

### Example Test Suite

```typescript
import { TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from '@jest/globals';
import { Config, CONFIG_DATA } from '@isa/core/config';
import { z } from 'zod';

describe('Config Service - Advanced', () => {
  let service: Config;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: CONFIG_DATA,
          useValue: {
            database: {
              host: 'localhost',
              port: 5432
            },
            timeout: 30000
          }
        }
      ]
    });
    service = TestBed.inject(Config);
  });

  it('should accept string path', () => {
    const host = service.get('database.host', z.string());
    expect(host).toBe('localhost');
  });

  it('should accept array path', () => {
    const host = service.get(['database', 'host'], z.string());
    expect(host).toBe('localhost');
  });

  it('should validate with schema', () => {
    const port = service.get('database.port', z.number().int().positive());
    expect(port).toBe(5432);
  });

  it('should apply default values', () => {
    const retries = service.get('retries', z.number().default(3));
    expect(retries).toBe(3);
  });
});
```

## Architecture Notes

### Current Architecture

Simple, focused architecture:

```
Components/Services
       ↓
  Config Service
       ↓
  CONFIG_DATA (Injection Token)
       ↓
  Application Configuration Object
```

### Design Principles

#### 1. Single Responsibility

The Config service has one job: retrieve configuration values by path.

**Benefits:**
- Easy to understand
- Easy to test
- Easy to maintain

#### 2. Dependency Injection

Uses Angular's DI system for configuration provisioning.

**Benefits:**
- Easy to mock in tests
- Easy to swap implementations
- Runtime configuration injection

#### 3. Type Safety through Validation

Zod schemas provide both runtime validation and compile-time types.

**Benefits:**
- Catch configuration errors early
- Type inference for better DX
- Self-documenting configuration

### Implementation Details

#### Path Resolution

The service uses Angular CDK's `coerceArray` for path handling:

```typescript
import { coerceArray } from '@angular/cdk/coercion';

// Handles both string and array paths
const path = coerceArray('database.host');  // ['database', 'host']
const path2 = coerceArray(['database', 'host']);  // ['database', 'host']
```

#### Nested Property Access

Iterative navigation through object properties:

```typescript
let result: JsonValue = this.#config;

for (const p of path) {
  if (typeof result === 'object' && result !== null && !Array.isArray(result)) {
    result = result[p];
  } else {
    break;  // Can't navigate further
  }

  if (result === null || result === undefined) {
    break;  // Path doesn't exist
  }
}
```

**Behavior:**
- Stops at first non-object or null/undefined
- Returns last valid value found
- Returns undefined if path doesn't exist

### Performance Considerations

1. **No Caching** - Service doesn't cache values (intentional for simplicity)
2. **Path Parsing** - String paths split on every access (minimal overhead)
3. **Validation Cost** - Zod validation runs on every access (consider caching in consuming services)

**Optimization Pattern:**
```typescript
@Injectable({ providedIn: 'root' })
export class ApiService {
  #config = inject(Config);

  // Cache validated config (runs once)
  #apiConfig = this.#config.get('api', ApiConfigSchema);

  // Access cached value (no repeated validation)
  get baseUrl(): string {
    return this.#apiConfig.baseUrl;
  }
}
```

### Known Limitations

#### 1. Shallow JSON Structure Only

**Current State:**
- Only supports JSON-compatible data
- No support for functions, classes, symbols
- Arrays can only contain primitives

**Reason:** Configuration should be serializable (JSON files, API responses)

**Workaround:** Transform configuration after loading if complex types needed

#### 2. No Deep Array Access

**Current State:**
```typescript
// This works
const features = config.get('features.enabled', z.array(z.string()));

// This doesn't work (can't index into arrays by path)
const firstFeature = config.get('features.enabled[0]', z.string());
```

**Workaround:** Retrieve array first, then index in application code

#### 3. No Path Existence Check

**Current State:**
- No dedicated method to check if path exists
- Must attempt retrieval and handle undefined

**Workaround:** Use optional schemas
```typescript
const value = config.get('maybe.exists', z.string().optional());
if (value !== undefined) {
  // Path exists
}
```

### Future Enhancements

Potential improvements identified:

1. **Path Validation** - Compile-time path checking with TypeScript
2. **Configuration Watching** - React to configuration changes
3. **Configuration Merging** - Merge multiple configuration sources
4. **Environment Validation** - Validate entire config object at startup
5. **Path Autocomplete** - IDE autocomplete for configuration paths
6. **Nested Array Access** - Support array indexing in paths
7. **Configuration Schema** - Define complete application configuration schema

### Migration from Untyped Access

Gradual migration path:

```typescript
// Old (untyped)
const url = this.#config.get('api.baseUrl');  // Type: any

// New (typed)
const url = this.#config.get('api.baseUrl', z.string());  // Type: string
```

**Migration Strategy:**
1. Start with critical paths (API URLs, database config)
2. Add schemas incrementally
3. Use optional schemas for uncertain paths
4. Eventually remove deprecated untyped access

## Dependencies

### Required Libraries

- `@angular/core` - Angular framework
- `@angular/cdk/coercion` - Array coercion utilities
- `zod` - Schema validation

### Path Alias

Import from: `@isa/core/config`

## License

Internal ISA Frontend library - not for external distribution.
