@llm-dev-ops/agentics-cli 1.6.8 → 1.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.
@@ -3583,6 +3583,13 @@ export function buildDomainCode(domainModel, tddPlan, language, sparc) {
3583
3583
  kind: 'implementation-prompt',
3584
3584
  });
3585
3585
  }
3586
+ // =========================================================================
3587
+ // ADR-PIPELINE-027: Production Infrastructure Files
3588
+ // Every generated project gets config, logging, errors, health, env example
3589
+ // =========================================================================
3590
+ if (isTs) {
3591
+ files.push(...generateInfrastructureFiles(ROOT_DIR));
3592
+ }
3586
3593
  return {
3587
3594
  files,
3588
3595
  totalAggregates,
@@ -3595,6 +3602,292 @@ export function buildDomainCode(domainModel, tddPlan, language, sparc) {
3595
3602
  };
3596
3603
  }
3597
3604
  // ============================================================================
3605
+ // ADR-PIPELINE-027: Infrastructure File Generators
3606
+ // ============================================================================
3607
+ function generateInfrastructureFiles(rootDir) {
3608
+ const files = [];
3609
+ // --- src/config.ts ---
3610
+ files.push({
3611
+ relativePath: `${rootDir}/src/config.ts`,
3612
+ content: [
3613
+ '/**',
3614
+ ' * Central configuration — all settings from environment variables.',
3615
+ ' * ADR-PIPELINE-027: No hardcoded values in business logic.',
3616
+ ' */',
3617
+ '',
3618
+ 'function env(key: string, fallback: string): string {',
3619
+ ' return process.env[key] ?? fallback;',
3620
+ '}',
3621
+ '',
3622
+ 'function envInt(key: string, fallback: number): number {',
3623
+ ' const v = process.env[key];',
3624
+ ' return v ? parseInt(v, 10) : fallback;',
3625
+ '}',
3626
+ '',
3627
+ 'function envFloat(key: string, fallback: number): number {',
3628
+ ' const v = process.env[key];',
3629
+ ' return v ? parseFloat(v) : fallback;',
3630
+ '}',
3631
+ '',
3632
+ 'export const config = {',
3633
+ ' erp: {',
3634
+ ' baseUrl: env(\'ERP_BASE_URL\', \'\'),',
3635
+ ' timeoutMs: envInt(\'ERP_TIMEOUT_MS\', 30000),',
3636
+ ' maxRetries: envInt(\'ERP_MAX_RETRIES\', 3),',
3637
+ ' },',
3638
+ ' scoring: {',
3639
+ ' // Override via WEIGHT_* environment variables',
3640
+ ' weights: {} as Record<string, number>,',
3641
+ ' },',
3642
+ ' governance: {',
3643
+ ' approvalExpirationHours: envInt(\'APPROVAL_EXPIRATION_HOURS\', 72),',
3644
+ ' maxPendingApprovals: envInt(\'MAX_PENDING_APPROVALS\', 10),',
3645
+ ' requiredApproverRole: env(\'REQUIRED_APPROVER_ROLE\', \'Manager\'),',
3646
+ ' },',
3647
+ ' logging: {',
3648
+ ' level: env(\'LOG_LEVEL\', \'info\') as \'debug\' | \'info\' | \'warn\' | \'error\',',
3649
+ ' },',
3650
+ ' mockMode: env(\'MOCK_MODE\', \'true\') === \'true\',',
3651
+ '} as const;',
3652
+ '',
3653
+ '// Validate ERP URL is set when not in mock mode',
3654
+ 'if (!config.mockMode && !config.erp.baseUrl) {',
3655
+ ' throw new Error(\'ERP_BASE_URL must be set when MOCK_MODE is false\');',
3656
+ '}',
3657
+ '',
3658
+ ].join('\n'),
3659
+ boundedContext: '_infrastructure',
3660
+ kind: 'infrastructure',
3661
+ });
3662
+ // --- src/logger.ts ---
3663
+ files.push({
3664
+ relativePath: `${rootDir}/src/logger.ts`,
3665
+ content: [
3666
+ '/**',
3667
+ ' * Structured JSON logger — ADR-PIPELINE-027.',
3668
+ ' * All output is JSON to stdout for cloud-native log aggregation.',
3669
+ ' */',
3670
+ '',
3671
+ 'import { config } from \'./config.js\';',
3672
+ '',
3673
+ 'type LogLevel = \'debug\' | \'info\' | \'warn\' | \'error\';',
3674
+ 'const LEVELS: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };',
3675
+ '',
3676
+ 'export function createLogger(component: string) {',
3677
+ ' const minLevel = LEVELS[config.logging.level] ?? 1;',
3678
+ '',
3679
+ ' function log(level: LogLevel, message: string, data?: Record<string, unknown>) {',
3680
+ ' if (LEVELS[level] < minLevel) return;',
3681
+ ' const entry = {',
3682
+ ' level,',
3683
+ ' timestamp: new Date().toISOString(),',
3684
+ ' component,',
3685
+ ' message,',
3686
+ ' ...data,',
3687
+ ' };',
3688
+ ' process.stdout.write(JSON.stringify(entry) + \'\\n\');',
3689
+ ' }',
3690
+ '',
3691
+ ' return {',
3692
+ ' debug: (msg: string, data?: Record<string, unknown>) => log(\'debug\', msg, data),',
3693
+ ' info: (msg: string, data?: Record<string, unknown>) => log(\'info\', msg, data),',
3694
+ ' warn: (msg: string, data?: Record<string, unknown>) => log(\'warn\', msg, data),',
3695
+ ' error: (msg: string, data?: Record<string, unknown>) => log(\'error\', msg, data),',
3696
+ ' };',
3697
+ '}',
3698
+ '',
3699
+ ].join('\n'),
3700
+ boundedContext: '_infrastructure',
3701
+ kind: 'infrastructure',
3702
+ });
3703
+ // --- src/errors.ts ---
3704
+ files.push({
3705
+ relativePath: `${rootDir}/src/errors.ts`,
3706
+ content: [
3707
+ '/**',
3708
+ ' * Typed error classes — ADR-PIPELINE-027.',
3709
+ ' * Use these instead of generic Error for structured error handling.',
3710
+ ' */',
3711
+ '',
3712
+ 'export class DomainValidationError extends Error {',
3713
+ ' constructor(message: string, public readonly field: string, public readonly value: unknown) {',
3714
+ ' super(`Validation failed: ${message} (field: ${field})`);',
3715
+ ' this.name = \'DomainValidationError\';',
3716
+ ' }',
3717
+ '}',
3718
+ '',
3719
+ 'export class ERPIntegrationError extends Error {',
3720
+ ' constructor(message: string, public readonly system: string, public readonly statusCode?: number) {',
3721
+ ' super(`ERP integration failed: ${message} (system: ${system})`);',
3722
+ ' this.name = \'ERPIntegrationError\';',
3723
+ ' }',
3724
+ '}',
3725
+ '',
3726
+ 'export class GovernanceViolationError extends Error {',
3727
+ ' constructor(message: string, public readonly requiredRole: string) {',
3728
+ ' super(`Governance violation: ${message} (required role: ${requiredRole})`);',
3729
+ ' this.name = \'GovernanceViolationError\';',
3730
+ ' }',
3731
+ '}',
3732
+ '',
3733
+ 'export class ConfigurationError extends Error {',
3734
+ ' constructor(message: string, public readonly variable: string) {',
3735
+ ' super(`Configuration error: ${message} (variable: ${variable})`);',
3736
+ ' this.name = \'ConfigurationError\';',
3737
+ ' }',
3738
+ '}',
3739
+ '',
3740
+ ].join('\n'),
3741
+ boundedContext: '_infrastructure',
3742
+ kind: 'infrastructure',
3743
+ });
3744
+ // --- src/retry.ts ---
3745
+ files.push({
3746
+ relativePath: `${rootDir}/src/retry.ts`,
3747
+ content: [
3748
+ '/**',
3749
+ ' * Retry with exponential backoff + circuit breaker — ADR-PIPELINE-027.',
3750
+ ' */',
3751
+ '',
3752
+ 'import { config } from \'./config.js\';',
3753
+ 'import { createLogger } from \'./logger.js\';',
3754
+ '',
3755
+ 'const logger = createLogger(\'retry\');',
3756
+ '',
3757
+ 'function isRetryable(err: unknown): boolean {',
3758
+ ' if (err instanceof Error) {',
3759
+ ' // Network errors and 5xx are retryable; 4xx are not',
3760
+ ' const msg = err.message.toLowerCase();',
3761
+ ' if (msg.includes(\'timeout\') || msg.includes(\'econnrefused\') || msg.includes(\'econnreset\')) return true;',
3762
+ ' if (msg.includes(\'5\') && msg.includes(\'00\')) return true; // 500, 502, 503, 504',
3763
+ ' }',
3764
+ ' return false;',
3765
+ '}',
3766
+ '',
3767
+ 'export async function withRetry<T>(fn: () => Promise<T>, label = \'operation\'): Promise<T> {',
3768
+ ' const maxRetries = config.erp.maxRetries;',
3769
+ ' for (let attempt = 0; attempt <= maxRetries; attempt++) {',
3770
+ ' try {',
3771
+ ' return await fn();',
3772
+ ' } catch (err) {',
3773
+ ' if (attempt === maxRetries || !isRetryable(err)) {',
3774
+ ' logger.error(`${label} failed after ${attempt + 1} attempt(s)`, {',
3775
+ ' error: err instanceof Error ? err.message : String(err),',
3776
+ ' });',
3777
+ ' throw err;',
3778
+ ' }',
3779
+ ' const delay = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 500, 10000);',
3780
+ ' logger.warn(`${label} attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms`, {',
3781
+ ' error: err instanceof Error ? err.message : String(err),',
3782
+ ' });',
3783
+ ' await new Promise(resolve => setTimeout(resolve, delay));',
3784
+ ' }',
3785
+ ' }',
3786
+ ' throw new Error(\'Unreachable\');',
3787
+ '}',
3788
+ '',
3789
+ '// Circuit breaker state',
3790
+ 'let failures = 0;',
3791
+ 'let circuitOpenUntil = 0;',
3792
+ 'const FAILURE_THRESHOLD = 5;',
3793
+ 'const RECOVERY_MS = 30000;',
3794
+ '',
3795
+ 'export async function withCircuitBreaker<T>(fn: () => Promise<T>, label = \'operation\'): Promise<T> {',
3796
+ ' if (Date.now() < circuitOpenUntil) {',
3797
+ ' throw new Error(`Circuit breaker OPEN for ${label} — retry after ${new Date(circuitOpenUntil).toISOString()}`);',
3798
+ ' }',
3799
+ ' try {',
3800
+ ' const result = await withRetry(fn, label);',
3801
+ ' failures = 0; // Reset on success',
3802
+ ' return result;',
3803
+ ' } catch (err) {',
3804
+ ' failures++;',
3805
+ ' if (failures >= FAILURE_THRESHOLD) {',
3806
+ ' circuitOpenUntil = Date.now() + RECOVERY_MS;',
3807
+ ' logger.error(`Circuit breaker OPENED for ${label} after ${failures} failures`, {',
3808
+ ' recoveryAt: new Date(circuitOpenUntil).toISOString(),',
3809
+ ' });',
3810
+ ' }',
3811
+ ' throw err;',
3812
+ ' }',
3813
+ '}',
3814
+ '',
3815
+ ].join('\n'),
3816
+ boundedContext: '_infrastructure',
3817
+ kind: 'infrastructure',
3818
+ });
3819
+ // --- src/health.ts ---
3820
+ files.push({
3821
+ relativePath: `${rootDir}/src/health.ts`,
3822
+ content: [
3823
+ '/**',
3824
+ ' * Health check endpoints — ADR-PIPELINE-027.',
3825
+ ' */',
3826
+ '',
3827
+ 'import { config } from \'./config.js\';',
3828
+ '',
3829
+ 'export interface HealthStatus {',
3830
+ ' status: \'ok\' | \'degraded\' | \'unhealthy\';',
3831
+ ' timestamp: string;',
3832
+ ' uptime: number;',
3833
+ ' checks: Record<string, \'ok\' | \'fail\'>;',
3834
+ '}',
3835
+ '',
3836
+ 'export function checkHealth(): HealthStatus {',
3837
+ ' const checks: Record<string, \'ok\' | \'fail\'> = {};',
3838
+ '',
3839
+ ' // ERP connectivity check',
3840
+ ' checks[\'erp_configured\'] = (config.mockMode || config.erp.baseUrl) ? \'ok\' : \'fail\';',
3841
+ '',
3842
+ ' // Config validity',
3843
+ ' checks[\'config_valid\'] = \'ok\';',
3844
+ '',
3845
+ ' const allOk = Object.values(checks).every(c => c === \'ok\');',
3846
+ '',
3847
+ ' return {',
3848
+ ' status: allOk ? \'ok\' : \'degraded\',',
3849
+ ' timestamp: new Date().toISOString(),',
3850
+ ' uptime: process.uptime(),',
3851
+ ' checks,',
3852
+ ' };',
3853
+ '}',
3854
+ '',
3855
+ ].join('\n'),
3856
+ boundedContext: '_infrastructure',
3857
+ kind: 'infrastructure',
3858
+ });
3859
+ // --- .env.example ---
3860
+ files.push({
3861
+ relativePath: `${rootDir}/.env.example`,
3862
+ content: [
3863
+ '# =============================================================================',
3864
+ '# Environment Configuration',
3865
+ '# Copy to .env and fill in values for your deployment.',
3866
+ '# =============================================================================',
3867
+ '',
3868
+ '# ERP Configuration',
3869
+ 'ERP_BASE_URL=https://your-erp-instance.example.com/api/v2',
3870
+ 'ERP_TIMEOUT_MS=30000',
3871
+ 'ERP_MAX_RETRIES=3',
3872
+ '',
3873
+ '# Governance',
3874
+ 'APPROVAL_EXPIRATION_HOURS=72',
3875
+ 'MAX_PENDING_APPROVALS=10',
3876
+ 'REQUIRED_APPROVER_ROLE=Manager',
3877
+ '',
3878
+ '# Logging',
3879
+ 'LOG_LEVEL=info',
3880
+ '',
3881
+ '# Feature Flags',
3882
+ 'MOCK_MODE=true',
3883
+ '',
3884
+ ].join('\n'),
3885
+ boundedContext: '_infrastructure',
3886
+ kind: 'infrastructure',
3887
+ });
3888
+ return files;
3889
+ }
3890
+ // ============================================================================
3598
3891
  // Per-Context Implementation Prompt (ADR-004)
3599
3892
  // ============================================================================
3600
3893
  /**