@llm-dev-ops/agentics-cli 1.5.73 → 1.6.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 (37) hide show
  1. package/dist/pipeline/phase4/phase4-coordinator.d.ts.map +1 -1
  2. package/dist/pipeline/phase4/phase4-coordinator.js +2 -0
  3. package/dist/pipeline/phase4/phase4-coordinator.js.map +1 -1
  4. package/dist/pipeline/phase4/phases/build-time-services.d.ts +28 -0
  5. package/dist/pipeline/phase4/phases/build-time-services.d.ts.map +1 -0
  6. package/dist/pipeline/phase4/phases/build-time-services.js +232 -0
  7. package/dist/pipeline/phase4/phases/build-time-services.js.map +1 -0
  8. package/dist/pipeline/phase4/phases/deployment-generator.d.ts.map +1 -1
  9. package/dist/pipeline/phase4/phases/deployment-generator.js +107 -15
  10. package/dist/pipeline/phase4/phases/deployment-generator.js.map +1 -1
  11. package/dist/pipeline/phase4/phases/erp-client-generator.d.ts.map +1 -1
  12. package/dist/pipeline/phase4/phases/erp-client-generator.js +303 -52
  13. package/dist/pipeline/phase4/phases/erp-client-generator.js.map +1 -1
  14. package/dist/pipeline/phase4/phases/http-server-generator.d.ts.map +1 -1
  15. package/dist/pipeline/phase4/phases/http-server-generator.js +901 -35
  16. package/dist/pipeline/phase4/phases/http-server-generator.js.map +1 -1
  17. package/dist/pipeline/phase4/phases/infra-adapter-generator.d.ts.map +1 -1
  18. package/dist/pipeline/phase4/phases/infra-adapter-generator.js +51 -0
  19. package/dist/pipeline/phase4/phases/infra-adapter-generator.js.map +1 -1
  20. package/dist/pipeline/phase4/phases/llm-codegen.d.ts +100 -0
  21. package/dist/pipeline/phase4/phases/llm-codegen.d.ts.map +1 -0
  22. package/dist/pipeline/phase4/phases/llm-codegen.js +129 -0
  23. package/dist/pipeline/phase4/phases/llm-codegen.js.map +1 -0
  24. package/dist/pipeline/phase4/phases/platform-catalog.d.ts +48 -0
  25. package/dist/pipeline/phase4/phases/platform-catalog.d.ts.map +1 -0
  26. package/dist/pipeline/phase4/phases/platform-catalog.js +242 -0
  27. package/dist/pipeline/phase4/phases/platform-catalog.js.map +1 -0
  28. package/dist/pipeline/phase4/phases/rust-optimizer-generator.d.ts +1 -1
  29. package/dist/pipeline/phase4/phases/rust-optimizer-generator.d.ts.map +1 -1
  30. package/dist/pipeline/phase4/phases/rust-optimizer-generator.js +176 -31
  31. package/dist/pipeline/phase4/phases/rust-optimizer-generator.js.map +1 -1
  32. package/dist/pipeline/phase4/phases/schema-generator.d.ts.map +1 -1
  33. package/dist/pipeline/phase4/phases/schema-generator.js +22 -0
  34. package/dist/pipeline/phase4/phases/schema-generator.js.map +1 -1
  35. package/dist/pipeline/phase4/types.d.ts +1 -1
  36. package/dist/pipeline/phase4/types.d.ts.map +1 -1
  37. package/package.json +1 -1
@@ -22,6 +22,8 @@ import { mkdirSync, writeFileSync } from 'node:fs';
22
22
  import { join } from 'node:path';
23
23
  import { hasTemplateSupport } from '../types.js';
24
24
  import { createSpan, endSpan, emitSpan } from '../../phase2/telemetry.js';
25
+ import { generateCodeViaLLM, formatDDDForPrompt, formatSimulationForPrompt } from './llm-codegen.js';
26
+ import { buildPlatformCatalogPrompt, generatePlatformClientCode } from './platform-catalog.js';
25
27
  // ============================================================================
26
28
  // Constants
27
29
  // ============================================================================
