@llm-dev-ops/agentics-cli 2.1.4 → 2.3.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 (76) hide show
  1. package/dist/pipeline/auto-chain.d.ts +73 -0
  2. package/dist/pipeline/auto-chain.d.ts.map +1 -1
  3. package/dist/pipeline/auto-chain.js +525 -38
  4. package/dist/pipeline/auto-chain.js.map +1 -1
  5. package/dist/pipeline/phase2/phases/prompt-generator.d.ts.map +1 -1
  6. package/dist/pipeline/phase2/phases/prompt-generator.js +53 -6
  7. package/dist/pipeline/phase2/phases/prompt-generator.js.map +1 -1
  8. package/dist/pipeline/phase2/schemas.d.ts +10 -10
  9. package/dist/pipeline/phase4/phases/http-server-generator.d.ts +12 -0
  10. package/dist/pipeline/phase4/phases/http-server-generator.d.ts.map +1 -1
  11. package/dist/pipeline/phase4/phases/http-server-generator.js +92 -25
  12. package/dist/pipeline/phase4/phases/http-server-generator.js.map +1 -1
  13. package/dist/pipeline/phase5-build/phase5-build-coordinator.d.ts.map +1 -1
  14. package/dist/pipeline/phase5-build/phase5-build-coordinator.js +44 -0
  15. package/dist/pipeline/phase5-build/phase5-build-coordinator.js.map +1 -1
  16. package/dist/pipeline/phase5-build/phases/post-generation-validator.d.ts +75 -0
  17. package/dist/pipeline/phase5-build/phases/post-generation-validator.d.ts.map +1 -0
  18. package/dist/pipeline/phase5-build/phases/post-generation-validator.js +728 -0
  19. package/dist/pipeline/phase5-build/phases/post-generation-validator.js.map +1 -0
  20. package/dist/pipeline/phase5-build/types.d.ts +1 -1
  21. package/dist/pipeline/phase5-build/types.d.ts.map +1 -1
  22. package/dist/pipeline/types.d.ts +84 -0
  23. package/dist/pipeline/types.d.ts.map +1 -1
  24. package/dist/pipeline/types.js +43 -1
  25. package/dist/pipeline/types.js.map +1 -1
  26. package/dist/synthesis/consensus-svg.d.ts +19 -0
  27. package/dist/synthesis/consensus-svg.d.ts.map +1 -0
  28. package/dist/synthesis/consensus-svg.js +95 -0
  29. package/dist/synthesis/consensus-svg.js.map +1 -0
  30. package/dist/synthesis/consensus-tiers.d.ts +99 -0
  31. package/dist/synthesis/consensus-tiers.d.ts.map +1 -0
  32. package/dist/synthesis/consensus-tiers.js +285 -0
  33. package/dist/synthesis/consensus-tiers.js.map +1 -0
  34. package/dist/synthesis/domain-labor-classifier.d.ts +101 -0
  35. package/dist/synthesis/domain-labor-classifier.d.ts.map +1 -0
  36. package/dist/synthesis/domain-labor-classifier.js +312 -0
  37. package/dist/synthesis/domain-labor-classifier.js.map +1 -0
  38. package/dist/synthesis/domain-unit-registry.d.ts +59 -0
  39. package/dist/synthesis/domain-unit-registry.d.ts.map +1 -0
  40. package/dist/synthesis/domain-unit-registry.js +294 -0
  41. package/dist/synthesis/domain-unit-registry.js.map +1 -0
  42. package/dist/synthesis/financial-claim-extractor.d.ts +52 -0
  43. package/dist/synthesis/financial-claim-extractor.d.ts.map +1 -0
  44. package/dist/synthesis/financial-claim-extractor.js +351 -0
  45. package/dist/synthesis/financial-claim-extractor.js.map +1 -0
  46. package/dist/synthesis/financial-consistency-rules.d.ts +66 -0
  47. package/dist/synthesis/financial-consistency-rules.d.ts.map +1 -0
  48. package/dist/synthesis/financial-consistency-rules.js +432 -0
  49. package/dist/synthesis/financial-consistency-rules.js.map +1 -0
  50. package/dist/synthesis/financial-consistency-runner.d.ts +73 -0
  51. package/dist/synthesis/financial-consistency-runner.d.ts.map +1 -0
  52. package/dist/synthesis/financial-consistency-runner.js +131 -0
  53. package/dist/synthesis/financial-consistency-runner.js.map +1 -0
  54. package/dist/synthesis/forbidden-spin-phrases.d.ts +32 -0
  55. package/dist/synthesis/forbidden-spin-phrases.d.ts.map +1 -0
  56. package/dist/synthesis/forbidden-spin-phrases.js +84 -0
  57. package/dist/synthesis/forbidden-spin-phrases.js.map +1 -0
  58. package/dist/synthesis/phase-gate-thresholds.d.ts +30 -0
  59. package/dist/synthesis/phase-gate-thresholds.d.ts.map +1 -0
  60. package/dist/synthesis/phase-gate-thresholds.js +34 -0
  61. package/dist/synthesis/phase-gate-thresholds.js.map +1 -0
  62. package/dist/synthesis/prompts/index.d.ts.map +1 -1
  63. package/dist/synthesis/prompts/index.js +22 -0
  64. package/dist/synthesis/prompts/index.js.map +1 -1
  65. package/dist/synthesis/simulation-artifact-generator.d.ts.map +1 -1
  66. package/dist/synthesis/simulation-artifact-generator.js +89 -1
  67. package/dist/synthesis/simulation-artifact-generator.js.map +1 -1
  68. package/dist/synthesis/simulation-renderers.d.ts +105 -2
  69. package/dist/synthesis/simulation-renderers.d.ts.map +1 -1
  70. package/dist/synthesis/simulation-renderers.js +1056 -92
  71. package/dist/synthesis/simulation-renderers.js.map +1 -1
  72. package/dist/synthesis/unit-economics-loader.d.ts +71 -0
  73. package/dist/synthesis/unit-economics-loader.d.ts.map +1 -0
  74. package/dist/synthesis/unit-economics-loader.js +200 -0
  75. package/dist/synthesis/unit-economics-loader.js.map +1 -0
  76. package/package.json +1 -1
