@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.
- package/dist/pipeline/auto-chain.d.ts +73 -0
- package/dist/pipeline/auto-chain.d.ts.map +1 -1
- package/dist/pipeline/auto-chain.js +525 -38
- package/dist/pipeline/auto-chain.js.map +1 -1
- package/dist/pipeline/phase2/phases/prompt-generator.d.ts.map +1 -1
- package/dist/pipeline/phase2/phases/prompt-generator.js +53 -6
- package/dist/pipeline/phase2/phases/prompt-generator.js.map +1 -1
- package/dist/pipeline/phase2/schemas.d.ts +10 -10
- package/dist/pipeline/phase4/phases/http-server-generator.d.ts +12 -0
- package/dist/pipeline/phase4/phases/http-server-generator.d.ts.map +1 -1
- package/dist/pipeline/phase4/phases/http-server-generator.js +92 -25
- package/dist/pipeline/phase4/phases/http-server-generator.js.map +1 -1
- package/dist/pipeline/phase5-build/phase5-build-coordinator.d.ts.map +1 -1
- package/dist/pipeline/phase5-build/phase5-build-coordinator.js +44 -0
- package/dist/pipeline/phase5-build/phase5-build-coordinator.js.map +1 -1
- package/dist/pipeline/phase5-build/phases/post-generation-validator.d.ts +75 -0
- package/dist/pipeline/phase5-build/phases/post-generation-validator.d.ts.map +1 -0
- package/dist/pipeline/phase5-build/phases/post-generation-validator.js +728 -0
- package/dist/pipeline/phase5-build/phases/post-generation-validator.js.map +1 -0
- package/dist/pipeline/phase5-build/types.d.ts +1 -1
- package/dist/pipeline/phase5-build/types.d.ts.map +1 -1
- package/dist/pipeline/types.d.ts +84 -0
- package/dist/pipeline/types.d.ts.map +1 -1
- package/dist/pipeline/types.js +43 -1
- package/dist/pipeline/types.js.map +1 -1
- package/dist/synthesis/consensus-svg.d.ts +19 -0
- package/dist/synthesis/consensus-svg.d.ts.map +1 -0
- package/dist/synthesis/consensus-svg.js +95 -0
- package/dist/synthesis/consensus-svg.js.map +1 -0
- package/dist/synthesis/consensus-tiers.d.ts +99 -0
- package/dist/synthesis/consensus-tiers.d.ts.map +1 -0
- package/dist/synthesis/consensus-tiers.js +285 -0
- package/dist/synthesis/consensus-tiers.js.map +1 -0
- package/dist/synthesis/domain-labor-classifier.d.ts +101 -0
- package/dist/synthesis/domain-labor-classifier.d.ts.map +1 -0
- package/dist/synthesis/domain-labor-classifier.js +312 -0
- package/dist/synthesis/domain-labor-classifier.js.map +1 -0
- package/dist/synthesis/domain-unit-registry.d.ts +59 -0
- package/dist/synthesis/domain-unit-registry.d.ts.map +1 -0
- package/dist/synthesis/domain-unit-registry.js +294 -0
- package/dist/synthesis/domain-unit-registry.js.map +1 -0
- package/dist/synthesis/financial-claim-extractor.d.ts +52 -0
- package/dist/synthesis/financial-claim-extractor.d.ts.map +1 -0
- package/dist/synthesis/financial-claim-extractor.js +351 -0
- package/dist/synthesis/financial-claim-extractor.js.map +1 -0
- package/dist/synthesis/financial-consistency-rules.d.ts +66 -0
- package/dist/synthesis/financial-consistency-rules.d.ts.map +1 -0
- package/dist/synthesis/financial-consistency-rules.js +432 -0
- package/dist/synthesis/financial-consistency-rules.js.map +1 -0
- package/dist/synthesis/financial-consistency-runner.d.ts +73 -0
- package/dist/synthesis/financial-consistency-runner.d.ts.map +1 -0
- package/dist/synthesis/financial-consistency-runner.js +131 -0
- package/dist/synthesis/financial-consistency-runner.js.map +1 -0
- package/dist/synthesis/forbidden-spin-phrases.d.ts +32 -0
- package/dist/synthesis/forbidden-spin-phrases.d.ts.map +1 -0
- package/dist/synthesis/forbidden-spin-phrases.js +84 -0
- package/dist/synthesis/forbidden-spin-phrases.js.map +1 -0
- package/dist/synthesis/phase-gate-thresholds.d.ts +30 -0
- package/dist/synthesis/phase-gate-thresholds.d.ts.map +1 -0
- package/dist/synthesis/phase-gate-thresholds.js +34 -0
- package/dist/synthesis/phase-gate-thresholds.js.map +1 -0
- package/dist/synthesis/prompts/index.d.ts.map +1 -1
- package/dist/synthesis/prompts/index.js +22 -0
- package/dist/synthesis/prompts/index.js.map +1 -1
- package/dist/synthesis/simulation-artifact-generator.d.ts.map +1 -1
- package/dist/synthesis/simulation-artifact-generator.js +89 -1
- package/dist/synthesis/simulation-artifact-generator.js.map +1 -1
- package/dist/synthesis/simulation-renderers.d.ts +105 -2
- package/dist/synthesis/simulation-renderers.d.ts.map +1 -1
- package/dist/synthesis/simulation-renderers.js +1056 -92
- package/dist/synthesis/simulation-renderers.js.map +1 -1
- package/dist/synthesis/unit-economics-loader.d.ts +71 -0
- package/dist/synthesis/unit-economics-loader.d.ts.map +1 -0
- package/dist/synthesis/unit-economics-loader.js +200 -0
- package/dist/synthesis/unit-economics-loader.js.map +1 -0
- 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
|
|
1134
|
-
' const claims = c
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
'
|
|
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\')
|
|
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
|
-
|
|
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
|
|
2412
|
-
- Actor identity: const actor = (c.get('iamClaims') as Record<string, unknown>)?.sub
|
|
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',
|