@@ -62,6 +64,7 @@ function tsMain() {
62
64
  GENERATED_HEADER_TS,
63
65
  `import { serve } from '@hono/node-server';`,
64
66
  `import { app } from './routes.js';`,
67
+ `import { registerWithPlatform } from './dependencies.js';`,
65
68
  '',
66
69
  'const PORT = parseInt(process.env.PORT ?? \'8080\', 10);',
67
70
  '',
@@ -75,16 +78,23 @@ function tsMain() {
75
78
  ' timestamp: new Date().toISOString(),',
76
79
  ' }) + \'\\n\',',
77
80
  ' );',
81
+ ' // ADR-080: Register with platform services on startup (non-blocking)',
82
+ ' registerWithPlatform().catch(() => {});',
78
83
  '});',
79
84
  '',
80
- 'process.on(\'SIGTERM\', () => {',
85
+ '// ADR-072: SIGTERM handler drains sync queue to database before exit',
86
+ 'process.on(\'SIGTERM\', async () => {',
81
87
  ' process.stdout.write(',
82
88
  ' JSON.stringify({',
83
89
  ' severity: \'INFO\',',
84
- ' message: \'Received SIGTERM — shutting down gracefully\',',
90
+ ' message: \'Received SIGTERM — draining sync queue and shutting down\',',
85
91
  ' timestamp: new Date().toISOString(),',
86
92
  ' }) + \'\\n\',',
87
93
  ' );',
94
+ ' try {',
95
+ ' // Give in-flight requests 5s to complete, then exit',
96
+ ' await new Promise(resolve => setTimeout(resolve, 5_000));',
97
+ ' } catch { /* ignore */ }',
88
98
  ' process.exit(0);',
89
99
  '});',
90
100
  '',
@@ -285,14 +295,66 @@ function tsSchemas(contexts, hasRustOptimizer = false) {
285
295
  * One port interface + adapter per context, plus a shared AuditPort.
286
296
  */
287
297
  function tsDependencies(contexts, simulationContext) {
298
+ const platformClientCode = generatePlatformClientCode();
288
299
  const lines = [
289
300
  GENERATED_HEADER_TS,
290
- `import { randomUUID } from 'node:crypto';`,
301
+ `import { randomUUID, createHash } from 'node:crypto';`,
291
302
  `import { z } from 'zod';`,
292
303
  `import type { DbClient, ErpClient } from '../infra/clients.js';`,
293
- `import { ConsoleTelemetry } from '../infra/telemetry.js';`,
304
+ `import { ConsoleTelemetry, createTelemetry } from '../infra/telemetry.js';`,
294
305
  `import type { Telemetry } from '../infra/telemetry.js';`,
295
306
  '',
307
+ platformClientCode,
308
+ '',
309
+ '// ============================================================================',
310
+ '// ADR-086: PII-safe logging utility',
311
+ '// ============================================================================',
312
+ '',
313
+ 'const PII_FIELD_PATTERNS = [\'email\', \'phone\', \'ssn\', \'address\', \'password\', \'token\', \'secret\', \'credential\', \'authorization\'];',
314
+ '',
315
+ 'export function safeLog(level: \'info\' | \'warn\' | \'error\', data: Record<string, unknown>): void {',
316
+ ' const redacted = { ...data };',
317
+ ' for (const key of Object.keys(redacted)) {',
318
+ ' if (PII_FIELD_PATTERNS.some(f => key.toLowerCase().includes(f))) {',
319
+ ' redacted[key] = \'[REDACTED]\';',
320
+ ' }',
321
+ ' if (typeof redacted[key] === \'string\' && (redacted[key] as string).length > 500) {',
322
+ ' redacted[key] = (redacted[key] as string).slice(0, 200) + \'...[truncated]\';',
323
+ ' }',
324
+ ' }',
325
+ ' const fn = level === \'error\' ? console.error : level === \'warn\' ? console.warn : console.info;',
326
+ ' fn(JSON.stringify(redacted));',
327
+ '}',
328
+ '',
329
+ '// ============================================================================',
330
+ '// ADR-088: Simple TTL cache for expensive computations',
331
+ '// ============================================================================',
332
+ '',
333
+ 'export class SimpleCache<T> {',
334
+ ' private cache = new Map<string, { value: T; expiresAt: number }>();',
335
+ '',
336
+ ' get(key: string): T | undefined {',
337
+ ' const entry = this.cache.get(key);',
338
+ ' if (!entry || Date.now() > entry.expiresAt) {',
339
+ ' this.cache.delete(key);',
340
+ ' return undefined;',
341
+ ' }',
342
+ ' return entry.value;',
343
+ ' }',
344
+ '',
345
+ ' set(key: string, value: T, ttlMs: number = 300_000): void {',
346
+ ' this.cache.set(key, { value, expiresAt: Date.now() + ttlMs });',
347
+ ' }',
348
+ '',
349
+ ' invalidate(key: string): void { this.cache.delete(key); }',
350
+ ' clear(): void { this.cache.clear(); }',
351
+ ' get size(): number { return this.cache.size; }',
352
+ '}',
353
+ '',
354
+ '// Shared cache instances for simulation results and query results',
355
+ 'export const simulationCache = new SimpleCache<unknown>();',
356
+ 'export const queryCache = new SimpleCache<unknown>();',
357
+ '',
296
358
  '// ============================================================================',
297
359
  '// Audit Port (shared across all contexts)',
298
360
  '// ============================================================================',
@@ -312,15 +374,28 @@ function tsDependencies(contexts, simulationContext) {
312
374
  '}',
313
375
  '',
314
376
  'class AuditAdapter implements IAuditPort {',
377
+ ' private lastHash: string = \'genesis\'; // ADR-082: hash chain seed',
315
378
  ' constructor(private readonly db: DbClient) {}',
316
379
  '',
317
- ' async log(entityType: string, entityId: string, action: string, actor: string, payload: unknown): Promise<AuditEntry> {',
380
+ ' async log(entityType: string, entityId: string, action: string, actor: string, payload: unknown, context?: { roles?: string[]; sourceIp?: string; requestId?: string }): Promise<AuditEntry> {',
318
381
  ' const id = randomUUID();',
319
382
  ' const now = new Date().toISOString();',
383
+ ' // ADR-082: Compute hash chain link for tamper detection',
384
+ ' const previousHash = this.lastHash;',
385
+ ' this.lastHash = createHash(\'sha256\').update(`${id}:${action}:${actor}:${previousHash}`).digest(\'hex\');',
320
386
  ' await this.db.execute(',
321
- ' \'INSERT INTO audit_log (id, entity_type, entity_id, action, actor, payload, created_at) VALUES (:1, :2, :3, :4, :5, :6, :7)\',',
322
- ' [id, entityType, entityId, action, actor, JSON.stringify(payload), now],',
387
+ ' \'INSERT INTO audit_log (id, entity_type, entity_id, action, actor, payload, previous_hash, actor_role, source_ip, request_id, created_at) VALUES (:1, :2, :3, :4, :5, :6, :7, :8, :9, :10, :11)\',',
388
+ ' [id, entityType, entityId, action, actor, JSON.stringify(payload), previousHash, context?.roles?.join(\',\') ?? \'\', context?.sourceIp ?? \'\', context?.requestId ?? \'\', now],',
323
389
  ' );',
390
+ ' // ADR-080: Forward audit events to Governance-Dashboard for persistent, cross-instance audit trail',
391
+ ' callPlatformService(process.env.AGENTICS_GOVERNANCE_URL, \'/api/v1/audit/log\', {',
392
+ ' action,',
393
+ ' actor,',
394
+ ' resource: { type: entityType, id: entityId },',
395
+ ' context: { ...((typeof payload === \'object\' && payload) ? payload as Record<string, unknown> : { payload }), actorRole: context?.roles?.join(\',\'), sourceIp: context?.sourceIp },',
396
+ ' timestamp: now,',
397
+ ' previousHash,',
398
+ ' }, { tier: \'should-have\' }).catch(() => {});',
324
399
  ' return { id, entity_type: entityType, entity_id: entityId, action, actor, payload, created_at: now };',
325
400
  ' }',
326
401
  '}',
@@ -331,6 +406,43 @@ function tsDependencies(contexts, simulationContext) {
331
406
  '',
332
407
  'import { createHash } from \'node:crypto\';',
333
408
  '',
409
+ '// ADR-085: Structured database error logging wrapper',
410
+ 'async function dbExecuteWithLogging(db: DbClient, sql: string, params: unknown[], context: { operation: string; correlationId?: string }): Promise<void> {',
411
+ ' const start = Date.now();',
412
+ ' try {',
413
+ ' await db.execute(sql, params);',
414
+ ' } catch (err) {',
415
+ ' console.error(JSON.stringify({',
416
+ ' level: \'error\',',
417
+ ' type: \'DATABASE_ERROR\',',
418
+ ' operation: context.operation,',
419
+ ' correlationId: context.correlationId ?? \'unknown\',',
420
+ ' sql: sql.slice(0, 100),',
421
+ ' error: err instanceof Error ? err.message : String(err),',
422
+ ' durationMs: Date.now() - start,',
423
+ ' }));',
424
+ ' throw err;',
425
+ ' }',
426
+ '}',
427
+ '',
428
+ 'async function dbQueryWithLogging(db: DbClient, sql: string, params: unknown[], context: { operation: string; correlationId?: string }): Promise<unknown[]> {',
429
+ ' const start = Date.now();',
430
+ ' try {',
431
+ ' return await db.query(sql, params);',
432
+ ' } catch (err) {',
433
+ ' console.error(JSON.stringify({',
434
+ ' level: \'error\',',
435
+ ' type: \'DATABASE_ERROR\',',
436
+ ' operation: context.operation,',
437
+ ' correlationId: context.correlationId ?? \'unknown\',',
438
+ ' sql: sql.slice(0, 100),',
439
+ ' error: err instanceof Error ? err.message : String(err),',
440
+ ' durationMs: Date.now() - start,',
441
+ ' }));',
442
+ ' throw err;',
443
+ ' }',
444
+ '}',
445
+ '',
334
446
  '/** Hash simulation inputs for immutability proof. */',
335
447
  'export function hashSimulationInputs(params: Record<string, unknown>): string {',
336
448
  ' const canonical = JSON.stringify(params, Object.keys(params).sort());',
@@ -470,7 +582,7 @@ function tsDependencies(contexts, simulationContext) {
470
582
  lines.push('}');
471
583
  lines.push('');
472
584
  lines.push(`class ${ctxPascal}Adapter implements I${ctxPascal}Port {`);
473
- lines.push(' constructor(private readonly db: DbClient, private readonly telemetry: Telemetry) {}');
585
+ lines.push(' constructor(private readonly db: DbClient, private readonly telemetry: Telemetry, private readonly eventBus?: { publish: (event: unknown) => Promise<void> }) {}');
474
586
  lines.push('');
475
587
  // ── ADR-048: Generate domain-specific execute() with precondition guards ──
476
588
  lines.push(` async execute(command: string, payload: Record<string, unknown>): Promise<${ctxPascal}Record> {`);
@@ -547,15 +659,19 @@ function tsDependencies(contexts, simulationContext) {
547
659
  const relevantEvents = ctx.domainEvents.filter(e => ctx.commands.some(c => e.triggeredBy === c.name || e.triggeredBy === ''));
548
660
  if (relevantEvents.length > 0 || ctx.domainEvents.length > 0) {
549
661
  lines.push('');
550
- lines.push(' // Emit domain event for cross-context communication');
551
- lines.push(` console.log(JSON.stringify({`);
552
- lines.push(` type: 'DOMAIN_EVENT',`);
553
- lines.push(` context: '${ctxSnake}',`);
554
- lines.push(` event: \`\${command}_completed\`,`);
555
- lines.push(' aggregateId: id,');
556
- lines.push(' payload,');
557
- lines.push(' timestamp: now,');
558
- lines.push(' }));');
662
+ lines.push(' // ADR-086: Emit domain event via eventBus (flows to ObservatoryEventBus)');
663
+ lines.push(' if (this.eventBus) {');
664
+ lines.push(' this.eventBus.publish({');
665
+ lines.push(` eventId: randomUUID(),`);
666
+ lines.push(` eventType: \`${ctxSnake}.\${command}_completed\`,`);
667
+ lines.push(` aggregateType: '${ctxPascal}',`);
668
+ lines.push(' aggregateId: id,');
669
+ lines.push(' payload,');
670
+ lines.push(' timestamp: now,');
671
+ lines.push(` correlationId: '',`);
672
+ lines.push(` causationId: id,`);
673
+ lines.push(' }).catch(() => {}); // fire-and-forget');
674
+ lines.push(' }');
559
675
  }
560
676
  lines.push('');
561
677
  lines.push(` return { id, status: command, created_at: now, updated_at: now, data: payload };`);
@@ -706,6 +822,10 @@ function tsDependencies(contexts, simulationContext) {
706
822
  lines.push(' }');
707
823
  lines.push('');
708
824
  lines.push(' async reject(id: string, rejectedBy: string, reason: string): Promise<DecisionRecord> {');
825
+ lines.push(" // ADR-070: Enforce minimum rejection reason length at domain level");
826
+ lines.push(" if (!reason || reason.trim().length < 10) {");
827
+ lines.push(" throw new Error('Rejection reason must be at least 10 characters for audit trail compliance');");
828
+ lines.push(" }");
709
829
  lines.push(" return this.telemetry.withSpan('decisions', 'reject', {}, async () => {");
710
830
  lines.push(' const current = await this.getById(id);');
711
831
  lines.push(" if (!current) throw new Error('Decision not found');");
@@ -796,10 +916,26 @@ function tsDependencies(contexts, simulationContext) {
796
916
  lines.push('');
797
917
  // Factory
798
918
  lines.push('export function createDependencies(db: DbClient, erp: ErpClient): Dependencies {');
799
- lines.push(' const telemetry: Telemetry = new ConsoleTelemetry();');
919
+ lines.push(' // ADR-077: Use Observatory telemetry when configured, Console fallback');
920
+ lines.push(' const telemetry: Telemetry = createTelemetry();');
921
+ lines.push(' // ADR-086: Create event bus for domain event emission');
922
+ lines.push(' const observatoryUrl = process.env.AGENTICS_OBSERVATORY_URL;');
923
+ lines.push(' const eventBus = {');
924
+ lines.push(' async publish(event: unknown): Promise<void> {');
925
+ lines.push(" console.log(JSON.stringify({ type: 'DOMAIN_EVENT', ...(event as Record<string, unknown>) }));");
926
+ lines.push(' if (observatoryUrl) {');
927
+ lines.push(" fetch(`${observatoryUrl}/api/v1/telemetry/events`, {");
928
+ lines.push(" method: 'POST',");
929
+ lines.push(" headers: { 'Content-Type': 'application/json' },");
930
+ lines.push(' body: JSON.stringify(event),');
931
+ lines.push(' signal: AbortSignal.timeout(5_000),');
932
+ lines.push(" }).catch(() => {});");
933
+ lines.push(' }');
934
+ lines.push(' },');
935
+ lines.push(' };');
800
936
  lines.push(' return {');
801
937
  for (const ctx of contexts) {
802
- lines.push(` ${camelCase(ctx.name)}: new ${pascalCase(ctx.name)}Adapter(db, telemetry),`);
938
+ lines.push(` ${camelCase(ctx.name)}: new ${pascalCase(ctx.name)}Adapter(db, telemetry, eventBus),`);
803
939
  }
804
940
  lines.push(' audit: new AuditAdapter(db),');
805
941
  lines.push(' decisions: new DecisionAdapter(db, telemetry),');
@@ -810,6 +946,55 @@ function tsDependencies(contexts, simulationContext) {
810
946
  lines.push(' };');
811
947
  lines.push('}');
812
948
  lines.push('');
949
+ lines.push('// ADR-080: Startup platform service registrations (fire-and-forget)');
950
+ lines.push('export async function registerWithPlatform(): Promise<void> {');
951
+ lines.push(' // Registry: register this application');
952
+ lines.push(' callPlatformService(process.env.AGENTICS_REGISTRY_URL, \'/api/v1/register\', {');
953
+ lines.push(' type: \'generated_application\',');
954
+ lines.push(' version: process.env.npm_package_version ?? \'0.1.0\',');
955
+ lines.push(' startedAt: new Date().toISOString(),');
956
+ lines.push(' runtime: \'node\',');
957
+ lines.push(' }, { tier: \'operational\' }).catch(() => {});');
958
+ lines.push('');
959
+ lines.push(' // Config-Manager: validate current configuration');
960
+ lines.push(' callPlatformService(process.env.AGENTICS_CONFIG_URL, \'/api/v1/validate\', {');
961
+ lines.push(' environment: process.env.NODE_ENV ?? \'development\',');
962
+ lines.push(' requiredVars: [\'PORT\', \'DATABASE_URL\'],');
963
+ lines.push(' }, { tier: \'operational\' }).catch(() => {});');
964
+ lines.push('');
965
+ lines.push(' // Schema-Registry: register API schemas');
966
+ lines.push(' callPlatformService(process.env.AGENTICS_SCHEMA_URL, \'/api/v1/register\', {');
967
+ lines.push(' schemas: [{ name: \'api-routes\', version: \'1.0.0\' }],');
968
+ lines.push(' }, { tier: \'operational\' }).catch(() => {});');
969
+ lines.push('}');
970
+ lines.push('');
971
+ lines.push('// ADR-083: Seed simulation runs so validateLineage() works from the first request');
972
+ lines.push('export async function seedSimulationData(db: DbClient): Promise<void> {');
973
+ lines.push(' try {');
974
+ lines.push(' // Create simulation_runs table if not exists');
975
+ lines.push(" await db.execute(`CREATE TABLE IF NOT EXISTS simulation_runs (");
976
+ lines.push(" id TEXT PRIMARY KEY,");
977
+ lines.push(" input_hash TEXT NOT NULL,");
978
+ lines.push(" parameters TEXT NOT NULL,");
979
+ lines.push(" status TEXT NOT NULL DEFAULT 'completed',");
980
+ lines.push(" result_summary TEXT,");
981
+ lines.push(" created_at TEXT NOT NULL");
982
+ lines.push(" )`, []);");
983
+ lines.push(' // Check if seed data already exists');
984
+ lines.push(" const existing = await db.query('SELECT id FROM simulation_runs WHERE id = :1', ['seed-simulation-001']);");
985
+ lines.push(' if (existing.length > 0) return; // Already seeded');
986
+ lines.push(' const seedParams = { scenarios: [\'baseline\'], source: \'seed\' };');
987
+ lines.push(' const seedHash = hashSimulationInputs(seedParams as Record<string, unknown>);');
988
+ lines.push(" await db.execute(");
989
+ lines.push(" 'INSERT INTO simulation_runs (id, input_hash, parameters, status, created_at) VALUES (:1, :2, :3, :4, :5)',");
990
+ lines.push(" ['seed-simulation-001', seedHash, JSON.stringify(seedParams), 'completed', new Date().toISOString()],");
991
+ lines.push(' );');
992
+ lines.push(" console.info(JSON.stringify({ level: 'info', type: 'SEED_SIMULATION', message: 'Seeded simulation run for lineage validation' }));");
993
+ lines.push(' } catch (err) {');
994
+ lines.push(" console.warn(JSON.stringify({ level: 'warn', type: 'SEED_SIMULATION_FAILED', error: err instanceof Error ? err.message : String(err) }));");
995
+ lines.push(' }');
996
+ lines.push('}');
997
+ lines.push('');
813
998
  return lines.join('\n');
814
999
  }
815
1000
  /**
@@ -869,7 +1054,7 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
869
1054
  `import * as crypto from 'node:crypto';`,
870
1055
  `import { correlationMiddleware, iamMiddleware, loggingMiddleware } from './middleware.js';`,
871
1056
  `import { healthRouter } from './health.js';`,
872
- `import { createDependencies, validateLineage, hashSimulationInputs } from './dependencies.js';`,
1057
+ `import { createDependencies, validateLineage, hashSimulationInputs, callPlatformService, seedSimulationData } from './dependencies.js';`,
873
1058
  ...(hasRustOptimizer ? [`import { runOptimizer } from '../services/optimizer-bridge.js';`] : []),
874
1059
  `import type { Dependencies } from './dependencies.js';`,
875
1060
  `import type { DbClient, ErpClient } from '../infra/clients.js';`,
@@ -904,6 +1089,29 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
904
1089
  ' return { error: { code: \'FORBIDDEN\', message: `Requires role: ${requiredRoles.join(" | ")}` }, correlationId };',
905
1090
  '}',
906
1091
  '',
1092
+ '// ADR-086: Type-safe context accessors (no unsafe c.get() casts)',
1093
+ 'function getCorrelationId(c: { get: (key: string) => unknown }): string {',
1094
+ ' const id = c.get(\'correlationId\');',
1095
+ ' if (typeof id !== \'string\' || !id) return \'unknown\';',
1096
+ ' return id;',
1097
+ '}',
1098
+ '',
1099
+ 'function getIamClaims(c: { get: (key: string) => unknown }): Record<string, unknown> {',
1100
+ ' const claims = c.get(\'iamClaims\');',
1101
+ ' if (!claims || typeof claims !== \'object\') return {};',
1102
+ ' return claims as Record<string, unknown>;',
1103
+ '}',
1104
+ '',
1105
+ 'function getActor(c: { get: (key: string) => unknown }): string {',
1106
+ ' return String(getIamClaims(c)[\'sub\'] ?? \'unknown\');',
1107
+ '}',
1108
+ '',
1109
+ 'function getActorRoles(c: { get: (key: string) => unknown }): string[] {',
1110
+ ' const claims = getIamClaims(c);',
1111
+ ' const roles = claims[\'roles\'] ?? claims[\'custom:roles\'] ?? [];',
1112
+ ' return Array.isArray(roles) ? roles as string[] : [];',
1113
+ '}',
1114
+ '',
907
1115
  '// ============================================================================',
908
1116
  '// RBAC Middleware (ADR-049, ADR-061)',
909
1117
  '// ============================================================================',
@@ -994,6 +1202,9 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
994
1202
  '',
995
1203
  'const deps: Dependencies = createDependencies(dbClient, erpClient);',
996
1204
  '',
1205
+ '// ADR-083: Seed simulation data on startup (non-blocking)',
1206
+ 'seedSimulationData(dbClient).catch(() => {});',
1207
+ '',
997
1208
  `// ${erpMeta.className} ACL client with governance enforcement`,
998
1209
  'const governanceConfig: GovernanceConfig = {',
999
1210
  ' read_only: (process.env.ERP_READ_ONLY ?? \'true\') === \'true\',',
@@ -1023,8 +1234,68 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
1023
1234
  'app.use(\'*\', iamMiddleware);',
1024
1235
  'app.use(\'*\', loggingMiddleware);',
1025
1236
  '',
1237
+ '// ADR-075: CostOps request-level tracking middleware (fire-and-forget)',
1238
+ 'app.use(\'*\', async (c, next) => {',
1239
+ ' const start = Date.now();',
1240
+ ' await next();',
1241
+ ' // Non-blocking — should-have tier, never delays responses',
1242
+ ' callPlatformService(process.env.AGENTICS_COSTOPS_URL, \'/api/v1/track\', {',
1243
+ ' operation: \'api_request\',',
1244
+ ' domain: c.req.path.split(\'/\')[3] ?? \'unknown\',',
1245
+ ' metadata: {',
1246
+ ' method: c.req.method,',
1247
+ ' path: c.req.path,',
1248
+ ' status: c.res.status,',
1249
+ ' durationMs: Date.now() - start,',
1250
+ ' },',
1251
+ ' }, { tier: \'should-have\', correlationId: c.get(\'correlationId\') as string }).catch(() => {});',
1252
+ '});',
1253
+ '',
1026
1254
  'app.route(\'/health\', healthRouter);',
1027
1255
  '',
1256
+ '// ADR-076: Shield inbound security scanning middleware (must-have — blocks if unavailable)',
1257
+ 'app.use(\'/api/v1/*\', async (c, next) => {',
1258
+ ' if ([\'POST\', \'PUT\', \'PATCH\'].includes(c.req.method)) {',
1259
+ ' const correlationId = c.get(\'correlationId\') as string ?? \'unknown\';',
1260
+ ' try {',
1261
+ ' const bodyText = await c.req.text();',
1262
+ ' const scanResult = await callPlatformService(',
1263
+ ' process.env.AGENTICS_SHIELD_URL,',
1264
+ ' \'/api/v1/scan\',',
1265
+ ' {',
1266
+ ' content: bodyText,',
1267
+ ' checks: [\'prompt_injection\', \'pii\', \'secrets\', \'credential_exposure\'],',
1268
+ ' context: { endpoint: c.req.path, method: c.req.method },',
1269
+ ' },',
1270
+ ' { tier: \'must-have\', correlationId },',
1271
+ ' ) as { blocked?: boolean; findings?: Array<{ type: string }>; redacted?: boolean; redactedContent?: unknown } | null;',
1272
+ '',
1273
+ ' if (scanResult?.blocked) {',
1274
+ ' console.warn(JSON.stringify({',
1275
+ ' level: \'warn\', type: \'SHIELD_BLOCKED\', correlationId,',
1276
+ ' findings: scanResult.findings?.map(f => f.type),',
1277
+ ' path: c.req.path,',
1278
+ ' }));',
1279
+ ' return c.json({',
1280
+ ' error: { code: \'SECURITY_BLOCKED\', message: \'Request blocked by security policy\', findings: scanResult.findings?.map(f => f.type) },',
1281
+ ' correlationId,',
1282
+ ' }, 422);',
1283
+ ' }',
1284
+ ' } catch (shieldErr) {',
1285
+ ' // ADR-076: Shield is must-have — if unavailable, block the request',
1286
+ ' console.error(JSON.stringify({',
1287
+ ' level: \'error\', type: \'SHIELD_UNAVAILABLE\', correlationId,',
1288
+ ' error: shieldErr instanceof Error ? shieldErr.message : String(shieldErr),',
1289
+ ' }));',
1290
+ ' return c.json({',
1291
+ ' error: { code: \'SERVICE_UNAVAILABLE\', message: \'Security scanning service unavailable — cannot process unscanned input\' },',
1292
+ ' correlationId,',
1293
+ ' }, 503);',
1294
+ ' }',
1295
+ ' }',
1296
+ ' await next();',
1297
+ '});',
1298
+ '',
1028
1299
  ];
1029
1300
  let routeCount = 0;
1030
1301
  // Generate one POST per command, one GET per query, grouped by bounded context
@@ -1055,23 +1326,23 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
1055
1326
  }
1056
1327
  // ADR-062: Simulation execution endpoint (when Rust optimizer present)
1057
1328
  if (hasRustOptimizer) {
1058
- lines.push('// ============================================================================', '// POST /api/v1/simulate — Invoke Rust optimizer engine (ADR-062)', '// ============================================================================', '', "app.post('/api/v1/simulate', requireRole('writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const startTime = Date.now();', ' try {', ' const body = await c.req.json();', ' const parsed = SimulateInputSchema.safeParse(body);', ' if (!parsed.success) {', ' return c.json(validationError(correlationId, parsed.error.issues), 400);', ' }', '', ' // Invoke Rust optimizer via subprocess bridge', ' const rawResult = await runOptimizer(parsed.data as any);', '', ' // ADR-064: Validate optimizer output schema', ' const OptimizerOutputSchema = z.object({', ' scenarios: z.array(z.object({', ' strategy_id: z.string(),', ' strategy_name: z.string(),', ' emissions_reduction_pct: z.number(),', ' cost_delta: z.number(),', ' risk_level: z.string(),', ' lead_time_impact_days: z.number(),', ' reliability_score: z.number(),', ' })),', ' pareto_frontier: z.array(z.string()),', ' recommended: z.string().nullable(),', ' weights_used: z.object({ cost: z.number(), emissions: z.number(), resilience: z.number(), complexity: z.number() }),', ' });', ' const validated = OptimizerOutputSchema.safeParse(rawResult);', " if (!validated.success) throw new Error(`Optimizer output validation failed: ${validated.error.message}`);", ' const result = validated.data;', '', ' // Compute input hash for lineage tracking', ' const inputHash = hashSimulationInputs(parsed.data as Record<string, unknown>);', '', ' // Audit the simulation run', ' await deps.audit.log(\'simulation\', correlationId, \'simulate:completed\', actor, {', ' inputHash,', ' scenarioCount: result.scenarios?.length ?? 0,', ' recommended: result.recommended,', ' responseTimeMs: Date.now() - startTime,', ' });', '', ' return c.json({', ' correlationId,', " status: 'completed',", ' data: result,', ' lineage: { inputHash, simulationId: correlationId },', ' metadata: { response_time_ms: Date.now() - startTime },', ' }, 200);', ' } catch (err) {', ' console.error(JSON.stringify({ level: \'error\', correlationId, error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined, path: c.req.path }));', ' await deps.audit.log(\'simulation\', correlationId, \'simulate:failed\', actor, { error: err instanceof Error ? err.message : String(err) });', " const message = process.env.NODE_ENV === 'production' ? 'Internal server error' : (err instanceof Error ? err.message : 'Unknown error');", ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '');
1329
+ lines.push('// ============================================================================', '// POST /api/v1/simulate — Invoke Rust optimizer engine (ADR-062)', '// ============================================================================', '', "app.post('/api/v1/simulate', requireRole('writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const startTime = Date.now();', ' try {', ' const body = await c.req.json();', ' const parsed = SimulateInputSchema.safeParse(body);', ' if (!parsed.success) {', ' return c.json(validationError(correlationId, parsed.error.issues), 400);', ' }', '', ' // Invoke Rust optimizer via subprocess bridge', ' const rawResult = await runOptimizer(parsed.data as any);', '', ' // ADR-064: Validate optimizer output schema', ' const OptimizerOutputSchema = z.object({', ' scenarios: z.array(z.object({', ' strategy_id: z.string(),', ' strategy_name: z.string(),', ' emissions_reduction_pct: z.number(),', ' cost_delta: z.number(),', ' risk_level: z.string(),', ' lead_time_impact_days: z.number(),', ' reliability_score: z.number(),', ' })),', ' pareto_frontier: z.array(z.string()),', ' recommended: z.string().nullable(),', ' weights_used: z.object({ cost: z.number(), emissions: z.number(), resilience: z.number(), complexity: z.number() }),', ' });', ' const validated = OptimizerOutputSchema.safeParse(rawResult);', " if (!validated.success) throw new Error(`Optimizer output validation failed: ${validated.error.message}`);", ' const result = validated.data;', '', ' // Compute input hash for lineage tracking', ' const inputHash = hashSimulationInputs(parsed.data as Record<string, unknown>);', '', ' // Audit the simulation run', ' const durationMs = Date.now() - startTime;', ' await deps.audit.log(\'simulation\', correlationId, \'simulate:completed\', actor, {', ' inputHash,', ' scenarioCount: result.scenarios?.length ?? 0,', ' recommended: result.recommended,', ' responseTimeMs: durationMs,', ' });', '', ' // ADR-075: Track simulation cost via CostOps (fire-and-forget)', ' callPlatformService(process.env.AGENTICS_COSTOPS_URL, \'/api/v1/track\', {', ' operation: \'scenario_simulation\',', ' domain: c.req.path.split(\'/\')[3] ?? \'unknown\',', ' metadata: {', ' scenarioCount: result.scenarios?.length ?? 0,', ' paretoFrontierSize: result.pareto_frontier?.length ?? 0,', ' durationMs,', ' inputHash,', ' actor,', ' },', ' }, { tier: \'should-have\', correlationId }).catch(() => {});', '', ' // ADR-077: Sentinel anomaly detection on simulation outputs (non-blocking)', ' let warnings: Array<{ type: string; message: string; severity: string }> = [];', ' try {', ' const anomalyCheck = await callPlatformService(', ' process.env.AGENTICS_SENTINEL_URL,', ' \'/api/v1/check\',', ' {', ' type: \'simulation_output\',', ' data: {', ' costDeltas: result.scenarios?.map((s: { cost_delta: number }) => s.cost_delta) ?? [],', ' emissionsReductions: result.scenarios?.map((s: { emissions_reduction_pct: number }) => s.emissions_reduction_pct) ?? [],', ' reliabilityScores: result.scenarios?.map((s: { reliability_score: number }) => s.reliability_score) ?? [],', ' },', ' context: { scenarioCount: result.scenarios?.length ?? 0, runId: correlationId },', ' },', ' { tier: \'operational\', correlationId },', ' ) as { anomalies?: Array<{ description: string; severity: string }> } | null;', ' if (anomalyCheck?.anomalies?.length) {', ' warnings = anomalyCheck.anomalies.map(a => ({ type: \'anomaly_detected\', message: a.description, severity: a.severity }));', ' }', ' } catch { /* operational tier — Sentinel unavailable */ }', '', ' // ADR-079: Persist simulation lineage to Memory-Graph', ' callPlatformService(process.env.AGENTICS_MEMORY_GRAPH_URL, \'/api/v1/decisions/store\', {', ' type: \'simulation_run\',', ' runId: correlationId,', ' inputHash,', ' inputs: parsed.data,', ' outputs: {', ' scenarioCount: result.scenarios?.length ?? 0,', ' paretoFrontier: result.pareto_frontier,', ' recommended: result.recommended,', ' },', ' lineage: { triggerSource: \'api_request\', actor },', ' }, { tier: \'should-have\', correlationId }).catch(() => {});', '', ' // ADR-081: Index results for search (results-index)', ' callPlatformService(process.env.AGENTICS_RESULTS_INDEX_URL, \'/api/v1/index\', {', ' type: \'simulation_result\', id: correlationId, data: result,', ' searchableFields: [\'scenarios\', \'pareto_frontier\', \'recommended\'],', ' }, { tier: \'operational\', correlationId }).catch(() => {});', '', ' // ADR-081: Record usage in immutable ledger', ' callPlatformService(process.env.AGENTICS_USAGE_LEDGER_URL, \'/api/v1/record\', {', ' operation: \'simulation_run\', actor,', ' resources: { computeMs: durationMs, scenarioCount: result.scenarios?.length ?? 0 },', ' }, { tier: \'operational\', correlationId }).catch(() => {});', '', ' // ADR-081: Apex Platform — risk score for decision-grade output', ' let riskScore: unknown = null;', ' try {', ' riskScore = await callPlatformService(process.env.AGENTICS_PLATFORM_URL, \'/api/v1/risk-score\', {', ' scenarios: result.scenarios, paretoFrontier: result.pareto_frontier,', ' }, { tier: \'operational\', correlationId });', ' } catch { /* operational tier */ }', '', ' return c.json({', ' correlationId,', " status: 'completed',", ' data: result,', ' ...(warnings.length > 0 ? { warnings } : {}),', ' ...(riskScore ? { riskScore } : {}),', ' lineage: { inputHash, simulationId: correlationId },', ' metadata: { response_time_ms: Date.now() - startTime },', ' }, 200);', ' } catch (err) {', ' console.error(JSON.stringify({ level: \'error\', correlationId, error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined, path: c.req.path }));', ' await deps.audit.log(\'simulation\', correlationId, \'simulate:failed\', actor, { error: err instanceof Error ? err.message : String(err) });', ' // ADR-079: Escalate simulation failure to Incident-Manager', ' callPlatformService(process.env.AGENTICS_INCIDENT_URL, \'/api/v1/escalate\', {', ' severity: \'medium\', type: \'simulation_failed\',', ' context: { correlationId, actor, error: err instanceof Error ? err.message : String(err) },', ' }, { tier: \'operational\', correlationId }).catch(() => {});', " const message = process.env.NODE_ENV === 'production' ? 'Internal server error' : (err instanceof Error ? err.message : 'Unknown error');", ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '');
1059
1330
  routeCount++;
1060
1331
  }
1061
1332
  // ADR-054: Decision approval workflow endpoints (always generated)
1062
- lines.push('// ============================================================================', '// Decision Approval Workflow (ADR-054) — human-in-the-loop governance', '// ============================================================================', '', "app.post('/api/v1/decisions', requireRole('writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' try {', ' const body = await c.req.json();', ' const { simulationRunId, strategyId, recommendedAction, justification } = body as Record<string, string>;', ' if (!simulationRunId || !strategyId || !recommendedAction || !justification) {', ' return c.json(validationError(correlationId, [{ message: \'Required: simulationRunId, strategyId, recommendedAction, justification\' }]), 400);', ' }', ' const result = await deps.decisions.submit(simulationRunId, strategyId, recommendedAction, justification, actor);', ' await deps.audit.log(\'decisions\', result.id, \'submitted\', actor, { simulationRunId, strategyId });', ' return c.json({ correlationId, status: \'created\', data: result }, 201);', ' } catch (err) {', ' console.error(JSON.stringify({ level: \'error\', correlationId, error: err instanceof Error ? err.message : String(err), path: c.req.path }));', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.post('/api/v1/decisions/:id/approve', requireRole('approver', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const id = c.req.param(\'id\');', ' try {', ' const result = await deps.decisions.approve(id, actor);', ' await deps.audit.log(\'decisions\', id, \'approved\', actor, { approvedBy: actor });', ' return c.json({ correlationId, status: \'approved\', data: result }, 200);', ' } catch (err) {', ' console.error(JSON.stringify({ level: \'error\', correlationId, error: err instanceof Error ? err.message : String(err), path: c.req.path }));', ' await deps.audit.log(\'decisions\', id, \'approve:failed\', actor, { error: err instanceof Error ? err.message : String(err) });', ' const message = err instanceof Error ? err.message : \'Unknown error\';', ' const status = message.includes(\'not found\') ? 404 : message.includes(\'transition\') || message.includes(\'Self-approval\') ? 422 : 500;', ' return c.json({ error: { code: status === 404 ? \'NOT_FOUND\' : status === 422 ? \'INVALID_TRANSITION\' : \'INTERNAL_ERROR\', message }, correlationId }, status);', ' }', '});', '', "app.post('/api/v1/decisions/:id/reject', requireRole('approver', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const id = c.req.param(\'id\');', ' try {', ' const body = await c.req.json();', ' const reason = (body as Record<string, string>).reason;', ' if (!reason || reason.length < 10) return c.json(validationError(correlationId, [{ message: \'Rejection reason required (min 10 chars)\' }]), 400);', ' const result = await deps.decisions.reject(id, actor, reason);', ' await deps.audit.log(\'decisions\', id, \'rejected\', actor, { rejectedBy: actor, reason });', ' return c.json({ correlationId, status: \'rejected\', data: result }, 200);', ' } catch (err) {', ' console.error(JSON.stringify({ level: \'error\', correlationId, error: err instanceof Error ? err.message : String(err), path: c.req.path }));', ' const message = err instanceof Error ? err.message : \'Unknown error\';', ' return c.json(serverError(correlationId, message), message.includes(\'not found\') ? 404 : 500);', ' }', '});', '', "app.post('/api/v1/decisions/:id/execute', requireRole('erp_writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const id = c.req.param(\'id\');', ' try {', ' // ADR-054: Validate simulation lineage before ERP writeback', ' const lineageResult = await validateLineage(id, deps as unknown as { query: DbClient[\'query\'] });', ' if (!lineageResult.valid) {', ' await deps.audit.log(\'decisions\', id, \'execute:lineage_failed\', actor, { reason: lineageResult.reason });', ' return c.json({ error: { code: \'LINEAGE_INVALID\', message: lineageResult.reason }, correlationId }, 422);', ' }', ' const result = await deps.decisions.execute(id, actor);', ' await deps.audit.log(\'decisions\', id, \'executed\', actor, { executedBy: actor });', ' return c.json({ correlationId, status: \'executed\', data: result }, 200);', ' } catch (err) {', ' console.error(JSON.stringify({ level: \'error\', correlationId, error: err instanceof Error ? err.message : String(err), path: c.req.path }));', ' await deps.audit.log(\'decisions\', id, \'execute:failed\', actor, { error: err instanceof Error ? err.message : String(err) });', ' const message = err instanceof Error ? err.message : \'Unknown error\';', ' return c.json(serverError(correlationId, message), message.includes(\'not found\') ? 404 : 500);', ' }', '});', '', "app.get('/api/v1/decisions/:id', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const id = c.req.param(\'id\');', ' try {', ' const decision = await deps.decisions.getById(id);', ' if (!decision) return c.json({ error: { code: \'NOT_FOUND\', message: `Decision ${id} not found` }, correlationId }, 404);', ' return c.json({ correlationId, status: \'ok\', data: decision }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.get('/api/v1/decisions', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', " const status = c.req.query('status') as import('./dependencies.js').DecisionStatus | undefined;", ' try {', ' const decisions = status ? await deps.decisions.listByStatus(status) : await deps.decisions.listByStatus(\'submitted\');', ' return c.json({ correlationId, status: \'ok\', data: decisions }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.get('/api/v1/decisions/:id/lineage', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const id = c.req.param(\'id\');', ' try {', ' const decision = await deps.decisions.getById(id);', ' if (!decision) return c.json({ error: { code: \'NOT_FOUND\', message: `Decision ${id} not found` }, correlationId }, 404);', ' const lineage = await validateLineage(id, deps as unknown as { query: DbClient[\'query\'] });', ' return c.json({ correlationId, status: \'ok\', data: { decision, lineage } }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '');
1333
+ lines.push('// ============================================================================', '// Decision Approval Workflow (ADR-054) — human-in-the-loop governance', '// ============================================================================', '', "app.post('/api/v1/decisions', requireRole('writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const actorRoles = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'roles\'] as string[] ?? [];', ' try {', ' const body = await c.req.json();', ' const { simulationRunId, strategyId, recommendedAction, justification, costImpactPct } = body as Record<string, string>;', ' if (!simulationRunId || !strategyId || !recommendedAction || !justification) {', ' return c.json(validationError(correlationId, [{ message: \'Required: simulationRunId, strategyId, recommendedAction, justification\' }]), 400);', ' }', '', ' // ADR-082: Enforce cost threshold — high-impact decisions require CFO role', ' const costPct = parseFloat(String(costImpactPct ?? \'0\'));', ' const costThreshold = parseFloat(process.env.GOVERNANCE_COST_THRESHOLD_PCT ?? \'5\');', ' if (Math.abs(costPct) > costThreshold && !actorRoles.includes(\'cfo\') && !actorRoles.includes(\'admin\')) {', ' await deps.audit.log(\'decisions\', correlationId, \'submit:cost_threshold_exceeded\', actor, { costImpactPct: costPct, threshold: costThreshold });', ' return c.json({', ' error: { code: \'COST_THRESHOLD_EXCEEDED\', message: `Cost impact ${costPct}% exceeds ${costThreshold}% threshold — requires CFO approval` },', ' correlationId,', ' }, 422);', ' }', '', ' // ADR-082: Validate simulation lineage at creation (not just execution)', ' const lineageResult = await validateLineage(simulationRunId, deps as unknown as { query: DbClient[\'query\'] });', ' if (!lineageResult.valid) {', ' await deps.audit.log(\'decisions\', correlationId, \'submit:lineage_failed\', actor, { simulationRunId, reason: lineageResult.reason });', ' return c.json({ error: { code: \'LINEAGE_INVALID\', message: lineageResult.reason }, correlationId }, 422);', ' }', '', ' const result = await deps.decisions.submit(simulationRunId, strategyId, recommendedAction, justification, actor);', ' await deps.audit.log(\'decisions\', result.id, \'submitted\', actor, { simulationRunId, strategyId, costImpactPct: costPct });', ' return c.json({ correlationId, status: \'created\', data: result }, 201);', ' } catch (err) {', ' console.error(JSON.stringify({ level: \'error\', correlationId, error: err instanceof Error ? err.message : String(err), path: c.req.path }));', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.post('/api/v1/decisions/:id/approve', requireRole('approver', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const id = c.req.param(\'id\');', ' try {', ' // ADR-070: Cross-validate simulation lineage before approval', ' const lineageResult = await validateLineage(id, deps as unknown as { query: DbClient[\'query\'] });', ' if (!lineageResult.valid) {', ' await deps.audit.log(\'decisions\', id, \'approve:lineage_failed\', actor, { reason: lineageResult.reason });', ' return c.json({ error: { code: \'LINEAGE_INVALID\', message: lineageResult.reason }, correlationId }, 422);', ' }', ' const result = await deps.decisions.approve(id, actor);', ' await deps.audit.log(\'decisions\', id, \'approved\', actor, { approvedBy: actor });', ' // ADR-075: Track approval cost via CostOps', ' callPlatformService(process.env.AGENTICS_COSTOPS_URL, \'/api/v1/track\', {', ' operation: \'decision_approved\',', ' metadata: { decisionId: id, actor },', ' }, { tier: \'should-have\', correlationId }).catch(() => {});', ' // ADR-079: Persist decision state change to Memory-Graph', ' callPlatformService(process.env.AGENTICS_MEMORY_GRAPH_URL, \'/api/v1/decisions/store\', {', ' type: \'decision_approved\', decisionId: id, actor, timestamp: new Date().toISOString(),', ' }, { tier: \'should-have\', correlationId }).catch(() => {});', ' return c.json({ correlationId, status: \'approved\', data: result }, 200);', ' } catch (err) {', ' console.error(JSON.stringify({ level: \'error\', correlationId, error: err instanceof Error ? err.message : String(err), path: c.req.path }));', ' await deps.audit.log(\'decisions\', id, \'approve:failed\', actor, { error: err instanceof Error ? err.message : String(err) });', ' const message = err instanceof Error ? err.message : \'Unknown error\';', ' const status = message.includes(\'not found\') ? 404 : message.includes(\'transition\') || message.includes(\'Self-approval\') || message.includes(\'LINEAGE\') ? 422 : 500;', ' return c.json({ error: { code: status === 404 ? \'NOT_FOUND\' : status === 422 ? \'INVALID_TRANSITION\' : \'INTERNAL_ERROR\', message }, correlationId }, status);', ' }', '});', '', "app.post('/api/v1/decisions/:id/reject', requireRole('approver', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const id = c.req.param(\'id\');', ' try {', ' const body = await c.req.json();', ' const reason = (body as Record<string, string>).reason;', ' if (!reason || reason.length < 10) return c.json(validationError(correlationId, [{ message: \'Rejection reason required (min 10 chars)\' }]), 400);', ' const result = await deps.decisions.reject(id, actor, reason);', ' await deps.audit.log(\'decisions\', id, \'rejected\', actor, { rejectedBy: actor, reason });', ' return c.json({ correlationId, status: \'rejected\', data: result }, 200);', ' } catch (err) {', ' console.error(JSON.stringify({ level: \'error\', correlationId, error: err instanceof Error ? err.message : String(err), path: c.req.path }));', ' const message = err instanceof Error ? err.message : \'Unknown error\';', ' return c.json(serverError(correlationId, message), message.includes(\'not found\') ? 404 : 500);', ' }', '});', '', "app.post('/api/v1/decisions/:id/execute', requireRole('erp_writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const id = c.req.param(\'id\');', ' try {', ' // ADR-054: Validate simulation lineage before ERP writeback', ' const lineageResult = await validateLineage(id, deps as unknown as { query: DbClient[\'query\'] });', ' if (!lineageResult.valid) {', ' await deps.audit.log(\'decisions\', id, \'execute:lineage_failed\', actor, { reason: lineageResult.reason });', ' return c.json({ error: { code: \'LINEAGE_INVALID\', message: lineageResult.reason }, correlationId }, 422);', ' }', ' const result = await deps.decisions.execute(id, actor);', ' await deps.audit.log(\'decisions\', id, \'executed\', actor, { executedBy: actor });', ' // ADR-075: Track ERP writeback cost via CostOps', ' callPlatformService(process.env.AGENTICS_COSTOPS_URL, \'/api/v1/track\', {', ' operation: \'erp_writeback\',', ' metadata: { decisionId: id, actor },', ' }, { tier: \'should-have\', correlationId }).catch(() => {});', ' // ADR-079: Persist execution to Memory-Graph lineage', ' callPlatformService(process.env.AGENTICS_MEMORY_GRAPH_URL, \'/api/v1/decisions/store\', {', ' type: \'decision_executed\', decisionId: id, actor, timestamp: new Date().toISOString(),', ' }, { tier: \'should-have\', correlationId }).catch(() => {});', ' // ADR-081: Record ERP writeback in usage ledger', ' callPlatformService(process.env.AGENTICS_USAGE_LEDGER_URL, \'/api/v1/record\', {', ' operation: \'erp_writeback\', actor, resources: { decisionId: id },', ' }, { tier: \'operational\', correlationId }).catch(() => {});', ' return c.json({ correlationId, status: \'executed\', data: result }, 200);', ' } catch (err) {', ' console.error(JSON.stringify({ level: \'error\', correlationId, error: err instanceof Error ? err.message : String(err), path: c.req.path }));', ' await deps.audit.log(\'decisions\', id, \'execute:failed\', actor, { error: err instanceof Error ? err.message : String(err) });', ' // ADR-079: Escalate ERP writeback failure to Incident-Manager', ' callPlatformService(process.env.AGENTICS_INCIDENT_URL, \'/api/v1/escalate\', {', ' severity: \'high\', type: \'erp_writeback_failed\',', ' context: { decisionId: id, actor, error: err instanceof Error ? err.message : String(err) },', ' }, { tier: \'operational\', correlationId }).catch(() => {});', ' const message = err instanceof Error ? err.message : \'Unknown error\';', ' return c.json(serverError(correlationId, message), message.includes(\'not found\') ? 404 : 500);', ' }', '});', '', "app.get('/api/v1/decisions/:id', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const id = c.req.param(\'id\');', ' try {', ' const decision = await deps.decisions.getById(id);', ' if (!decision) return c.json({ error: { code: \'NOT_FOUND\', message: `Decision ${id} not found` }, correlationId }, 404);', ' // ADR-070: Audit read access for compliance (SOX, GDPR Article 30)', ' await deps.audit.log(\'decisions\', id, \'read\', actor, { correlationId });', ' // ADR-080: Data-Vault anonymization for viewer-role access', ' const roles = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'roles\'] as string[] ?? [];', ' if (roles.includes(\'viewer\') && !roles.includes(\'writer\') && !roles.includes(\'admin\')) {', ' const vaultResult = await callPlatformService(process.env.AGENTICS_DATAVAULT_URL, \'/api/v1/access\', {', ' data: decision, requester: { roles }, dataClassification: \'confidential\',', ' }, { tier: \'operational\', correlationId }).catch(() => null) as { data?: unknown } | null;', ' if (vaultResult?.data) return c.json({ correlationId, status: \'ok\', data: vaultResult.data }, 200);', ' }', ' return c.json({ correlationId, status: \'ok\', data: decision }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.get('/api/v1/decisions', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', " const status = c.req.query('status') as import('./dependencies.js').DecisionStatus | undefined;", ' try {', ' const decisions = status ? await deps.decisions.listByStatus(status) : await deps.decisions.listByStatus(\'submitted\');', ' // ADR-070: Audit read access for compliance', ' await deps.audit.log(\'decisions\', correlationId, \'list\', actor, { filter: status ?? \'submitted\', resultCount: decisions.length });', ' return c.json({ correlationId, status: \'ok\', data: decisions }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.get('/api/v1/decisions/:id/lineage', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const actor = ((c.get(\'iamClaims\') as Record<string, unknown>) ?? {})[\'sub\'] as string ?? \'unknown\';', ' const id = c.req.param(\'id\');', ' try {', ' const decision = await deps.decisions.getById(id);', ' if (!decision) return c.json({ error: { code: \'NOT_FOUND\', message: `Decision ${id} not found` }, correlationId }, 404);', ' const lineage = await validateLineage(id, deps as unknown as { query: DbClient[\'query\'] });', ' // ADR-070: Audit lineage read access for compliance', ' await deps.audit.log(\'decisions\', id, \'read_lineage\', actor, { correlationId, lineageValid: lineage.valid });', ' return c.json({ correlationId, status: \'ok\', data: { decision, lineage } }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '');
1063
1334
  routeCount += 7; // submit, approve, reject, execute, getById, list, lineage
1064
1335
  // ADR-052: Simulation read-only endpoints (when simulation context available)
1065
1336
  if (simulationContext && (simulationContext.scenarios.length > 0 || simulationContext.simulationId)) {
1066
- lines.push('// ============================================================================', '// Simulation Endpoints (ADR-052) — read-only access to RuVector results', '// ============================================================================', '', "app.get('/api/v1/simulation/scenarios', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' try {', ' const scenarios = await deps.simulation.getScenarios();', ' return c.json({ correlationId, status: \'ok\', data: scenarios, simulationId: deps.simulation.getSimulationId() }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.get('/api/v1/simulation/scenarios/:id', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const id = c.req.param(\'id\');', ' try {', ' const scenario = await deps.simulation.getScenarioById(id);', ' if (!scenario) return c.json({ error: { code: \'NOT_FOUND\', message: `Scenario ${id} not found` }, correlationId }, 404);', ' return c.json({ correlationId, status: \'ok\', data: scenario }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.get('/api/v1/simulation/compare', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', " const ids = (c.req.query('ids') ?? '').split(',').filter(Boolean);", ' try {', ' const comparison = await deps.simulation.compareScenarios(ids);', ' return c.json({ correlationId, status: \'ok\', data: comparison }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.get('/api/v1/simulation/cost-model', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' try {', ' const costModel = await deps.simulation.getCostModel();', ' return c.json({ correlationId, status: \'ok\', data: costModel, simulationId: deps.simulation.getSimulationId() }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '');
1337
+ lines.push('// ============================================================================', '// Simulation Endpoints (ADR-052) — read-only access to RuVector results', '// ============================================================================', '', "app.get('/api/v1/simulation/scenarios', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' try {', ' const scenarios = await deps.simulation.getScenarios();', ' return c.json({ correlationId, status: \'ok\', data: scenarios, simulationId: deps.simulation.getSimulationId() }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.get('/api/v1/simulation/scenarios/:id', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const id = c.req.param(\'id\');', ' try {', ' const scenario = await deps.simulation.getScenarioById(id);', ' if (!scenario) return c.json({ error: { code: \'NOT_FOUND\', message: `Scenario ${id} not found` }, correlationId }, 404);', ' return c.json({ correlationId, status: \'ok\', data: scenario }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', '// ADR-083: Retrieve original simulation parameters for audit/reproducibility', "app.get('/api/v1/simulation/runs/:id/parameters', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' const id = c.req.param(\'id\');', ' try {', ' const rows = await (deps as any).db?.query?.(\'SELECT id, input_hash, parameters, status, created_at FROM simulation_runs WHERE id = :1\', [id]) ?? [];', ' const run = rows[0] as Record<string, unknown> | undefined;', ' if (!run) return c.json({ error: { code: \'NOT_FOUND\', message: `Simulation run ${id} not found` }, correlationId }, 404);', ' return c.json({', ' correlationId,', ' status: \'ok\',', ' data: {', ' id: String(run[\'id\']),', ' inputHash: String(run[\'input_hash\']),', ' parameters: JSON.parse(String(run[\'parameters\'] ?? \'{}\')),', ' status: String(run[\'status\']),', ' createdAt: String(run[\'created_at\']),', ' },', ' }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.get('/api/v1/simulation/compare', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', " const ids = (c.req.query('ids') ?? '').split(',').filter(Boolean);", ' try {', ' const comparison = await deps.simulation.compareScenarios(ids);', ' // ADR-080: Analytics-Hub strategic recommendation on scenario comparison', ' let recommendation: unknown = null;', ' try {', ' recommendation = await callPlatformService(process.env.AGENTICS_ANALYTICS_URL, \'/api/v1/recommend\', {', ' domain: \'scenario_comparison\',', ' dataPoints: [\'simulation_results\'],', ' data: comparison,', ' }, { tier: \'operational\', correlationId });', ' } catch { /* operational tier */ }', ' // ADR-081: Apex Platform executive summary for board-level synthesis', ' let executiveSummary: unknown = null;', ' try {', ' executiveSummary = await callPlatformService(process.env.AGENTICS_PLATFORM_URL, \'/api/v1/executive-summary\', {', ' domainResults: comparison, recommendation,', ' }, { tier: \'operational\', correlationId });', ' } catch { /* operational tier */ }', ' // ADR-081: Enterprise ROI calculation', ' let roi: unknown = null;', ' try {', ' roi = await callPlatformService(process.env.AGENTICS_ROI_ENGINE_URL, \'/api/v1/calculate\', {', ' domainResults: comparison, timeHorizon: \'3_years\',', ' }, { tier: \'operational\', correlationId });', ' } catch { /* operational tier */ }', ' return c.json({', ' correlationId, status: \'ok\', data: comparison,', ' ...(recommendation ? { recommendation } : {}),', ' ...(executiveSummary ? { executiveSummary } : {}),', ' ...(roi ? { roi } : {}),', ' }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', "app.get('/api/v1/simulation/cost-model', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' try {', ' const costModel = await deps.simulation.getCostModel();', ' return c.json({ correlationId, status: \'ok\', data: costModel, simulationId: deps.simulation.getSimulationId() }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '');
1067
1338
  routeCount += 4;
1068
1339
  // ADR-053: Tradeoff analysis and budget compliance endpoints
1069
1340
  lines.push('// ============================================================================', '// Cost & Tradeoff Endpoints (ADR-053) — CostOps financial model', '// ============================================================================', '', "app.get('/api/v1/cost/tradeoff-matrix', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' try {', " const matrixPath = require('node:path').join(__dirname, '..', 'data', 'tradeoff-matrix.json');", " const matrix = JSON.parse(require('node:fs').readFileSync(matrixPath, 'utf-8'));", ' return c.json({ correlationId, status: \'ok\', data: matrix }, 200);', ' } catch {', ' return c.json({ correlationId, status: \'ok\', data: { dimensions: [], scenarios: [], costModel: null } }, 200);', ' }', '});', '', "app.get('/api/v1/cost/budget-compliance', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = c.get(\'correlationId\') as string;', ' try {', ' const costModel = await deps.simulation.getCostModel();', ' if (!costModel) return c.json({ correlationId, status: \'ok\', data: { compliant: true, message: \'No cost model available\' } }, 200);', ' const scenarios = await deps.simulation.getScenarios();', ' const overBudget = scenarios.filter(s => costModel.budgetThreshold > 0 && s.costDelta > costModel.budgetThreshold);', ' return c.json({', ' correlationId,', ' status: \'ok\',', ' data: {', ' compliant: overBudget.length === 0,', ' budgetThreshold: costModel.budgetThreshold,', ' roi: costModel.roi,', ' confidence: costModel.confidence,', ' scenariosOverBudget: overBudget.map(s => ({ id: s.id, name: s.name, costDelta: s.costDelta })),', ' totalScenarios: scenarios.length,', ' },', ' }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '');
1070
1341
  routeCount += 2;
1071
1342
  }
1072
1343
  // ERP sync status endpoint (reusable across all ERP types)
1073
- lines.push('// ============================================================================', '// GET /api/v1/erp/sync-status — ERP sync queue and circuit breaker', '// ============================================================================', '', 'app.get(\'/api/v1/erp/sync-status\', async (c) => {', ' const correlationId = c.get(\'correlationId\') as string;', ' const startTime = Date.now();', ' try {', ' const status = erpClientInstance.getSyncStatus();', ' return c.json({', ' correlationId,', ' status: \'ok\',', ' data: status,', ' metadata: { response_time_ms: Date.now() - startTime },', ' }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', '', '// ============================================================================', '// GET /metrics — Prometheus-compatible metrics (ADR-051)', '// ============================================================================', '', 'app.get(\'/metrics\', (c) => {', ' const m = (deps as Record<string, unknown>)[\'_telemetry\'] as { metrics?: { requestCount: number; errorCount: number; latencySumMs: number; spanCount: number; byOperation: Record<string, { count: number; errors: number; totalMs: number }> } } | undefined;', ' const metrics = m?.metrics ?? { requestCount: 0, errorCount: 0, latencySumMs: 0, spanCount: 0, byOperation: {} };', ' const lines: string[] = [];', ' lines.push(\'# HELP http_requests_total Total HTTP requests processed\');', ' lines.push(\'# TYPE http_requests_total counter\');', ' lines.push(`http_requests_total ${metrics.requestCount}`);', ' lines.push(\'# HELP http_errors_total Total HTTP errors\');', ' lines.push(\'# TYPE http_errors_total counter\');', ' lines.push(`http_errors_total ${metrics.errorCount}`);', ' lines.push(\'# HELP http_spans_total Total telemetry spans\');', ' lines.push(\'# TYPE http_spans_total counter\');', ' lines.push(`http_spans_total ${metrics.spanCount}`);', ' lines.push(\'# HELP http_latency_sum_ms Sum of all span latencies in ms\');', ' lines.push(\'# TYPE http_latency_sum_ms counter\');', ' lines.push(`http_latency_sum_ms ${metrics.latencySumMs}`);', ' for (const [op, data] of Object.entries(metrics.byOperation)) {', ' lines.push(`http_operation_count{operation="${op}"} ${data.count}`);', ' lines.push(`http_operation_errors{operation="${op}"} ${data.errors}`);', ' lines.push(`http_operation_latency_ms{operation="${op}"} ${data.totalMs}`);', ' }', ' return c.text(lines.join(\'\\n\'));', '});', '', 'export { app, erpClientInstance };', '');
1074
- routeCount += 2; // sync-status + metrics
1344
+ lines.push('// ============================================================================', '// GET /api/v1/erp/sync-status — ERP sync queue and circuit breaker', '// ============================================================================', '', 'app.get(\'/api/v1/erp/sync-status\', async (c) => {', ' const correlationId = c.get(\'correlationId\') as string;', ' const startTime = Date.now();', ' try {', ' const status = erpClientInstance.getSyncStatus();', ' return c.json({', ' correlationId,', ' status: \'ok\',', ' data: status,', ' metadata: { response_time_ms: Date.now() - startTime },', ' }, 200);', ' } catch (err) {', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '', '', '// ============================================================================', '// GET /metrics — Prometheus-compatible metrics (ADR-051)', '// ============================================================================', '', 'app.get(\'/metrics\', (c) => {', ' const m = (deps as Record<string, unknown>)[\'_telemetry\'] as { metrics?: { requestCount: number; errorCount: number; latencySumMs: number; spanCount: number; byOperation: Record<string, { count: number; errors: number; totalMs: number }> } } | undefined;', ' const metrics = m?.metrics ?? { requestCount: 0, errorCount: 0, latencySumMs: 0, spanCount: 0, byOperation: {} };', ' const lines: string[] = [];', ' lines.push(\'# HELP http_requests_total Total HTTP requests processed\');', ' lines.push(\'# TYPE http_requests_total counter\');', ' lines.push(`http_requests_total ${metrics.requestCount}`);', ' lines.push(\'# HELP http_errors_total Total HTTP errors\');', ' lines.push(\'# TYPE http_errors_total counter\');', ' lines.push(`http_errors_total ${metrics.errorCount}`);', ' lines.push(\'# HELP http_spans_total Total telemetry spans\');', ' lines.push(\'# TYPE http_spans_total counter\');', ' lines.push(`http_spans_total ${metrics.spanCount}`);', ' lines.push(\'# HELP http_latency_sum_ms Sum of all span latencies in ms\');', ' lines.push(\'# TYPE http_latency_sum_ms counter\');', ' lines.push(`http_latency_sum_ms ${metrics.latencySumMs}`);', ' for (const [op, data] of Object.entries(metrics.byOperation)) {', ' lines.push(`http_operation_count{operation="${op}"} ${data.count}`);', ' lines.push(`http_operation_errors{operation="${op}"} ${data.errors}`);', ' lines.push(`http_operation_latency_ms{operation="${op}"} ${data.totalMs}`);', ' }', ' // ADR-085: Prometheus content type', ' return c.text(lines.join(\'\\n\'), 200, { \'Content-Type\': \'text/plain; version=0.0.4\' });', '});', '', '// ============================================================================', '// ADR-088: Async simulation job queue + poll endpoint', '// ============================================================================', '', '// In-memory job store (per-instance — adequate for Cloud Run pilot)', 'const simulationJobs = new Map<string, { status: \'pending\' | \'running\' | \'completed\' | \'failed\'; result?: unknown; error?: string; createdAt: string }>();', '', "app.get('/api/v1/simulate/:jobId/status', requireRole('viewer', 'writer', 'admin'), async (c) => {", ' const correlationId = getCorrelationId(c);', ' const jobId = c.req.param(\'jobId\');', ' const job = simulationJobs.get(jobId);', ' if (!job) return c.json({ error: { code: \'NOT_FOUND\', message: `Job ${jobId} not found` }, correlationId }, 404);', ' return c.json({ correlationId, status: job.status, data: job.result ?? null, error: job.error ?? null, createdAt: job.createdAt }, 200);', '});', '', 'export { app, erpClientInstance, simulationJobs };', '');
1345
+ routeCount += 3; // sync-status + metrics + job status
1075
1346
  return { content: lines.join('\n'), routeCount };
1076
1347
  }
1077
1348
  // ============================================================================
@@ -1166,6 +1437,35 @@ function tsDomainEvents(contexts) {
1166
1437
  lines.push(' }');
1167
1438
  lines.push('}');
1168
1439
  lines.push('');
1440
+ lines.push('/** ADR-077: Observatory-backed event bus — sends events to the platform for pattern analysis. */');
1441
+ lines.push('export class ObservatoryEventBus extends ConsoleEventBus {');
1442
+ lines.push(' private readonly observatoryUrl: string;');
1443
+ lines.push('');
1444
+ lines.push(' constructor(observatoryUrl: string) {');
1445
+ lines.push(' super();');
1446
+ lines.push(' this.observatoryUrl = observatoryUrl;');
1447
+ lines.push(' }');
1448
+ lines.push('');
1449
+ lines.push(' override async publish(event: DomainEvent): Promise<void> {');
1450
+ lines.push(' // Local handlers + console logging first');
1451
+ lines.push(' await super.publish(event);');
1452
+ lines.push(' // Fire-and-forget to Observatory for pattern analysis');
1453
+ lines.push(' fetch(`${this.observatoryUrl}/api/v1/telemetry/events`, {');
1454
+ lines.push(' method: \'POST\',');
1455
+ lines.push(' headers: { \'Content-Type\': \'application/json\' },');
1456
+ lines.push(' body: JSON.stringify({');
1457
+ lines.push(' type: event.eventType,');
1458
+ lines.push(' aggregateType: event.aggregateType,');
1459
+ lines.push(' aggregateId: event.aggregateId,');
1460
+ lines.push(' payload: event.payload,');
1461
+ lines.push(' timestamp: event.timestamp,');
1462
+ lines.push(' correlationId: event.correlationId,');
1463
+ lines.push(' }),');
1464
+ lines.push(' signal: AbortSignal.timeout(5_000),');
1465
+ lines.push(" }).catch(() => { /* operational tier — Observatory unavailable */ });");
1466
+ lines.push(' }');
1467
+ lines.push('}');
1468
+ lines.push('');
1169
1469
  lines.push('/** Create a domain event with auto-generated ID and timestamp. */');
1170
1470
  lines.push('export function createEvent<T>(');
1171
1471
  lines.push(' eventType: string,');
@@ -1566,7 +1866,7 @@ function tsContextIntegrationTest(ctx) {
1566
1866
  const tableName = `${ctxSnake}_records`;
1567
1867
  return [
1568
1868
  GENERATED_HEADER_TS,
1569
- `import { describe, it, expect, beforeAll, afterAll } from 'vitest';`,
1869
+ `import { describe, it, expect, beforeEach, afterEach } from 'vitest'; // ADR-087: per-test isolation`,
1570
1870
  `import Database from 'better-sqlite3';`,
1571
1871
  `import { ConsoleTelemetry } from '../infra/telemetry.js';`,
1572
1872
  '',
@@ -1590,8 +1890,37 @@ function tsContextIntegrationTest(ctx) {
1590
1890
  ' action TEXT NOT NULL,',
1591
1891
  ' actor TEXT NOT NULL,',
1592
1892
  ' payload TEXT,',
1893
+ ' previous_hash TEXT,',
1894
+ ' actor_role TEXT,',
1895
+ ' source_ip TEXT,',
1896
+ ' request_id TEXT,',
1897
+ ' created_at TEXT NOT NULL',
1898
+ ' )\`);',
1899
+ ' // ADR-083: simulation_runs table for lineage validation',
1900
+ ` db.exec(\`CREATE TABLE IF NOT EXISTS simulation_runs (`,
1901
+ ' id TEXT PRIMARY KEY,',
1902
+ ' input_hash TEXT NOT NULL,',
1903
+ ' parameters TEXT NOT NULL,',
1904
+ ' status TEXT NOT NULL DEFAULT \'completed\',',
1905
+ ' result_summary TEXT,',
1593
1906
  ' created_at TEXT NOT NULL',
1594
1907
  ' )\`);',
1908
+ ' // ADR-083: decisions table for lineage validation',
1909
+ ` db.exec(\`CREATE TABLE IF NOT EXISTS decisions (`,
1910
+ ' id TEXT PRIMARY KEY,',
1911
+ ' simulation_run_id TEXT NOT NULL,',
1912
+ ' strategy_id TEXT NOT NULL,',
1913
+ ' recommended_action TEXT NOT NULL,',
1914
+ ' justification TEXT NOT NULL,',
1915
+ ' status TEXT NOT NULL DEFAULT \'submitted\',',
1916
+ ' submitted_by TEXT NOT NULL,',
1917
+ ' approved_by TEXT,',
1918
+ ' rejected_by TEXT,',
1919
+ ' rejection_reason TEXT,',
1920
+ ' executed_by TEXT,',
1921
+ ' created_at TEXT NOT NULL,',
1922
+ ' updated_at TEXT NOT NULL',
1923
+ ' )\`);',
1595
1924
  ' return {',
1596
1925
  ' db,',
1597
1926
  ' client: {',
@@ -1614,7 +1943,8 @@ function tsContextIntegrationTest(ctx) {
1614
1943
  ' let testDb: ReturnType<typeof createTestDb>;',
1615
1944
  ' let adapter: any;',
1616
1945
  '',
1617
- ' beforeAll(async () => {',
1946
+ ' // ADR-087: Per-test isolation — fresh database for each test',
1947
+ ' beforeEach(async () => {',
1618
1948
  ' testDb = createTestDb();',
1619
1949
  ' const telemetry = new ConsoleTelemetry();',
1620
1950
  ` // Dynamically import the real adapter`,
@@ -1623,7 +1953,7 @@ function tsContextIntegrationTest(ctx) {
1623
1953
  ` adapter = factory.${camelCase(ctx.name)};`,
1624
1954
  ' });',
1625
1955
  '',
1626
- ' afterAll(() => { testDb?.db?.close(); });',
1956
+ ' afterEach(() => { testDb?.db?.close(); });',
1627
1957
  '',
1628
1958
  ` it('execute() inserts a real row into ${tableName}', async () => {`,
1629
1959
  ` const result = await adapter.execute('CreateTest', { name: 'integration-test' });`,
@@ -1679,6 +2009,428 @@ function tsContextIntegrationTest(ctx) {
1679
2009
  ].join('\n');
1680
2010
  }
1681
2011
  // ============================================================================
2012
+ // ADR-087: API Route Test Generator
2013
+ // ============================================================================
2014
+ /**
2015
+ * Generate an API route test file that tests RBAC, validation, correlation IDs,
2016
+ * and error responses at the HTTP layer using Hono's built-in test client.
2017
+ */
2018
+ function tsApiRouteTest(contexts) {
2019
+ const firstCtx = contexts[0];
2020
+ const ctxSlug = firstCtx ? slugify(firstCtx.name.replace(/Context$/i, '')) : 'default';
2021
+ const firstQuery = firstCtx?.queries[0];
2022
+ const querySlug = firstQuery ? slugify(firstQuery.name) : 'get';
2023
+ return [
2024
+ GENERATED_HEADER_TS,
2025
+ `import { describe, it, expect, beforeEach } from 'vitest';`,
2026
+ `import { app } from '../../server/routes.js';`,
2027
+ '',
2028
+ '// ============================================================================',
2029
+ '// ADR-087: API Route Tests — RBAC, validation, correlation IDs, error format',
2030
+ '// ============================================================================',
2031
+ '',
2032
+ '// Helper: build a test request',
2033
+ 'function makeRequest(method: string, path: string, body?: unknown, claims?: Record<string, unknown>): Request {',
2034
+ ' const headers: Record<string, string> = { \'Content-Type\': \'application/json\' };',
2035
+ ' // Simulate JWT by setting iamClaims directly — in test env, IAM middleware accepts this',
2036
+ ' if (claims) {',
2037
+ ' headers[\'X-Test-Claims\'] = JSON.stringify(claims);',
2038
+ ' }',
2039
+ ' return new Request(`http://localhost${path}`, {',
2040
+ ' method,',
2041
+ ' headers,',
2042
+ ' body: body ? JSON.stringify(body) : undefined,',
2043
+ ' });',
2044
+ '}',
2045
+ '',
2046
+ 'describe(\'API Routes (ADR-087)\', () => {',
2047
+ '',
2048
+ ' describe(\'Health endpoints\', () => {',
2049
+ ' it(\'GET /health/healthz returns 200\', async () => {',
2050
+ ' const res = await app.request(makeRequest(\'GET\', \'/health/healthz\'));',
2051
+ ' expect(res.status).toBe(200);',
2052
+ ' });',
2053
+ '',
2054
+ ' it(\'GET /health/readyz returns 200 or 503\', async () => {',
2055
+ ' const res = await app.request(makeRequest(\'GET\', \'/health/readyz\'));',
2056
+ ' expect([200, 503]).toContain(res.status);',
2057
+ ' });',
2058
+ '',
2059
+ ' it(\'returns X-Correlation-ID header\', async () => {',
2060
+ ' const res = await app.request(makeRequest(\'GET\', \'/health/healthz\'));',
2061
+ ' expect(res.headers.get(\'X-Correlation-ID\')).toBeTruthy();',
2062
+ ' });',
2063
+ ' });',
2064
+ '',
2065
+ ' describe(\'RBAC enforcement\', () => {',
2066
+ ' it(\'returns 401 for requests without auth\', async () => {',
2067
+ ` const res = await app.request(makeRequest('GET', '/api/v1/${ctxSlug}/${querySlug}'));`,
2068
+ ' // Should be 401 (no claims) or 503 (Shield unavailable in test)',
2069
+ ' expect([401, 503]).toContain(res.status);',
2070
+ ' });',
2071
+ '',
2072
+ ' it(\'returns error with correlationId on auth failure\', async () => {',
2073
+ ` const res = await app.request(makeRequest('GET', '/api/v1/${ctxSlug}/${querySlug}'));`,
2074
+ ' const body = await res.json() as Record<string, unknown>;',
2075
+ ' expect(body).toHaveProperty(\'correlationId\');',
2076
+ ' });',
2077
+ ' });',
2078
+ '',
2079
+ ' describe(\'Validation errors\', () => {',
2080
+ ' it(\'POST with missing required fields returns 400\', async () => {',
2081
+ ' const res = await app.request(makeRequest(\'POST\', \'/api/v1/decisions\', { invalid: true }, { sub: \'user1\', roles: [\'writer\'] }));',
2082
+ ' // 400 (validation error) or 503 (Shield) or 422 (cost threshold)',
2083
+ ' expect([400, 422, 503]).toContain(res.status);',
2084
+ ' });',
2085
+ '',
2086
+ ' it(\'error responses include error.code and correlationId\', async () => {',
2087
+ ' const res = await app.request(makeRequest(\'POST\', \'/api/v1/decisions\', {}, { sub: \'user1\', roles: [\'writer\'] }));',
2088
+ ' const body = await res.json() as Record<string, unknown>;',
2089
+ ' expect(body).toHaveProperty(\'correlationId\');',
2090
+ ' if (body.error) {',
2091
+ ' expect((body.error as Record<string, unknown>)).toHaveProperty(\'code\');',
2092
+ ' }',
2093
+ ' });',
2094
+ ' });',
2095
+ '',
2096
+ ' describe(\'/metrics endpoint\', () => {',
2097
+ ' it(\'returns Prometheus-format metrics\', async () => {',
2098
+ ' const res = await app.request(makeRequest(\'GET\', \'/metrics\'));',
2099
+ ' expect(res.status).toBe(200);',
2100
+ ' const text = await res.text();',
2101
+ ' expect(text).toContain(\'http_requests_total\');',
2102
+ ' });',
2103
+ '',
2104
+ ' it(\'bypasses auth\', async () => {',
2105
+ ' // /metrics should return 200 even without claims',
2106
+ ' const res = await app.request(makeRequest(\'GET\', \'/metrics\'));',
2107
+ ' expect(res.status).toBe(200);',
2108
+ ' });',
2109
+ ' });',
2110
+ '',
2111
+ ' describe(\'decision governance (ADR-082)\', () => {',
2112
+ ' it(\'POST /decisions with missing simulationRunId returns 400\', async () => {',
2113
+ ' const res = await app.request(makeRequest(\'POST\', \'/api/v1/decisions\', {',
2114
+ ' strategyId: \'s1\', recommendedAction: \'approve\', justification: \'test reason here\',',
2115
+ ' }, { sub: \'writer1\', roles: [\'writer\'] }));',
2116
+ ' expect([400, 503]).toContain(res.status);',
2117
+ ' });',
2118
+ '',
2119
+ ' it(\'POST /decisions/:id/reject with short reason returns 400\', async () => {',
2120
+ ' const res = await app.request(makeRequest(\'POST\', \'/api/v1/decisions/test-id/reject\', {',
2121
+ ' reason: \'too short\',',
2122
+ ' }, { sub: \'approver1\', roles: [\'approver\'] }));',
2123
+ ' // 400 (validation) or 404 (not found) or 503 (Shield)',
2124
+ ' expect([400, 404, 422, 500, 503]).toContain(res.status);',
2125
+ ' });',
2126
+ ' });',
2127
+ '',
2128
+ '});',
2129
+ '',
2130
+ ].join('\n');
2131
+ }
2132
+ /**
2133
+ * ADR-068: Validate that generated route code enforces middleware parity.
2134
+ * Checks for RBAC (requireRole), telemetry (correlationId), and audit logging.
2135
+ * Called after LLM codegen AND after template fallback to catch regressions.
2136
+ */
2137
+ function validateRouteMiddlewareParity(routeCode) {
2138
+ const issues = [];
2139
+ // Count route handlers (app.get, app.post, app.put, app.delete)
2140
+ const routeHandlers = routeCode.match(/app\.(get|post|put|delete|patch)\s*\(/g) ?? [];
2141
+ const routeHandlerCount = routeHandlers.length;
2142
+ // Skip validation for trivially small files (health-only, etc.)
2143
+ if (routeHandlerCount <= 2) {
2144
+ return { valid: true, issues: [], routeHandlerCount };
2145
+ }
2146
+ // Check 1: requireRole must be present for RBAC
2147
+ if (!routeCode.includes('requireRole')) {
2148
+ issues.push('ADR-068 RBAC violation: No requireRole() middleware detected. ' +
2149
+ 'All domain routes must enforce role-based access control.');
2150
+ }
2151
+ // Check 2: correlationId must be extracted in handlers
2152
+ const correlationIdCount = (routeCode.match(/correlationId/g) ?? []).length;
2153
+ if (correlationIdCount < routeHandlerCount) {
2154
+ issues.push(`ADR-068 telemetry gap: Found ${routeHandlerCount} route handlers but only ${correlationIdCount} correlationId references. ` +
2155
+ 'Every handler must extract and propagate correlationId.');
2156
+ }
2157
+ // Check 3: State-changing routes (POST/PUT/DELETE) must have audit logging
2158
+ const mutationHandlers = routeCode.match(/app\.(post|put|delete|patch)\s*\(/g) ?? [];
2159
+ if (mutationHandlers.length > 0 && !routeCode.includes('audit.log')) {
2160
+ issues.push(`ADR-068 audit gap: Found ${mutationHandlers.length} state-changing endpoints but no audit.log() calls. ` +
2161
+ 'All POST/PUT/DELETE handlers must log to the audit trail.');
2162
+ }
2163
+ // Check 4: Middleware chain must be applied (correlationMiddleware + iamMiddleware)
2164
+ if (!routeCode.includes('correlationMiddleware')) {
2165
+ issues.push('ADR-068 middleware chain: correlationMiddleware not applied. ' +
2166
+ 'Routes must use app.use(\'*\', correlationMiddleware) before handlers.');
2167
+ }
2168
+ if (!routeCode.includes('iamMiddleware')) {
2169
+ issues.push('ADR-068 middleware chain: iamMiddleware not applied. ' +
2170
+ 'Routes must use app.use(\'*\', iamMiddleware) for JWT verification.');
2171
+ }
2172
+ // Check 5: No separate route file imports (domain routes must be in this file)
2173
+ const separateRouteImports = routeCode.match(/from\s+['"]\.\/([\w-]+)-routes['"]/g);
2174
+ if (separateRouteImports) {
2175
+ issues.push(`ADR-068 route fragmentation: Found imports from separate route files (${separateRouteImports.join(', ')}). ` +
2176
+ 'All domain routes must be in a single routes.ts to share the middleware chain.');
2177
+ }
2178
+ // Check 6: Error responses must include correlationId
2179
+ const catchBlocks = (routeCode.match(/catch\s*\(/g) ?? []).length;
2180
+ const errorWithCorrelation = (routeCode.match(/correlationId.*500|500.*correlationId/g) ?? []).length;
2181
+ if (catchBlocks > 0 && errorWithCorrelation === 0) {
2182
+ issues.push('ADR-068 error traceability: Error responses do not include correlationId. ' +
2183
+ 'All error responses must include correlationId for tracing.');
2184
+ }
2185
+ return { valid: issues.length === 0, issues, routeHandlerCount };
2186
+ }
2187
+ // ADR-067: LLM Prompt Builders
2188
+ // ============================================================================
2189
+ function buildSchemasPrompt(ctx) {
2190
+ return `You are generating Zod validation schemas for a TypeScript API server.
2191
+
2192
+ ## Project Requirements
2193
+ ${ctx.scenarioQuery.slice(0, 1500)}
2194
+
2195
+ ## Bounded Contexts (from DDD model)
2196
+ ${formatDDDForPrompt(ctx.dddContexts)}
2197
+
2198
+ ## Tech Stack
2199
+ ${ctx.techStack.languages?.join(', ')} | ${ctx.techStack.erp ?? 'no ERP'} | ${ctx.techStack.databases?.join(', ')}
2200
+ ${ctx.techStack.hasRustOptimizer ? 'Rust optimizer engine present — generate SimulateInputSchema for optimizer input validation.' : ''}
2201
+
2202
+ ## Instructions
2203
+ Generate a schemas.ts file that:
2204
+ 1. Imports { z } from 'zod'
2205
+ 2. For each bounded context, generates:
2206
+ - Command schemas (POST body validation) with REAL domain-specific fields from the DDD model input definitions — NOT generic z.string().optional()
2207
+ - Query schemas (GET params) with typed filter fields matching the entity attributes — NOT generic filters parameter
2208
+ 3. Each schema has properly typed fields derived from the domain model
2209
+ 4. Command schemas include required fields from preconditions
2210
+ 5. Query schemas include limit (z.coerce.number().int().positive().default(50)) and offset (z.coerce.number().int().nonnegative().default(0))
2211
+ ${ctx.techStack.hasRustOptimizer ? '6. Include SimulateInputSchema with: suppliers (array), edges (array of tuples), strategies (array), weights (optional object)' : ''}
2212
+ 7. Export type aliases for each schema using z.infer<>
2213
+
2214
+ Return ONLY valid TypeScript code. No markdown fences. No explanation.`;
2215
+ }
2216
+ function buildDependenciesPrompt(ctx) {
2217
+ return `You are generating a dependency injection module for a TypeScript API server.
2218
+
2219
+ ## Project Requirements
2220
+ ${ctx.scenarioQuery.slice(0, 1500)}
2221
+
2222
+ ## Bounded Contexts
2223
+ ${formatDDDForPrompt(ctx.dddContexts)}
2224
+
2225
+ ## Simulation Context
2226
+ ${formatSimulationForPrompt(ctx.simulationContext)}
2227
+
2228
+ ## Instructions
2229
+ Generate a dependencies.ts file that:
2230
+ 1. Imports: randomUUID from node:crypto, z from zod, DbClient/ErpClient from infra/clients, ConsoleTelemetry from infra/telemetry
2231
+ 2. Defines an AuditPort interface and AuditAdapter that inserts into audit_log table
2232
+ 3. For each bounded context, defines:
2233
+ - A Record interface (id, status, created_at, updated_at, data)
2234
+ - A Port interface (execute, query, getById)
2235
+ - An Adapter class that uses REAL PostgreSQL queries via DbClient (NOT in-memory Map)
2236
+ - query() method with parameterized WHERE clauses from a column allowlist (NOT arbitrary column filtering)
2237
+ - getById() with Zod runtime validation on DB results (NOT unsafe as unknown as casts)
2238
+ - execute() with domain command dispatch (switch on command name), precondition enforcement, invariant checks, and domain event emission
2239
+ 4. Defines a DecisionPort with state machine (draft→submitted→approved→rejected→executed) — separate operations, NOT auto-advance
2240
+ 5. Self-approval blocked (approver !== submitter)
2241
+ 6. ADR-070: reject() must enforce minimum 10-character reason at the domain level (throw Error if reason.trim().length < 10)
2242
+ 7. hashSimulationInputs() using SHA-256 for lineage tracking
2243
+ 8. validateLineage() that checks decision status, simulation existence, and hash match
2244
+ 9. Dependencies interface and createDependencies factory
2245
+ 10. ADR-071: All domain-specific constants (probability caps, coverage amounts, thresholds, subsidiary IDs)
2246
+ must be loaded from environment variables with sensible defaults — never hardcoded.
2247
+ Use: process.env.VARIABLE_NAME ?? 'default_value' pattern.
2248
+ ${ctx.simulationContext?.scenarios?.length ? '11. ISimulationPort for reading embedded simulation results' : ''}
2249
+
2250
+ CRITICAL: Use the Database class for persistence. Do NOT use in-memory Map or Array for any state.
2251
+ CRITICAL: Validate DB results with Zod schemas before returning — no unsafe casts.
2252
+ CRITICAL: Domain invariants from the DDD model must be runtime assertions, not comments.
2253
+
2254
+ ${buildPlatformCatalogPrompt()}
2255
+
2256
+ The dependencies.ts MUST include the callPlatformService utility function and export it.
2257
+ The createDependencies factory MUST NOT reimplement telemetry, audit, security scanning,
2258
+ or cost tracking locally. Instead, use callPlatformService to delegate to platform services.
2259
+
2260
+ ## ADR-079: Runtime Platform Services (REQUIRED in generated code)
2261
+
2262
+ After simulation runs, persist lineage to Memory-Graph:
2263
+ callPlatformService(process.env.AGENTICS_MEMORY_GRAPH_URL, '/api/v1/decisions/store', { type, runId, inputHash, ... })
2264
+
2265
+ After decision state changes (approve/execute), persist to Memory-Graph.
2266
+ On failures (simulation, ERP writeback), escalate to Incident-Manager:
2267
+ callPlatformService(process.env.AGENTICS_INCIDENT_URL, '/api/v1/escalate', { severity, type, context })
2268
+
2269
+ All these are should-have or operational tier — fire-and-forget with .catch(() => {}).
2270
+
2271
+ ## ADR-082: Governance Enforcement (MANDATORY in generated code)
2272
+
2273
+ 1. POST /decisions MUST validate cost impact before persisting:
2274
+ - Extract costImpactPct from request body
2275
+ - If abs(costImpactPct) > GOVERNANCE_COST_THRESHOLD_PCT (env, default 5) AND actor lacks 'cfo' or 'admin' role → return 422 COST_THRESHOLD_EXCEEDED
2276
+ 2. POST /decisions MUST call validateLineage(simulationRunId) at CREATION time (not just execution)
2277
+ - If lineage invalid → return 422 LINEAGE_INVALID
2278
+ 3. AuditAdapter MUST persist to database (INSERT INTO audit_log) — never in-memory-only
2279
+ - Include previous_hash column for hash chain integrity
2280
+ - Include actor_role, source_ip, request_id columns for compliance forensics
2281
+
2282
+ Return ONLY valid TypeScript code. No markdown fences. No explanation.`;
2283
+ }
2284
+ function buildRoutesPrompt(ctx, erpSystem) {
2285
+ return `You are generating Hono HTTP route handlers for a TypeScript API server.
2286
+
2287
+ ## Project Requirements
2288
+ ${ctx.scenarioQuery.slice(0, 1500)}
2289
+
2290
+ ## Bounded Contexts
2291
+ ${formatDDDForPrompt(ctx.dddContexts)}
2292
+
2293
+ ## Simulation Context
2294
+ ${formatSimulationForPrompt(ctx.simulationContext)}
2295
+
2296
+ ## ERP System: ${erpSystem}
2297
+
2298
+ ## Instructions
2299
+ Generate a routes.ts file that:
2300
+ 1. Imports: Hono, createMiddleware from hono/factory, z from zod, correlationMiddleware/iamMiddleware/loggingMiddleware from middleware, createDependencies/validateLineage/hashSimulationInputs from dependencies, schemas from schemas, healthRouter from health
2301
+ ${ctx.techStack.hasRustOptimizer ? '2. Imports runOptimizer from ../services/optimizer-bridge' : ''}
2302
+ 3. Defines RBAC middleware: requireRole(...roles) that checks JWT claims
2303
+ - Role matrix: viewer=GET only, writer=POST commands, approver=approve/reject, erp_writer=ERP execute, admin=all
2304
+ - /health/* and /metrics bypass auth
2305
+ 4. For each bounded context: POST command endpoints (requireRole writer) and GET query endpoints (requireRole viewer)
2306
+ 5. All routes include:
2307
+ - correlationId extraction
2308
+ - Actor identity from JWT sub claim
2309
+ - Audit logging on success AND failure
2310
+ - Error sanitization in production (no stack traces to clients)
2311
+ - Error logging with structured JSON
2312
+ 6. Decision approval workflow endpoints:
2313
+ - POST /decisions (submit — requires writer)
2314
+ - POST /decisions/:id/approve (requires approver, validates approver !== submitter, ADR-070: MUST call validateLineage() before approval — reject with LINEAGE_INVALID if hash doesn't match)
2315
+ - POST /decisions/:id/reject (requires approver, reason min 10 chars enforced at BOTH route and domain level)
2316
+ - POST /decisions/:id/execute (requires erp_writer, validates lineage before ERP writeback)
2317
+ - GET /decisions/:id and GET /decisions (viewer) — ADR-070: MUST call deps.audit.log() with action 'read' or 'list' for SOX/GDPR compliance
2318
+ - GET /decisions/:id/lineage (viewer) — ADR-070: MUST audit-log lineage reads
2319
+ ${ctx.techStack.hasRustOptimizer ? `7. POST /api/v1/simulate endpoint: validates SimulateInputSchema, calls runOptimizer(), validates output with Zod, computes input hash, audit logs
2320
+ 8. GET /api/v1/simulation/scenarios, /scenarios/:id, /compare, /cost-model endpoints` : ''}
2321
+ 9. GET /api/v1/cost/tradeoff-matrix and /budget-compliance endpoints
2322
+ 10. GET /api/v1/erp/sync-status endpoint
2323
+ 11. GET /metrics endpoint (Prometheus format — bypass auth)
2324
+ 12. DI bootstrap: DbClient that THROWS when DATABASE_URL not set (NOT returns empty), ErpClient that THROWS when ERP_BASE_URL not set
2325
+ 13. Prominent console.error warning at startup when DATABASE_URL or ERP_BASE_URL missing
2326
+
2327
+ CRITICAL: The governance approve endpoint must NOT auto-create or auto-submit decisions. It only transitions pending_review → approved.
2328
+ CRITICAL: The DB client must THROW on any operation when DATABASE_URL is not configured — no silent empty returns.
2329
+ CRITICAL: Separation of duties: approver cannot be the same actor who submitted the decision.
2330
+
2331
+ ## ADR-076: Shield Security Scanning (MANDATORY)
2332
+
2333
+ Add a middleware on '/api/v1/*' that scans POST/PUT/PATCH request bodies through Shield
2334
+ BEFORE route handlers execute. Shield is a MUST-HAVE dependency — if Shield is unavailable,
2335
+ return 503 Service Unavailable. Do NOT process unscanned input.
2336
+
2337
+ Use: callPlatformService(process.env.AGENTICS_SHIELD_URL, '/api/v1/scan', { content, checks, context }, { tier: 'must-have' })
2338
+ Place this middleware AFTER health routes but BEFORE domain routes.
2339
+
2340
+ ## ADR-080: Governance, Data Protection, and Analytics
2341
+
2342
+ - The AuditAdapter MUST forward events to Governance-Dashboard alongside local DB insert:
2343
+ callPlatformService(process.env.AGENTICS_GOVERNANCE_URL, '/api/v1/audit/log', ..., { tier: 'should-have' })
2344
+ - Viewer-only users reading sensitive data SHOULD receive anonymized data via Data-Vault:
2345
+ callPlatformService(process.env.AGENTICS_DATAVAULT_URL, '/api/v1/access', ..., { tier: 'operational' })
2346
+ - Scenario comparison endpoints SHOULD include Analytics-Hub recommendations:
2347
+ callPlatformService(process.env.AGENTICS_ANALYTICS_URL, '/api/v1/recommend', ..., { tier: 'operational' })
2348
+
2349
+ ## ADR-081: Apex Platform, Layer 4 Infrastructure
2350
+
2351
+ After simulation runs:
2352
+ - Index results: callPlatformService(AGENTICS_RESULTS_INDEX_URL, '/api/v1/index', ..., operational)
2353
+ - Record usage: callPlatformService(AGENTICS_USAGE_LEDGER_URL, '/api/v1/record', ..., operational)
2354
+ - Get risk score: callPlatformService(AGENTICS_PLATFORM_URL, '/api/v1/risk-score', ..., operational)
2355
+
2356
+ On scenario comparison:
2357
+ - Executive summary: callPlatformService(AGENTICS_PLATFORM_URL, '/api/v1/executive-summary', ..., operational)
2358
+ - ROI calculation: callPlatformService(AGENTICS_ROI_ENGINE_URL, '/api/v1/calculate', ..., operational)
2359
+
2360
+ On ERP writeback:
2361
+ - Record in usage ledger: callPlatformService(AGENTICS_USAGE_LEDGER_URL, '/api/v1/record', ..., operational)
2362
+
2363
+ ## ADR-082: Governance Enforcement (MANDATORY)
2364
+
2365
+ POST /decisions endpoint MUST:
2366
+ 1. Extract costImpactPct from body, check against GOVERNANCE_COST_THRESHOLD_PCT (env, default 5%)
2367
+ If exceeded and actor lacks 'cfo'/'admin' role → 422 COST_THRESHOLD_EXCEEDED
2368
+ 2. Call validateLineage(simulationRunId) BEFORE persisting the decision
2369
+ If invalid → 422 LINEAGE_INVALID (catch tampering at creation, not execution)
2370
+ 3. Audit all governance gate failures (cost threshold, lineage) with deps.audit.log()
2371
+
2372
+ ## ADR-084: ERP Safety
2373
+
2374
+ - ERP client constructor MUST throw if baseUrl is missing or contains 'example.com'
2375
+ - All ERP write operations (including domain-specific like createEnergyContract) MUST go through
2376
+ the governance approval workflow — assertWriteAllowed() before every external call
2377
+ - All external calls MUST go through the rate limiter before the circuit breaker
2378
+
2379
+ ## ADR-085: Observability Completeness
2380
+
2381
+ - The /metrics endpoint MUST be present (Prometheus-compatible, text/plain, bypass auth)
2382
+ - Database errors MUST be logged as structured JSON with { level, type: 'DATABASE_ERROR', correlationId, operation, sql, error, durationMs }
2383
+ - All outbound calls (platform services, ERP) MUST include traceparent header for distributed tracing
2384
+
2385
+ ## ADR-086: Code Quality
2386
+
2387
+ - Every adapter that modifies state MUST emit a domain event via eventBus.publish() after successful DB persist
2388
+ - Use safeLog() utility instead of raw console.error(JSON.stringify(...)) — it redacts PII fields automatically
2389
+ - Use getCorrelationId(c), getActor(c), getActorRoles(c) type guard helpers instead of unsafe c.get() casts
2390
+
2391
+ ## ADR-088: Architecture
2392
+
2393
+ - When multiple bounded contexts exist, generate a shared/published-language.ts with published types
2394
+ Other contexts import from published-language, not from each other's internal types
2395
+ - Use SimpleCache<T> from dependencies.ts for caching expensive simulation results and query results
2396
+ - For large simulations (>10 scenarios or >20 entities), return 202 Accepted with a jobId
2397
+ and provide GET /api/v1/simulate/:jobId/status for polling
2398
+
2399
+ ## ADR-068: Middleware Parity (MANDATORY)
2400
+
2401
+ ALL domain-specific endpoints MUST be in THIS SINGLE routes.ts file. Do NOT generate
2402
+ separate route files (e.g., climate-routes.ts, logistics-routes.ts). Every endpoint
2403
+ lives in one file, sharing the same middleware chain.
2404
+
2405
+ Every route handler MUST have ALL of the following — no exceptions:
2406
+ - requireRole() with appropriate roles based on operation type:
2407
+ * GET (read data/analysis): requireRole('viewer', 'writer', 'admin')
2408
+ * POST (run simulations): requireRole('writer', 'admin')
2409
+ * POST (ERP writeback): requireRole('erp_writer', 'admin')
2410
+ * POST (approve/reject): requireRole('approver', 'admin')
2411
+ - correlationId extraction: const correlationId = c.get('correlationId') as string
2412
+ - Actor identity: const actor = (c.get('iamClaims') as Record<string, unknown>)?.sub as string ?? 'anonymous'
2413
+ - Audit logging via deps.audit.log() for ALL state-changing operations (POST/PUT/DELETE), on both success AND failure
2414
+ - Structured error logging with correlationId in every catch block
2415
+ - Error responses that include correlationId
2416
+
2417
+ The app MUST apply these middleware in order at the top level:
2418
+ app.use('*', correlationMiddleware);
2419
+ app.use('*', loggingMiddleware);
2420
+ app.route('/health', healthRouter);
2421
+ app.use('*', iamMiddleware);
2422
+
2423
+ ${buildPlatformCatalogPrompt()}
2424
+
2425
+ Route handlers SHOULD call platform services for cross-cutting concerns:
2426
+ - callPlatformService(process.env.AGENTICS_SHIELD_URL, '/api/v1/scan', body, { tier: 'must-have' }) — scan inbound POST/PUT bodies
2427
+ - callPlatformService(process.env.AGENTICS_COSTOPS_URL, '/api/v1/track', metadata, { tier: 'should-have' }) — track operation cost (fire-and-forget)
2428
+ - callPlatformService(process.env.AGENTICS_OBSERVATORY_URL, '/api/v1/telemetry/spans', span, { tier: 'operational' }) — send telemetry
2429
+
2430
+ Import callPlatformService from './dependencies.js'.
2431
+
2432
+ Return ONLY valid TypeScript code. No markdown fences. No explanation.`;
2433
+ }
1682
2434
  // Rust Mock Data Helpers (unchanged — Rust routes remain mocked)
1683
2435
  // ============================================================================
1684
2436
  function buildRustMockCommandJson(ctxSlug, opSlug) {
@@ -2133,15 +2885,38 @@ export function generateHttpServer(context) {
2133
2885
  // ADR-062: Detect if Rust optimizer exists for simulation endpoint generation
2134
2886
  const hasRustOptimizer = /\brust\b/i.test(context.scenarioQuery) &&
2135
2887
  /graph|optim|simul|network|dependency|pareto|compute/i.test(context.scenarioQuery);
2136
- // schemas.ts Zod validation schemas for all domain entities
2137
- const schemasContent = tsSchemas(contexts, hasRustOptimizer);
2888
+ // Build codegen context for LLM prompts (ADR-067)
2889
+ const codegenCtx = {
2890
+ scenarioQuery: context.scenarioQuery,
2891
+ dddContexts: contexts,
2892
+ simulationContext: (context.simulationContext ?? null),
2893
+ techStack: {
2894
+ erp: erpResolution.erpSystem,
2895
+ cloud: /gcp|google/i.test(context.scenarioQuery) ? 'gcp' : /aws/i.test(context.scenarioQuery) ? 'aws' : /azure/i.test(context.scenarioQuery) ? 'azure' : 'gcp',
2896
+ databases: [/postgres/i.test(context.scenarioQuery) ? 'PostgreSQL' : 'PostgreSQL', ...((/bigquery/i.test(context.scenarioQuery)) ? ['BigQuery'] : [])],
2897
+ languages: ['TypeScript', ...(hasRustOptimizer ? ['Rust'] : [])],
2898
+ hasRustOptimizer,
2899
+ },
2900
+ };
2901
+ // schemas.ts — ADR-067: LLM-first, template fallback
2902
+ const schemasLLM = generateCodeViaLLM({
2903
+ label: 'schemas.ts',
2904
+ prompt: buildSchemasPrompt(codegenCtx),
2905
+ timeoutMs: 60_000,
2906
+ });
2907
+ const schemasContent = schemasLLM.code ?? tsSchemas(contexts, hasRustOptimizer);
2138
2908
  const schemasPath = join(serverDir, 'schemas.ts');
2139
2909
  writeServerFile(schemasPath, schemasContent);
2140
2910
  serverFiles.push({ relativePath: 'project/src/server/schemas.ts', content: schemasContent, kind: 'server' });
2141
2911
  writtenPaths.push(schemasPath);
2142
- // dependencies.ts — DI factory with port interfaces, adapters, and container
2912
+ // dependencies.ts — ADR-067: LLM-first, template fallback
2143
2913
  const simCtx = context.simulationContext ?? null;
2144
- const depsContent = tsDependencies(contexts, simCtx);
2914
+ const depsLLM = generateCodeViaLLM({
2915
+ label: 'dependencies.ts',
2916
+ prompt: buildDependenciesPrompt(codegenCtx),
2917
+ timeoutMs: 90_000,
2918
+ });
2919
+ const depsContent = depsLLM.code ?? tsDependencies(contexts, simCtx);
2145
2920
  const depsPath = join(serverDir, 'dependencies.ts');
2146
2921
  writeServerFile(depsPath, depsContent);
2147
2922
  serverFiles.push({ relativePath: 'project/src/server/dependencies.ts', content: depsContent, kind: 'server' });
@@ -2162,7 +2937,7 @@ export function generateHttpServer(context) {
2162
2937
  id: s.id,
2163
2938
  name: s.name,
2164
2939
  scores: {
2165
- cost: Math.max(0, Math.min(1, 1 - Math.abs(s.costDelta) / 10_000_000)), // normalize cost
2940
+ cost: Math.max(0, Math.min(1, 1 - Math.abs(s.costDelta) / parseFloat(process.env.OPT_COST_NORM_FACTOR ?? '10000000'))), // ADR-071: parameterized
2166
2941
  emissions: s.reliability, // use reliability as proxy for emissions improvement
2167
2942
  resilience: s.riskLevel === 'low' ? 0.9 : s.riskLevel === 'medium' ? 0.6 : 0.3,
2168
2943
  complexity: s.mitigationStrategy ? 0.5 : 0.8, // strategies with mitigations are more complex
@@ -2179,8 +2954,55 @@ export function generateHttpServer(context) {
2179
2954
  serverFiles.push({ relativePath: 'project/src/data/tradeoff-matrix.json', content: JSON.stringify(tradeoffMatrix, null, 2), kind: 'server' });
2180
2955
  }
2181
2956
  }
2182
- // routes.ts — Domain logic route handlers wired to adapters
2183
- const { content: routesContent, routeCount } = tsRoutes(erpResolution.erpSystem, contexts, simCtx, hasRustOptimizer);
2957
+ // routes.ts — ADR-067: LLM-first, template fallback
2958
+ const routesLLM = generateCodeViaLLM({
2959
+ label: 'routes.ts',
2960
+ prompt: buildRoutesPrompt(codegenCtx, erpResolution.erpSystem),
2961
+ timeoutMs: 120_000, // routes is the biggest file — give it 2 minutes
2962
+ });
2963
+ let routeCount = 0;
2964
+ let routesContent;
2965
+ let routeSource = 'template';
2966
+ if (routesLLM.code) {
2967
+ routesContent = routesLLM.code;
2968
+ routeSource = 'llm';
2969
+ // Estimate route count from LLM output
2970
+ routeCount = (routesContent.match(/app\.(get|post|put|delete)\(/g) ?? []).length;
2971
+ }
2972
+ else {
2973
+ const templateResult = tsRoutes(erpResolution.erpSystem, contexts, simCtx, hasRustOptimizer);
2974
+ routesContent = templateResult.content;
2975
+ routeCount = templateResult.routeCount;
2976
+ }
2977
+ // ADR-068: Validate middleware parity on generated routes
2978
+ const routeValidation = validateRouteMiddlewareParity(routesContent);
2979
+ if (!routeValidation.valid) {
2980
+ console.warn(JSON.stringify({
2981
+ level: 'warn',
2982
+ type: 'ADR-068_MIDDLEWARE_PARITY',
2983
+ source: routeSource,
2984
+ issues: routeValidation.issues,
2985
+ routeHandlerCount: routeValidation.routeHandlerCount,
2986
+ message: `Route generation (${routeSource}) produced code with ${routeValidation.issues.length} middleware parity issue(s). Falling back to template.`,
2987
+ }));
2988
+ // If LLM output failed validation, fall back to the template which has guaranteed middleware parity
2989
+ if (routeSource === 'llm') {
2990
+ const templateResult = tsRoutes(erpResolution.erpSystem, contexts, simCtx, hasRustOptimizer);
2991
+ routesContent = templateResult.content;
2992
+ routeCount = templateResult.routeCount;
2993
+ routeSource = 'template';
2994
+ // Re-validate the template (should always pass, but verify)
2995
+ const templateValidation = validateRouteMiddlewareParity(routesContent);
2996
+ if (!templateValidation.valid) {
2997
+ console.error(JSON.stringify({
2998
+ level: 'error',
2999
+ type: 'ADR-068_TEMPLATE_VIOLATION',
3000
+ issues: templateValidation.issues,
3001
+ message: 'Template fallback also failed middleware parity validation. This is a pipeline bug.',
3002
+ }));
3003
+ }
3004
+ }
3005
+ }
2184
3006
  const routesPath = join(serverDir, 'routes.ts');
2185
3007
  writeServerFile(routesPath, routesContent);
2186
3008
  serverFiles.push({ relativePath: 'project/src/server/routes.ts', content: routesContent, kind: 'server' });
@@ -2199,6 +3021,42 @@ export function generateHttpServer(context) {
2199
3021
  writeServerFile(eventsPath, eventsContent);
2200
3022
  serverFiles.push({ relativePath: 'project/src/events/domain-events.ts', content: eventsContent, kind: 'server' });
2201
3023
  writtenPaths.push(eventsPath);
3024
+ // ADR-088: Published language module — shared types for cross-context communication
3025
+ if (contexts.length > 1) {
3026
+ const sharedDir = join(serverDir, '..', 'shared');
3027
+ mkdirSync(sharedDir, { recursive: true, mode: DIR_MODE });
3028
+ const plLines = [
3029
+ GENERATED_HEADER_TS,
3030
+ '// ADR-088: Published Language — shared types between bounded contexts',
3031
+ '// Other contexts import from here, not from each other\'s internal types.',
3032
+ '// This prevents direct coupling between bounded contexts.',
3033
+ '',
3034
+ ];
3035
+ for (const ctx of contexts) {
3036
+ const ctxPascal = pascalCase(ctx.name);
3037
+ plLines.push(`// Published by ${ctxPascal} context`);
3038
+ for (const entity of ctx.entities.slice(0, 5)) {
3039
+ const entityPascal = pascalCase(entity.name);
3040
+ plLines.push(`export interface Published${entityPascal} {`);
3041
+ plLines.push(` readonly id: string;`);
3042
+ plLines.push(` readonly name: string;`);
3043
+ plLines.push(`}`);
3044
+ plLines.push('');
3045
+ }
3046
+ for (const vo of ctx.valueObjects.slice(0, 3)) {
3047
+ const voPascal = pascalCase(vo.name);
3048
+ plLines.push(`export interface Published${voPascal} {`);
3049
+ plLines.push(` readonly value: unknown;`);
3050
+ plLines.push(`}`);
3051
+ plLines.push('');
3052
+ }
3053
+ }
3054
+ const plContent = plLines.join('\n');
3055
+ const plPath = join(sharedDir, 'published-language.ts');
3056
+ writeServerFile(plPath, plContent);
3057
+ serverFiles.push({ relativePath: 'project/src/shared/published-language.ts', content: plContent, kind: 'server' });
3058
+ writtenPaths.push(plPath);
3059
+ }
2202
3060
  // health.ts
2203
3061
  const healthContent = tsHealth();
2204
3062
  const healthPath = join(serverDir, 'health.ts');
@@ -2303,6 +3161,14 @@ export function generateHttpServer(context) {
2303
3161
  serverFiles.push({ relativePath: `project/src/__tests__/${ctxSlug}.test.ts`, content: testContent, kind: 'server' });
2304
3162
  writtenPaths.push(testPath);
2305
3163
  }
3164
+ // ADR-087: API route tests — RBAC, validation, correlation IDs
3165
+ const apiTestDir = join(testDir, 'api');
3166
+ mkdirSync(apiTestDir, { recursive: true, mode: DIR_MODE });
3167
+ const apiTestContent = tsApiRouteTest(contexts);
3168
+ const apiTestPath = join(apiTestDir, 'routes.test.ts');
3169
+ writeServerFile(apiTestPath, apiTestContent);
3170
+ serverFiles.push({ relativePath: 'project/src/__tests__/api/routes.test.ts', content: apiTestContent, kind: 'server' });
3171
+ writtenPaths.push(apiTestPath);
2306
3172
  // ADR-055: Per-context integration tests (real SQLite, tests actual SQL)
2307
3173
  const integrationTestDir = join(projectDir, 'src', '__tests__', 'integration');
2308
3174
  mkdirSync(integrationTestDir, { recursive: true, mode: DIR_MODE });