@@ -1050,6 +1050,7 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
1050
1050
  `import { Hono } from 'hono';`,
1051
1051
  `import { createMiddleware } from 'hono/factory';`,
1052
1052
  `import type { MiddlewareHandler } from 'hono';`,
1053
+ `import type { AppEnv } from './types.js';`,
1053
1054
  `import { z } from 'zod';`,
1054
1055
  `import * as crypto from 'node:crypto';`,
1055
1056
  `import { correlationMiddleware, iamMiddleware, loggingMiddleware } from './middleware.js';`,
@@ -1129,9 +1130,9 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
1129
1130
  '//',
1130
1131
  '',
1131
1132
  'function requireRole(...roles: string[]) {',
1132
- ' return createMiddleware(async (c, next) => {',
1133
- ' const correlationId = c.get(\'correlationId\') as string ?? \'unknown\';',
1134
- ' const claims = c.get(\'iamClaims\') as Record<string, unknown> | undefined;',
1133
+ ' return createMiddleware<AppEnv>(async (c, next) => {',
1134
+ ' const correlationId = getCorrelationId(c);',
1135
+ ' const claims = getIamClaims(c);',
1135
1136
  ' if (!claims) {',
1136
1137
  ' return c.json({ error: { code: \'UNAUTHORIZED\', message: \'No claims available\' }, correlationId }, 401);',
1137
1138
  ' }',
@@ -1228,7 +1229,7 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
1228
1229
  '// App & Middleware',
1229
1230
  '// ============================================================================',
1230
1231
  '',
1231
- 'const app = new Hono();',
1232
+ 'const app = new Hono<AppEnv>();',
1232
1233
  '',
1233
1234
  'app.use(\'*\', correlationMiddleware);',
1234
1235
  'app.use(\'*\', iamMiddleware);',
@@ -1248,7 +1249,7 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
1248
1249
  ' status: c.res.status,',
1249
1250
  ' durationMs: Date.now() - start,',
1250
1251
  ' },',
1251
- ' }, { tier: \'should-have\', correlationId: c.get(\'correlationId\') as string }).catch(() => {});',
1252
+ ' }, { tier: \'should-have\', correlationId: getCorrelationId(c) }).catch(() => {});',
1252
1253
  '});',
1253
1254
  '',
1254
1255
  'app.route(\'/health\', healthRouter);',
@@ -1256,7 +1257,7 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
1256
1257
  '// ADR-076: Shield inbound security scanning middleware (must-have — blocks if unavailable)',
1257
1258
  'app.use(\'/api/v1/*\', async (c, next) => {',
1258
1259
  ' if ([\'POST\', \'PUT\', \'PATCH\'].includes(c.req.method)) {',
1259
- ' const correlationId = c.get(\'correlationId\') as string ?? \'unknown\';',
1260
+ ' const correlationId = getCorrelationId(c);',
1260
1261
  ' try {',
1261
1262
  ' const bodyText = await c.req.text();',
1262
1263
  ' const scanResult = await callPlatformService(',
@@ -1312,7 +1313,7 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
1312
1313
  const isApproval = /^(?:approve|reject|review|authorize)/i.test(cmd.name);
1313
1314
  const isErpWrite = /^(?:sync|push|write|export).*(?:erp|oracle|sap|workday)/i.test(cmd.name);
1314
1315
  const requiredRole = isApproval ? "'approver', 'admin'" : isErpWrite ? "'erp_writer', 'admin'" : "'writer', 'admin'";
1315
- lines.push(`app.post('/api/v1/${ctxSlug}/${cmdSlug}', requireRole(${requiredRole}), 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 = ${schemaName}.safeParse(body);`, ' if (!parsed.success) {', ' return c.json(validationError(correlationId, parsed.error.issues), 400);', ' }', ` const result = await deps.${portName}.execute('${cmd.name}', parsed.data);`, ` await deps.audit.log('${ctxSlug}', result?.id ?? correlationId, '${cmdSlug}', actor, parsed.data);`, ' return c.json({', ' correlationId,', ' status: \'completed\',', ' data: result,', ' metadata: { response_time_ms: Date.now() - startTime },', ' }, 201);', ' } 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('${ctxSlug}', correlationId, '${cmdSlug}: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);', ' }', '});', '');
1316
+ lines.push(`app.post('/api/v1/${ctxSlug}/${cmdSlug}', requireRole(${requiredRole}), async (c) => {`, ' const correlationId = getCorrelationId(c);', ' const actor = getActor(c);', ' const startTime = Date.now();', ' try {', ' const body = await c.req.json();', ` const parsed = ${schemaName}.safeParse(body);`, ' if (!parsed.success) {', ' return c.json(validationError(correlationId, parsed.error.issues), 400);', ' }', ` const result = await deps.${portName}.execute('${cmd.name}', parsed.data);`, ` await deps.audit.log('${ctxSlug}', result?.id ?? correlationId, '${cmdSlug}', actor, parsed.data);`, ' return c.json({', ' correlationId,', ' status: \'completed\',', ' data: result,', ' metadata: { response_time_ms: Date.now() - startTime },', ' }, 201);', ' } 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('${ctxSlug}', correlationId, '${cmdSlug}: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);', ' }', '});', '');
1316
1317
  routeCount++;
1317
1318
  }
1318
1319
  // GET handlers for queries
@@ -1320,28 +1321,28 @@ function tsRoutes(erpSystem, contexts, simulationContext, hasRustOptimizer = fal
1320
1321
  const querySlug = slugify(query.name);
1321
1322
  const schemaName = `${pascalCase(query.name)}Schema`;
1322
1323
  const portName = `${camelCase(ctx.name.replace(/Context$/i, ''))}Port`;
1323
- lines.push(`app.get('/api/v1/${ctxSlug}/${querySlug}', 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 startTime = Date.now();', ' try {', ' const params = c.req.query();', ` const parsed = ${schemaName}.safeParse(params);`, ' if (!parsed.success) {', ' return c.json(validationError(correlationId, parsed.error.issues), 400);', ' }', ` const result = await deps.${portName}.query('${query.name}', parsed.data);`, ` await deps.audit.log('${ctxSlug}', correlationId, '${querySlug}:read', actor, { resultCount: result.total });`, ' return c.json({', ' correlationId,', ' status: \'ok\',', ' data: result.rows,', ' pagination: { limit: parsed.data.limit ?? 50, offset: parsed.data.offset ?? 0, total: result.total },', ' 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 }));', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '');
1324
+ lines.push(`app.get('/api/v1/${ctxSlug}/${querySlug}', requireRole('viewer', 'writer', 'admin'), async (c) => {`, ' const correlationId = getCorrelationId(c);', ' const actor = getActor(c);', ' const startTime = Date.now();', ' try {', ' const params = c.req.query();', ` const parsed = ${schemaName}.safeParse(params);`, ' if (!parsed.success) {', ' return c.json(validationError(correlationId, parsed.error.issues), 400);', ' }', ` const result = await deps.${portName}.query('${query.name}', parsed.data);`, ` await deps.audit.log('${ctxSlug}', correlationId, '${querySlug}:read', actor, { resultCount: result.total });`, ' return c.json({', ' correlationId,', ' status: \'ok\',', ' data: result.rows,', ' pagination: { limit: parsed.data.limit ?? 50, offset: parsed.data.offset ?? 0, total: result.total },', ' 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 }));', ' const message = process.env.NODE_ENV === \'production\' ? \'Internal server error\' : (err instanceof Error ? err.message : \'Unknown error\');', ' return c.json(serverError(correlationId, message), 500);', ' }', '});', '');
1324
1325
  routeCount++;
1325
1326
  }
1326
1327
  }
1327
1328
  // ADR-062: Simulation execution endpoint (when Rust optimizer present)
1328
1329
  if (hasRustOptimizer) {
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);', ' }', '});', '');
1330
+ lines.push('// ============================================================================', '// POST /api/v1/simulate — Invoke Rust optimizer engine (ADR-062)', '// ============================================================================', '', "app.post('/api/v1/simulate', requireRole('writer', 'admin'), async (c) => {", ' const correlationId = getCorrelationId(c);', ' const actor = getActor(c);', ' 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);', ' }', '});', '');
1330
1331
  routeCount++;
1331
1332
  }
1332
1333
  // ADR-054: Decision approval workflow endpoints (always generated)
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);', ' }', '});', '');
1334
+ lines.push('// ============================================================================', '// Decision Approval Workflow (ADR-054) — human-in-the-loop governance', '// ============================================================================', '', "app.post('/api/v1/decisions', requireRole('writer', 'admin'), async (c) => {", ' const correlationId = getCorrelationId(c);', ' const actor = getActor(c);', ' const actorRoles = getActorRoles(c);', ' 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 = getCorrelationId(c);', ' const actor = getActor(c);', ' 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 = getCorrelationId(c);', ' const actor = getActor(c);', ' 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 = getCorrelationId(c);', ' const actor = getActor(c);', ' 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 = getCorrelationId(c);', ' const actor = getActor(c);', ' 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 = getActorRoles(c);', ' 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 = getCorrelationId(c);', ' const actor = getActor(c);', " 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 = getCorrelationId(c);', ' const actor = getActor(c);', ' 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);', ' }', '});', '');
1334
1335
  routeCount += 7; // submit, approve, reject, execute, getById, list, lineage
1335
1336
  // ADR-052: Simulation read-only endpoints (when simulation context available)
1336
1337
  if (simulationContext && (simulationContext.scenarios.length > 0 || simulationContext.simulationId)) {
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);', ' }', '});', '');
1338
+ 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 = getCorrelationId(c);', ' 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 = getCorrelationId(c);', ' 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 = getCorrelationId(c);', ' 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 = getCorrelationId(c);', " 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 = getCorrelationId(c);', ' 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);', ' }', '});', '');
1338
1339
  routeCount += 4;
1339
1340
  // ADR-053: Tradeoff analysis and budget compliance endpoints
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);', ' }', '});', '');
1341
+ 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 = getCorrelationId(c);', ' 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 = getCorrelationId(c);', ' 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);', ' }', '});', '');
1341
1342
  routeCount += 2;
1342
1343
  }
1343
1344
  // ERP sync status endpoint (reusable across all ERP types)
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
+ 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 = getCorrelationId(c);', ' 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
1346
  routeCount += 3; // sync-status + metrics + job status
1346
1347
  return { content: lines.join('\n'), routeCount };
1347
1348
  }
@@ -1489,11 +1490,53 @@ function tsDomainEvents(contexts) {
1489
1490
  lines.push('');
1490
1491
  return lines.join('\n');
1491
1492
  }
1492
- function tsMiddleware() {
1493
+ /**
1494
+ * ADR-PIPELINE-064: Server-wide typed Hono context variables.
1495
+ *
1496
+ * Emits `src/server/types.ts` exporting the `ContextVariables` interface,
1497
+ * `IamClaims` interface, and the `AppEnv` type alias. These are consumed
1498
+ * by routes.ts (`new Hono<AppEnv>()`), middleware.ts (`createMiddleware<AppEnv>()`),
1499
+ * and health.ts — eliminating `c.get('correlationId') as string` casts and
1500
+ * `Record<string, unknown>` on iamClaims.
1501
+ */
1502
+ // Exported for unit tests (ADR-PIPELINE-064 verification)
1503
+ export function tsServerTypes() {
1504
+ return [
1505
+ GENERATED_HEADER_TS,
1506
+ '// ADR-PIPELINE-064: Typed Hono context variables eliminate unsafe type assertions.',
1507
+ '// Every middleware that calls c.set() MUST add its key to ContextVariables.',
1508
+ '// Route handlers use getCorrelationId(c), getActor(c), getActorRoles(c) helpers',
1509
+ '// (declared in routes.ts) rather than reading context directly.',
1510
+ '',
1511
+ '/** Claims extracted from a verified IAM JWT token. */',
1512
+ 'export interface IamClaims {',
1513
+ ' sub?: string;',
1514
+ ' roles?: string[];',
1515
+ ' iss?: string;',
1516
+ ' aud?: string | string[];',
1517
+ ' exp?: number;',
1518
+ ' iat?: number;',
1519
+ ' [claim: string]: unknown;',
1520
+ '}',
1521
+ '',
1522
+ '/** Hono context variables set by middleware and read by route handlers. */',
1523
+ 'export interface ContextVariables {',
1524
+ ' correlationId: string;',
1525
+ ' iamClaims?: IamClaims;',
1526
+ '}',
1527
+ '',
1528
+ '/** Hono environment type — use as `new Hono<AppEnv>()` and `createMiddleware<AppEnv>()`. */',
1529
+ 'export type AppEnv = { Variables: ContextVariables };',
1530
+ '',
1531
+ ].join('\n');
1532
+ }
1533
+ // Exported for unit tests (ADR-PIPELINE-064 verification)
1534
+ export function tsMiddleware() {
1493
1535
  return [
1494
1536
  GENERATED_HEADER_TS,
1495
1537
  `import type { MiddlewareHandler } from 'hono';`,
1496
1538
  `import { createMiddleware } from 'hono/factory';`,
1539
+ `import type { AppEnv, IamClaims } from './types.js';`,
1497
1540
  `import * as crypto from 'node:crypto';`,
1498
1541
  `import * as jwt from 'jsonwebtoken';`,
1499
1542
  `import jwksClient from 'jwks-rsa';`,
@@ -1502,7 +1545,7 @@ function tsMiddleware() {
1502
1545
  '// Correlation ID middleware',
1503
1546
  '// ---------------------------------------------------------------------------',
1504
1547
  '',
1505
- 'export const correlationMiddleware: MiddlewareHandler = createMiddleware(async (c, next) => {',
1548
+ 'export const correlationMiddleware: MiddlewareHandler = createMiddleware<AppEnv>(async (c, next) => {',
1506
1549
  ' const correlationId = c.req.header(\'X-Correlation-ID\') ?? crypto.randomUUID();',
1507
1550
  ' c.set(\'correlationId\', correlationId);',
1508
1551
  ' c.header(\'X-Correlation-ID\', correlationId);',
@@ -1534,7 +1577,7 @@ function tsMiddleware() {
1534
1577
  ' });',
1535
1578
  '}',
1536
1579
  '',
1537
- 'export const iamMiddleware: MiddlewareHandler = createMiddleware(async (c, next) => {',
1580
+ 'export const iamMiddleware: MiddlewareHandler = createMiddleware<AppEnv>(async (c, next) => {',
1538
1581
  ' const path = new URL(c.req.url).pathname;',
1539
1582
  ' if (path === \'/health/healthz\' || path === \'/health/readyz\' || path === \'/metrics\') {',
1540
1583
  ' await next();',
@@ -1542,7 +1585,7 @@ function tsMiddleware() {
1542
1585
  ' }',
1543
1586
  '',
1544
1587
  ' // ADR-051: Auth errors include correlationId for traceability',
1545
- ' const correlationId = c.get(\'correlationId\') as string ?? \'unknown\';',
1588
+ ' const correlationId = getCorrelationId(c);',
1546
1589
  ' const authHeader = c.req.header(\'Authorization\');',
1547
1590
  ' if (!authHeader?.startsWith(\'Bearer \')) {',
1548
1591
  ' return c.json({ error: { code: \'UNAUTHORIZED\', message: \'Missing Bearer token\' }, correlationId }, 401);',
@@ -1572,7 +1615,11 @@ function tsMiddleware() {
1572
1615
  ' }',
1573
1616
  '',
1574
1617
  ' const payload = jwt.verify(token, signingKey, verifyOptions);',
1575
- ' c.set(\'iamClaims\', payload);',
1618
+ ' if (typeof payload === \'string\') {',
1619
+ ' return c.json({ error: { code: \'UNAUTHORIZED\', message: \'Invalid token: string payload\' }, correlationId }, 401);',
1620
+ ' }',
1621
+ ' // ADR-PIPELINE-064: typed context variables — payload is narrowed to an object here',
1622
+ ' c.set(\'iamClaims\', payload as IamClaims);',
1576
1623
  ' } catch (err) {',
1577
1624
  ' const message = err instanceof Error ? err.message : \'Token verification failed\';',
1578
1625
  ' console.warn(JSON.stringify({ level: \'warn\', type: \'AUTH_FAILURE\', correlationId, error: message, path: c.req.path }));',
@@ -1586,9 +1633,9 @@ function tsMiddleware() {
1586
1633
  '// Structured JSON request/response logging middleware',
1587
1634
  '// ---------------------------------------------------------------------------',
1588
1635
  '',
1589
- 'export const loggingMiddleware: MiddlewareHandler = createMiddleware(async (c, next) => {',
1636
+ 'export const loggingMiddleware: MiddlewareHandler = createMiddleware<AppEnv>(async (c, next) => {',
1590
1637
  ' const startTime = Date.now();',
1591
- ' const correlationId = c.get(\'correlationId\') as string | undefined;',
1638
+ ' const correlationId = c.get(\'correlationId\');',
1592
1639
  '',
1593
1640
  ' process.stdout.write(',
1594
1641
  ' JSON.stringify({',
@@ -1625,17 +1672,21 @@ function tsMiddleware() {
1625
1672
  '',
1626
1673
  ].join('\n');
1627
1674
  }
1628
- function tsHealth() {
1675
+ // Exported for unit tests (ADR-PIPELINE-064 verification)
1676
+ export function tsHealth() {
1629
1677
  return [
1630
1678
  GENERATED_HEADER_TS,
1631
1679
  `import { Hono } from 'hono';`,
1680
+ `import type { AppEnv } from './types.js';`,
1632
1681
  '',
1633
1682
  '// ADR-050: Real health checks that verify downstream dependencies',
1683
+ '// ADR-PIPELINE-064: healthRouter is typed with AppEnv so it can be mounted',
1684
+ '// on the main app (also typed with AppEnv) without variance errors.',
1634
1685
  'export function createHealthRouter(deps: {',
1635
1686
  ' db?: { query(sql: string, params: unknown[]): Promise<unknown[]> };',
1636
1687
  ' erp?: { getSyncStatus(): { circuit_breaker_state: string } };',
1637
- '}): Hono {',
1638
- ' const healthRouter = new Hono();',
1688
+ '}): Hono<AppEnv> {',
1689
+ ' const healthRouter = new Hono<AppEnv>();',
1639
1690
  '',
1640
1691
  ' // Liveness — is the process running?',
1641
1692
  ' healthRouter.get(\'/healthz\', (c) => {',
@@ -2298,6 +2349,14 @@ ${formatSimulationForPrompt(ctx.simulationContext)}
2298
2349
  ## Instructions
2299
2350
  Generate a routes.ts file that:
2300
2351
  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
2352
+ **ADR-PIPELINE-064: ALSO import \`import type { AppEnv } from './types.js';\`**
2353
+ **The Hono app MUST be typed as \`new Hono<AppEnv>()\` — NEVER \`new Hono()\` without the generic.**
2354
+ **Type-safe context accessors MUST be defined at the top of routes.ts:**
2355
+ - \`function getCorrelationId(c: { get: (key: string) => unknown }): string\` — returns c.get('correlationId') as string with 'unknown' fallback
2356
+ - \`function getIamClaims(c: { get: (key: string) => unknown }): Record<string, unknown>\` — returns c.get('iamClaims') as Record with {} fallback
2357
+ - \`function getActor(c: { get: (key: string) => unknown }): string\` — returns getIamClaims(c).sub with 'unknown' fallback
2358
+ - \`function getActorRoles(c: { get: (key: string) => unknown }): string[]\`
2359
+ **All route handlers MUST use these helpers. NEVER emit \`c.get('correlationId') as string\` or \`c.get('iamClaims') as Record<string, unknown>\` in route handler bodies — those patterns are rejected by the post-generation validator (PGV-001).**
2301
2360
  ${ctx.techStack.hasRustOptimizer ? '2. Imports runOptimizer from ../services/optimizer-bridge' : ''}
2302
2361
  3. Defines RBAC middleware: requireRole(...roles) that checks JWT claims
2303
2362
  - Role matrix: viewer=GET only, writer=POST commands, approver=approve/reject, erp_writer=ERP execute, admin=all
@@ -2408,8 +2467,9 @@ Every route handler MUST have ALL of the following — no exceptions:
2408
2467
  * POST (run simulations): requireRole('writer', 'admin')
2409
2468
  * POST (ERP writeback): requireRole('erp_writer', 'admin')
2410
2469
  * 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'
2470
+ - correlationId extraction: const correlationId = getCorrelationId(c) // ADR-PIPELINE-064 helper; do NOT use 'as string' casts
2471
+ - Actor identity: const actor = getActor(c) // ADR-PIPELINE-064 helper; do NOT use '(c.get(\\'iamClaims\\') as Record<string, unknown>)?.sub' patterns
2472
+ - Actor roles: const roles = getActorRoles(c) // ADR-PIPELINE-064 helper
2413
2473
  - Audit logging via deps.audit.log() for ALL state-changing operations (POST/PUT/DELETE), on both success AND failure
2414
2474
  - Structured error logging with correlationId in every catch block
2415
2475
  - Error responses that include correlationId
@@ -2954,6 +3014,13 @@ export function generateHttpServer(context) {
2954
3014
  serverFiles.push({ relativePath: 'project/src/data/tradeoff-matrix.json', content: JSON.stringify(tradeoffMatrix, null, 2), kind: 'server' });
2955
3015
  }
2956
3016
  }
3017
+ // types.ts — ADR-PIPELINE-064: typed Hono context variables (must be written
3018
+ // BEFORE routes.ts/middleware.ts/health.ts because they import from it)
3019
+ const typesContent = tsServerTypes();
3020
+ const typesPath = join(serverDir, 'types.ts');
3021
+ writeServerFile(typesPath, typesContent);
3022
+ serverFiles.push({ relativePath: 'project/src/server/types.ts', content: typesContent, kind: 'server' });
3023
+ writtenPaths.push(typesPath);
2957
3024
  // routes.ts — ADR-067: LLM-first, template fallback
2958
3025
  const routesLLM = generateCodeViaLLM({
2959
3026
  label: 'routes.ts',