@llm-dev-ops/agentics-cli 2.1.5 → 2.4.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 +190 -0
- package/dist/pipeline/auto-chain.d.ts.map +1 -1
- package/dist/pipeline/auto-chain.js +1571 -72
- 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 +205 -12
- 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/phase4-5-pre-render/financial-model.d.ts +51 -0
- package/dist/pipeline/phase4-5-pre-render/financial-model.d.ts.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/financial-model.js +118 -0
- package/dist/pipeline/phase4-5-pre-render/financial-model.js.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.d.ts +53 -0
- package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.d.ts.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.js +130 -0
- package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.js.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.d.ts +47 -0
- package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.d.ts.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.js +105 -0
- package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.js.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/sector-baselines.d.ts +42 -0
- package/dist/pipeline/phase4-5-pre-render/sector-baselines.d.ts.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/sector-baselines.js +117 -0
- package/dist/pipeline/phase4-5-pre-render/sector-baselines.js.map +1 -0
- 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 +1068 -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 +87 -0
- package/dist/pipeline/types.d.ts.map +1 -1
- package/dist/pipeline/types.js +51 -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 +320 -0
- package/dist/synthesis/domain-unit-registry.js.map +1 -0
- package/dist/synthesis/financial-claim-extractor.d.ts +72 -0
- package/dist/synthesis/financial-claim-extractor.d.ts.map +1 -0
- package/dist/synthesis/financial-claim-extractor.js +382 -0
- package/dist/synthesis/financial-claim-extractor.js.map +1 -0
- package/dist/synthesis/financial-consistency-rules.d.ts +70 -0
- package/dist/synthesis/financial-consistency-rules.d.ts.map +1 -0
- package/dist/synthesis/financial-consistency-rules.js +483 -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/roadmap-dates.d.ts +72 -0
- package/dist/synthesis/roadmap-dates.d.ts.map +1 -0
- package/dist/synthesis/roadmap-dates.js +203 -0
- package/dist/synthesis/roadmap-dates.js.map +1 -0
- package/dist/synthesis/simulation-artifact-generator.d.ts.map +1 -1
- package/dist/synthesis/simulation-artifact-generator.js +135 -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 +1192 -123
- 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
|
@@ -5,10 +5,19 @@
|
|
|
5
5
|
* No I/O, no side effects, no external calls.
|
|
6
6
|
*
|
|
7
7
|
* FORBIDDEN:
|
|
8
|
-
* - File I/O (use simulation-artifact-generator for writes)
|
|
9
8
|
* - External API calls
|
|
10
9
|
* - State mutation
|
|
10
|
+
*
|
|
11
|
+
* EXCEPTION (ADR-PIPELINE-066): renderFinancialAnalysis may read
|
|
12
|
+
* `<runDir>/unit-economics.json` via the loader module to prefer prototype
|
|
13
|
+
* unit economics over per-employee heuristics. The loader is the single
|
|
14
|
+
* permitted I/O path and never throws.
|
|
11
15
|
*/
|
|
16
|
+
import { loadUnitEconomics, buildFinancialModelFromUnitEconomics, enforceManifestConsistency, } from './unit-economics-loader.js';
|
|
17
|
+
import { fcvTag, countFcvTags } from './financial-claim-extractor.js';
|
|
18
|
+
import { classifyLaborIntensity, isLaborIntensive, workforceRiskTemplate, workforceRaciTemplate, workforceSensitivityRow, estimateWorkforceExposure, } from './domain-labor-classifier.js';
|
|
19
|
+
import { populateConsensusSnapshot, consensusBanner, consensusOpeningParagraph, whyNotMoreConfidentSection, consensusGate, analyticalUncertaintyRisk, } from './consensus-tiers.js';
|
|
20
|
+
import { resolvePilotStart, projectDates, } from './roadmap-dates.js';
|
|
12
21
|
// ============================================================================
|
|
13
22
|
// Helper: safe extraction from unknown agent responses
|
|
14
23
|
// ============================================================================
|
|
@@ -839,6 +848,48 @@ function estimateSupplierCount(employees) {
|
|
|
839
848
|
// Rough: 1 active supplier per 50-100 employees for large enterprises
|
|
840
849
|
return Math.max(20, Math.round(employees / 75));
|
|
841
850
|
}
|
|
851
|
+
/**
|
|
852
|
+
* ADR-PIPELINE-070: Parse a financial budget string like "$2.7M – $4.1M ..."
|
|
853
|
+
* and return the mid-point in USD. Returns null when no money is found.
|
|
854
|
+
*/
|
|
855
|
+
function parseUsdRangeMid(budget) {
|
|
856
|
+
if (!budget)
|
|
857
|
+
return null;
|
|
858
|
+
const matches = Array.from(budget.matchAll(/\$([0-9.,]+)\s*([KMB])?/gi));
|
|
859
|
+
if (matches.length === 0)
|
|
860
|
+
return null;
|
|
861
|
+
const values = [];
|
|
862
|
+
for (const m of matches) {
|
|
863
|
+
const num = parseFloat(m[1].replace(/,/g, ''));
|
|
864
|
+
if (Number.isNaN(num))
|
|
865
|
+
continue;
|
|
866
|
+
const suffix = (m[2] ?? '').toUpperCase();
|
|
867
|
+
const mult = suffix === 'M' ? 1_000_000 : suffix === 'K' ? 1_000 : suffix === 'B' ? 1_000_000_000 : 1;
|
|
868
|
+
values.push(num * mult);
|
|
869
|
+
}
|
|
870
|
+
if (values.length === 0)
|
|
871
|
+
return null;
|
|
872
|
+
if (values.length === 1)
|
|
873
|
+
return values[0];
|
|
874
|
+
return (values[0] + values[1]) / 2;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* ADR-PIPELINE-070: Pull the primary operational unit count from a unit
|
|
878
|
+
* economics manifest's measured_scope, preferring the same scope key the
|
|
879
|
+
* sector registry uses (rooms → sqft → vehicles → ...).
|
|
880
|
+
*/
|
|
881
|
+
function primaryScopeCount(manifest) {
|
|
882
|
+
if (!manifest)
|
|
883
|
+
return undefined;
|
|
884
|
+
const scope = manifest.measured_scope;
|
|
885
|
+
const order = ['rooms', 'sqft', 'vehicles', 'beds', 'agents', 'units', 'properties'];
|
|
886
|
+
for (const key of order) {
|
|
887
|
+
const v = scope[key];
|
|
888
|
+
if (typeof v === 'number' && v > 0)
|
|
889
|
+
return v;
|
|
890
|
+
}
|
|
891
|
+
return undefined;
|
|
892
|
+
}
|
|
842
893
|
// extractFinancials is now synthesizeFinancials — kept as export for external callers
|
|
843
894
|
export { synthesizeFinancials as extractFinancials };
|
|
844
895
|
/**
|
|
@@ -1189,6 +1240,24 @@ function extractCoreDomainNoun(query) {
|
|
|
1189
1240
|
*/
|
|
1190
1241
|
const SYNTHESIS_BASE_RULES = `You are a senior partner at McKinsey & Company rewriting an enterprise decision document.
|
|
1191
1242
|
|
|
1243
|
+
🛑 CRITICAL — PRESERVE HTML COMMENT TAGS (ADR-PIPELINE-073)
|
|
1244
|
+
|
|
1245
|
+
The skeleton document contains HTML comment tags like
|
|
1246
|
+
<!-- fcv:kind=investment scope=pilot doc=decision-memo -->
|
|
1247
|
+
These are financial consistency tags. You MUST:
|
|
1248
|
+
|
|
1249
|
+
1. NEVER remove or modify any <!-- fcv:... --> tag
|
|
1250
|
+
2. NEVER move a tag away from the money figure it precedes
|
|
1251
|
+
3. NEVER introduce a money figure without a preceding tag
|
|
1252
|
+
4. If you rewrite a section, keep every tag attached to its rewritten figure
|
|
1253
|
+
5. If you split a figure into two, duplicate the tag so each resulting line
|
|
1254
|
+
has its own tag
|
|
1255
|
+
6. NEVER change a tag's doc= value — it must match the document type you
|
|
1256
|
+
are rewriting (executive-summary / decision-memo / financial-analysis)
|
|
1257
|
+
|
|
1258
|
+
These tags drive a downstream validator. If you remove or misplace them,
|
|
1259
|
+
the pipeline will discard your output and fall back to the skeleton.
|
|
1260
|
+
|
|
1192
1261
|
ABSOLUTE RULES — violating any of these means the document fails:
|
|
1193
1262
|
1. Lead with the recommendation and quantified impact (Pyramid Principle)
|
|
1194
1263
|
2. Every financial figure MUST show its derivation (unit × rate × volume = total)
|
|
@@ -1201,7 +1270,7 @@ ABSOLUTE RULES — violating any of these means the document fails:
|
|
|
1201
1270
|
9. No sentence fragments, no echoing the user's query, no template placeholders
|
|
1202
1271
|
10. Every paragraph must add insight or evidence, not just rearrange data points
|
|
1203
1272
|
|
|
1204
|
-
PRESERVE the markdown structure (headings, tables, bullet points) but REWRITE the prose.
|
|
1273
|
+
PRESERVE the markdown structure (headings, tables, bullet points, HTML comments) but REWRITE the prose.
|
|
1205
1274
|
Do NOT change financial figures — keep the same numbers but improve how they're presented and justified.
|
|
1206
1275
|
Output ONLY the rewritten document — no preamble, no commentary.`;
|
|
1207
1276
|
const SYNTHESIS_TYPE_INSTRUCTIONS = {
|
|
@@ -1245,6 +1314,11 @@ export async function synthesizeExecutiveDocument(skeleton, query, documentType,
|
|
|
1245
1314
|
'',
|
|
1246
1315
|
`DOCUMENT TO REWRITE:\n${skeleton}`,
|
|
1247
1316
|
].filter(Boolean).join('\n');
|
|
1317
|
+
// ADR-PIPELINE-073: tag-survival checking. We count fcv tags in the
|
|
1318
|
+
// skeleton before LLM synthesis and require the output to preserve the
|
|
1319
|
+
// same count. Any LLM output that drops a tag gets discarded — fall
|
|
1320
|
+
// back to the skeleton rather than shipping an unlabeled document.
|
|
1321
|
+
const skeletonTagCount = countFcvTags(skeleton);
|
|
1248
1322
|
// Tier 1: Claude binary (uses user's Claude subscription, no API key needed)
|
|
1249
1323
|
try {
|
|
1250
1324
|
const { execSync } = await import('node:child_process');
|
|
@@ -1256,8 +1330,17 @@ export async function synthesizeExecutiveDocument(skeleton, query, documentType,
|
|
|
1256
1330
|
try {
|
|
1257
1331
|
const result = execSync(`${claudeBin} --print < "${tmpFile}"`, { encoding: 'utf-8', timeout: 90_000, maxBuffer: 1_000_000, shell: '/bin/sh' });
|
|
1258
1332
|
if (result && result.trim().length > skeleton.length * 0.3) {
|
|
1259
|
-
|
|
1260
|
-
|
|
1333
|
+
const trimmed = result.trim();
|
|
1334
|
+
const synthTagCount = countFcvTags(trimmed);
|
|
1335
|
+
if (synthTagCount < skeletonTagCount) {
|
|
1336
|
+
process.stderr.write(`[exec-docs] ADR-073 tag survival check FAILED for ${documentType} ` +
|
|
1337
|
+
`(skeleton=${skeletonTagCount} tags, synthesized=${synthTagCount}) — ` +
|
|
1338
|
+
`discarding LLM output, using skeleton\n`);
|
|
1339
|
+
return skeleton;
|
|
1340
|
+
}
|
|
1341
|
+
process.stderr.write(`[exec-docs] LLM synthesis succeeded for ${documentType} ` +
|
|
1342
|
+
`(${trimmed.length} chars, ${synthTagCount} fcv tags preserved)\n`);
|
|
1343
|
+
return trimmed;
|
|
1261
1344
|
}
|
|
1262
1345
|
}
|
|
1263
1346
|
finally {
|
|
@@ -1293,7 +1376,16 @@ export async function synthesizeExecutiveDocument(skeleton, query, documentType,
|
|
|
1293
1376
|
const parsed = JSON.parse(result);
|
|
1294
1377
|
const text = parsed?.content?.[0]?.text;
|
|
1295
1378
|
if (text && text.length > skeleton.length * 0.3) {
|
|
1296
|
-
|
|
1379
|
+
// ADR-PIPELINE-073: tag-survival check on the API path too.
|
|
1380
|
+
const synthTagCount = countFcvTags(text);
|
|
1381
|
+
if (synthTagCount < skeletonTagCount) {
|
|
1382
|
+
process.stderr.write(`[exec-docs] ADR-073 tag survival check FAILED for ${documentType} ` +
|
|
1383
|
+
`(skeleton=${skeletonTagCount}, synthesized=${synthTagCount}) — ` +
|
|
1384
|
+
`discarding API output, using skeleton\n`);
|
|
1385
|
+
return skeleton;
|
|
1386
|
+
}
|
|
1387
|
+
process.stderr.write(`[exec-docs] API synthesis succeeded for ${documentType} ` +
|
|
1388
|
+
`(${text.length} chars, ${synthTagCount} fcv tags preserved)\n`);
|
|
1297
1389
|
return text;
|
|
1298
1390
|
}
|
|
1299
1391
|
}
|
|
@@ -1337,28 +1429,40 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
|
|
|
1337
1429
|
recommendation = 'CONDITIONAL PROCEED';
|
|
1338
1430
|
recDetail = `Proceed with a scoped pilot to validate core assumptions before full commitment.`;
|
|
1339
1431
|
}
|
|
1432
|
+
// ADR-PIPELINE-071: Consensus tier drives document structure.
|
|
1433
|
+
// - aligned → footnote at the bottom (existing behavior)
|
|
1434
|
+
// - directional → opening paragraph acknowledgement
|
|
1435
|
+
// - contested → loud header AND opening paragraph leading with the
|
|
1436
|
+
// contested state (the pilot exists BECAUSE of this)
|
|
1437
|
+
const consensus = populateConsensusSnapshot(simulationResult, platformResults);
|
|
1438
|
+
const banner = consensusBanner(consensus);
|
|
1439
|
+
const opening = consensusOpeningParagraph(consensus);
|
|
1340
1440
|
// ADR-PIPELINE-024: Pyramid Principle — lead with the answer
|
|
1341
|
-
const lines = [
|
|
1342
|
-
|
|
1343
|
-
''
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
'',
|
|
1348
|
-
|
|
1349
|
-
'',
|
|
1350
|
-
`**${recommendation}** — ${recDetail}`,
|
|
1351
|
-
];
|
|
1441
|
+
const lines = ['# Executive Summary', ''];
|
|
1442
|
+
if (banner) {
|
|
1443
|
+
lines.push(banner, '');
|
|
1444
|
+
}
|
|
1445
|
+
lines.push(`**Date:** ${now}`, '', '---', '', '## Recommendation', '', `**${recommendation}** — ${recDetail}`);
|
|
1446
|
+
if (opening) {
|
|
1447
|
+
lines.push('', opening);
|
|
1448
|
+
}
|
|
1352
1449
|
if (fin.hasData) {
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1450
|
+
// ADR-PIPELINE-073: split the impact line into per-metric bullets so
|
|
1451
|
+
// each gets its own authoritative fcv tag. One tag per line; the
|
|
1452
|
+
// extractor reads the inline tag as the kind/scope of the first money
|
|
1453
|
+
// match on the line.
|
|
1454
|
+
if (fin.budget || fin.payback || fin.npv) {
|
|
1455
|
+
lines.push('Projected impact:');
|
|
1456
|
+
if (fin.budget) {
|
|
1457
|
+
lines.push(`- ${fcvTag('investment', 'full-program', 'executive-summary')} ${fin.budget} investment`);
|
|
1458
|
+
}
|
|
1459
|
+
if (fin.payback) {
|
|
1460
|
+
lines.push(`- ${fcvTag('timeline', 'full-program', 'executive-summary')} ${fin.payback} payback`);
|
|
1461
|
+
}
|
|
1462
|
+
if (fin.npv) {
|
|
1463
|
+
lines.push(`- ${fcvTag('npv', 'full-program', 'executive-summary')} ${fin.npv} 5-year NPV`);
|
|
1464
|
+
}
|
|
1465
|
+
lines.push('');
|
|
1362
1466
|
}
|
|
1363
1467
|
}
|
|
1364
1468
|
// ADR-PIPELINE-025 §4: Success probability with domain-aware decomposition
|
|
@@ -1420,21 +1524,25 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
|
|
|
1420
1524
|
}
|
|
1421
1525
|
lines.push('');
|
|
1422
1526
|
// Financial Impact
|
|
1527
|
+
// ADR-PIPELINE-073: every money cell gets an inline fcv tag so FCR-011
|
|
1528
|
+
// doesn't fire on the rendered output. All rows here describe the
|
|
1529
|
+
// program-level figures (Total Investment, 5-Year NPV, etc.) so they
|
|
1530
|
+
// carry scope=full-program.
|
|
1423
1531
|
if (fin.hasData) {
|
|
1424
1532
|
lines.push('## Financial Impact', '');
|
|
1425
1533
|
lines.push('| Metric | Value |', '|--------|-------|');
|
|
1426
1534
|
if (fin.budget)
|
|
1427
|
-
lines.push(`| Total Investment | ${fin.budget} |`);
|
|
1535
|
+
lines.push(`| Total Investment | ${fcvTag('investment', 'full-program', 'executive-summary')} ${fin.budget} |`);
|
|
1428
1536
|
if (fin.roi)
|
|
1429
|
-
lines.push(`| Expected ROI | ${fin.roi} |`);
|
|
1537
|
+
lines.push(`| Expected ROI | ${fcvTag('roi', 'full-program', 'executive-summary')} ${fin.roi} |`);
|
|
1430
1538
|
if (fin.npv)
|
|
1431
|
-
lines.push(`| 5-Year NPV | ${fin.npv} |`);
|
|
1539
|
+
lines.push(`| 5-Year NPV | ${fcvTag('npv', 'full-program', 'executive-summary')} ${fin.npv} |`);
|
|
1432
1540
|
if (fin.payback)
|
|
1433
|
-
lines.push(`| Payback Period | ${fin.payback} |`);
|
|
1541
|
+
lines.push(`| Payback Period | ${fcvTag('timeline', 'full-program', 'executive-summary')} ${fin.payback} |`);
|
|
1434
1542
|
if (fin.revenue)
|
|
1435
|
-
lines.push(`| Revenue Impact | ${fin.revenue} |`);
|
|
1543
|
+
lines.push(`| Revenue Impact | ${fcvTag('savings', 'full-program', 'executive-summary')} ${fin.revenue} |`);
|
|
1436
1544
|
if (fin.costSavings)
|
|
1437
|
-
lines.push(`| Cost Savings | ${fin.costSavings} |`);
|
|
1545
|
+
lines.push(`| Cost Savings | ${fcvTag('savings', 'full-program', 'executive-summary')} ${fin.costSavings} |`);
|
|
1438
1546
|
lines.push('');
|
|
1439
1547
|
}
|
|
1440
1548
|
// Risk Profile — never empty
|
|
@@ -1447,6 +1555,11 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
|
|
|
1447
1555
|
lines.push(`- **${r.category}** (${r.likelihood} likelihood, ${r.impact} impact): ${r.risk}`);
|
|
1448
1556
|
}
|
|
1449
1557
|
lines.push('');
|
|
1558
|
+
// ADR-PIPELINE-062: Competitive Analysis — never empty (LLM or fallback)
|
|
1559
|
+
// Implements ADR-PIPELINE-057 §3. The fallback builder guarantees the
|
|
1560
|
+
// section is present even when the LLM doesn't return competitive_analysis.
|
|
1561
|
+
const competitiveAnalysis = getOrBuildCompetitiveAnalysis(query, simData, extracted);
|
|
1562
|
+
lines.push(...renderCompetitiveSection(competitiveAnalysis));
|
|
1450
1563
|
// Recommended Next Steps — time-bound
|
|
1451
1564
|
lines.push('## Recommended Next Steps', '');
|
|
1452
1565
|
const steps = buildDomainNextSteps(recommendation, extracted);
|
|
@@ -1454,19 +1567,31 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
|
|
|
1454
1567
|
lines.push(step);
|
|
1455
1568
|
}
|
|
1456
1569
|
lines.push('');
|
|
1457
|
-
// ADR-PIPELINE-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1570
|
+
// ADR-PIPELINE-071: tiered consensus disclosure.
|
|
1571
|
+
// - aligned → optional legacy footnote (kept for backwards compat)
|
|
1572
|
+
// - directional → "Why we are not more confident" section
|
|
1573
|
+
// - contested → "Why we are not more confident" section (mandatory)
|
|
1574
|
+
if (consensus.tier === 'aligned') {
|
|
1575
|
+
// Legacy footnote retained for aligned runs only.
|
|
1576
|
+
const consensusAgent = platformResults.find(r => r.domain === 'analytics-hub' && r.agent === 'consensus');
|
|
1577
|
+
if (consensusAgent) {
|
|
1578
|
+
const consensusData = extractSignalPayload(consensusAgent.response).data ?? {};
|
|
1579
|
+
const achieved = consensusData['consensusAchieved'] ?? consensusData['consensus_achieved'];
|
|
1580
|
+
const confidence = Number(consensusData['confidence'] ?? consensusData['overallConfidence'] ?? 0);
|
|
1581
|
+
if (achieved === false || confidence < 0.6) {
|
|
1582
|
+
lines.push('## Analysis Confidence Note', '');
|
|
1583
|
+
lines.push(`The multi-agent consensus process achieved **${Math.round(confidence * 100)}% confidence**. ` +
|
|
1584
|
+
`This indicates divergent signals across analytical perspectives. ` +
|
|
1585
|
+
`The recommendation accounts for this uncertainty by proposing a scoped pilot ` +
|
|
1586
|
+
`rather than full commitment, allowing validation before broader investment.`);
|
|
1587
|
+
lines.push('');
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
else {
|
|
1592
|
+
const whyLines = whyNotMoreConfidentSection(consensus);
|
|
1593
|
+
if (whyLines.length > 0) {
|
|
1594
|
+
lines.push(...whyLines);
|
|
1470
1595
|
}
|
|
1471
1596
|
}
|
|
1472
1597
|
// Provenance — ADR-PIPELINE-033 + 041: simulation lineage and domain analysis
|
|
@@ -1507,20 +1632,27 @@ export function renderDecisionMemo(query, simulationResult, platformResults) {
|
|
|
1507
1632
|
recommendation = 'CONDITIONAL PROCEED';
|
|
1508
1633
|
rationale = 'Moderate confidence. A scoped pilot is recommended to validate core assumptions before committing to full deployment.';
|
|
1509
1634
|
}
|
|
1635
|
+
// ADR-PIPELINE-071: branch decision-memo structure on consensus tier.
|
|
1636
|
+
// The banner/opening/why-section are computed up front and emitted at
|
|
1637
|
+
// strategic points: banner before the date header, opening after the
|
|
1638
|
+
// recommendation, why-section after the rationale.
|
|
1639
|
+
const memoConsensus = populateConsensusSnapshot(simulationResult, platformResults);
|
|
1640
|
+
const memoBanner = consensusBanner(memoConsensus);
|
|
1641
|
+
const memoOpening = consensusOpeningParagraph(memoConsensus);
|
|
1642
|
+
const memoWhyLines = whyNotMoreConfidentSection(memoConsensus);
|
|
1510
1643
|
// ADR-PIPELINE-024: Structured decision package
|
|
1511
|
-
const lines = [
|
|
1512
|
-
|
|
1513
|
-
''
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
''
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
'',
|
|
1644
|
+
const lines = ['# Decision Memo', ''];
|
|
1645
|
+
if (memoBanner) {
|
|
1646
|
+
lines.push(memoBanner, '');
|
|
1647
|
+
}
|
|
1648
|
+
lines.push(`**Date:** ${now}`, `**Subject:** ${problemStatement}`, `**Decision Requested:** Approval to proceed with ${successProb >= 0.7 ? 'scoped pilot' : 'feasibility investigation'}`, '', '---', '', '## Recommendation', '', `**${recommendation}** — ${rationale}`, '');
|
|
1649
|
+
if (memoOpening) {
|
|
1650
|
+
lines.push(memoOpening, '');
|
|
1651
|
+
}
|
|
1652
|
+
if (memoWhyLines.length > 0) {
|
|
1653
|
+
lines.push(...memoWhyLines);
|
|
1654
|
+
}
|
|
1655
|
+
lines.push(...[
|
|
1524
1656
|
`Success probability: ${(successProb * 100).toFixed(0)}%`,
|
|
1525
1657
|
'',
|
|
1526
1658
|
// Business Context
|
|
@@ -1534,7 +1666,7 @@ export function renderDecisionMemo(query, simulationResult, platformResults) {
|
|
|
1534
1666
|
'## Options Considered', '',
|
|
1535
1667
|
'| Option | Description | Investment | Timeline | Risk Level | Recommendation |',
|
|
1536
1668
|
'|--------|-------------|-----------|----------|------------|---------------|',
|
|
1537
|
-
];
|
|
1669
|
+
]);
|
|
1538
1670
|
// Compute pilot cost as ~30% of full deployment
|
|
1539
1671
|
const fullBudget = fin.budget || 'TBD';
|
|
1540
1672
|
let pilotBudget = fullBudget;
|
|
@@ -1548,23 +1680,28 @@ export function renderDecisionMemo(query, simulationResult, platformResults) {
|
|
|
1548
1680
|
const fmt = (n) => n >= 1_000_000 ? `$${(n / 1_000_000).toFixed(1)}M` : `$${(n / 1000).toFixed(0)}K`;
|
|
1549
1681
|
pilotBudget = `${fmt(pilotVal)} - ${fmt(pilotVal * 1.5)}`;
|
|
1550
1682
|
}
|
|
1551
|
-
|
|
1552
|
-
//
|
|
1683
|
+
// ADR-PIPELINE-073: Options Considered rows carry distinct scope labels —
|
|
1684
|
+
// row A is the pilot, row B is the full program, row C has a savings
|
|
1685
|
+
// figure scoped to full-program. Each row gets an inline fcv tag so
|
|
1686
|
+
// FCR-001/003 can cross-check them against the executive summary.
|
|
1687
|
+
lines.push(`| **A. Scoped Pilot** | Validate with subset of data and ${extracted.domain_entities.slice(0, 2).join(', ') || 'core entities'} | ${fcvTag('investment', 'pilot', 'decision-memo')} ${pilotBudget} | 8-12 weeks | Low | **Recommended** |`, `| **B. Full Deployment** | Enterprise-wide rollout across all ${extracted.systems.length > 0 ? extracted.systems.join(', ') : 'systems'} | ${fcvTag('investment', 'full-program', 'decision-memo')} ${fullBudget} | ${safeString(simData, 'timeline_estimate') || '16-24 weeks'} | Medium | After pilot validation |`, `| **C. Do Nothing** | Continue with manual processes | $0 upfront | N/A | High | Not recommended — ${fin.costSavings ? fcvTag('savings', 'full-program', 'decision-memo') + ' ' + fin.costSavings + ' annual waste continues' : fin.revenue ? fcvTag('savings', 'full-program', 'decision-memo') + ' ' + fin.revenue + ' in annual savings foregone' : 'costs compound without intervention'} |`, '');
|
|
1688
|
+
// Financial Impact — ADR-PIPELINE-073: every row gets an inline fcv tag
|
|
1689
|
+
// with scope=full-program (the table describes the overall program cost).
|
|
1553
1690
|
lines.push('## Financial Impact', '');
|
|
1554
1691
|
if (fin.hasData) {
|
|
1555
1692
|
lines.push('| Metric | Value |', '|--------|-------|');
|
|
1556
1693
|
if (fin.budget)
|
|
1557
|
-
lines.push(`| Total Investment | ${fin.budget} |`);
|
|
1694
|
+
lines.push(`| Total Investment | ${fcvTag('investment', 'full-program', 'decision-memo')} ${fin.budget} |`);
|
|
1558
1695
|
if (fin.roi)
|
|
1559
|
-
lines.push(`| Expected ROI | ${fin.roi} |`);
|
|
1696
|
+
lines.push(`| Expected ROI | ${fcvTag('roi', 'full-program', 'decision-memo')} ${fin.roi} |`);
|
|
1560
1697
|
if (fin.npv)
|
|
1561
|
-
lines.push(`| 5-Year NPV | ${fin.npv} |`);
|
|
1698
|
+
lines.push(`| 5-Year NPV | ${fcvTag('npv', 'full-program', 'decision-memo')} ${fin.npv} |`);
|
|
1562
1699
|
if (fin.payback)
|
|
1563
|
-
lines.push(`| Payback Period | ${fin.payback} |`);
|
|
1700
|
+
lines.push(`| Payback Period | ${fcvTag('timeline', 'full-program', 'decision-memo')} ${fin.payback} |`);
|
|
1564
1701
|
if (fin.revenue)
|
|
1565
|
-
lines.push(`| Revenue Impact | ${fin.revenue} |`);
|
|
1702
|
+
lines.push(`| Revenue Impact | ${fcvTag('savings', 'full-program', 'decision-memo')} ${fin.revenue} |`);
|
|
1566
1703
|
if (fin.costSavings)
|
|
1567
|
-
lines.push(`| Cost Savings | ${fin.costSavings} |`);
|
|
1704
|
+
lines.push(`| Cost Savings | ${fcvTag('savings', 'full-program', 'decision-memo')} ${fin.costSavings} |`);
|
|
1568
1705
|
lines.push('');
|
|
1569
1706
|
}
|
|
1570
1707
|
else {
|
|
@@ -1644,6 +1781,33 @@ export function renderDecisionMemo(query, simulationResult, platformResults) {
|
|
|
1644
1781
|
lines.push(`| ${row.name} | ${row.role} | ${row.impact} | ${row.action} |`);
|
|
1645
1782
|
}
|
|
1646
1783
|
lines.push('');
|
|
1784
|
+
// ADR-PIPELINE-070: HR + Legal RACI rows when the engagement is
|
|
1785
|
+
// labor-intensive. The base stakeholder table above is impact-flavoured;
|
|
1786
|
+
// this section is the explicit RACI overlay top-tier consulting decks
|
|
1787
|
+
// expect when changes touch operational labor.
|
|
1788
|
+
const memoDomainAnalysis = extractDomainAnalysis(simData, query);
|
|
1789
|
+
const memoLaborProfile = classifyLaborIntensity(memoDomainAnalysis, query);
|
|
1790
|
+
if (isLaborIntensive(memoLaborProfile)) {
|
|
1791
|
+
const raciRows = workforceRaciTemplate(memoLaborProfile);
|
|
1792
|
+
if (raciRows.length > 0) {
|
|
1793
|
+
lines.push('## Workforce RACI (ADR-PIPELINE-070)', '', `*Required because the engagement touches a ${memoLaborProfile.intensity}-labor-intensity sector (${memoLaborProfile.sector}). ${memoLaborProfile.reasoning}*`, '', '| Role | Responsibility | Cadence |', '|------|----------------|---------|');
|
|
1794
|
+
for (const row of raciRows) {
|
|
1795
|
+
lines.push(`| ${row.role} | ${row.responsibility} | ${row.cadence} |`);
|
|
1796
|
+
}
|
|
1797
|
+
lines.push('');
|
|
1798
|
+
// Best-effort log so reviewers can grep when the RACI was injected
|
|
1799
|
+
// synthetically vs. supplied by an upstream agent.
|
|
1800
|
+
try {
|
|
1801
|
+
process.stderr.write(JSON.stringify({
|
|
1802
|
+
event: 'renderer.workforce.raci.injected',
|
|
1803
|
+
sector: memoLaborProfile.sector,
|
|
1804
|
+
intensity: memoLaborProfile.intensity,
|
|
1805
|
+
rows: raciRows.length,
|
|
1806
|
+
}) + '\n');
|
|
1807
|
+
}
|
|
1808
|
+
catch { /* logging is best-effort */ }
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1647
1811
|
// Provenance — ADR-PIPELINE-033: include simulation lineage
|
|
1648
1812
|
const sources = platformResults.filter(r => r.status >= 200 && r.status < 300).map(r => `${r.domain}/${r.agent}`);
|
|
1649
1813
|
lines.push('---', '');
|
|
@@ -2239,6 +2403,596 @@ export function buildScenarioArtifact(query, simulationResult, platformResults)
|
|
|
2239
2403
|
})),
|
|
2240
2404
|
};
|
|
2241
2405
|
}
|
|
2406
|
+
/** Rank maturity levels for severity-sorted rendering and gap-level jumps. */
|
|
2407
|
+
const MATURITY_RANK = {
|
|
2408
|
+
none: 0,
|
|
2409
|
+
manual: 1,
|
|
2410
|
+
rule_based: 2,
|
|
2411
|
+
automated: 3,
|
|
2412
|
+
ai_optimized: 4,
|
|
2413
|
+
};
|
|
2414
|
+
/** Human-readable labels for maturity levels. */
|
|
2415
|
+
function maturityLabel(level) {
|
|
2416
|
+
switch (level) {
|
|
2417
|
+
case 'none': return 'None';
|
|
2418
|
+
case 'manual': return 'Manual';
|
|
2419
|
+
case 'rule_based': return 'Rule-Based';
|
|
2420
|
+
case 'automated': return 'Automated';
|
|
2421
|
+
case 'ai_optimized': return 'AI-Optimized';
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Infer a 3-peer-firm set from the scenario type. Names are real companies
|
|
2426
|
+
* operating in the relevant industry as of 2024; citations in `sources`
|
|
2427
|
+
* point to public sustainability/technology reports. The list is
|
|
2428
|
+
* alphabetical for determinism.
|
|
2429
|
+
*/
|
|
2430
|
+
function inferPeerFirms(scenarioType) {
|
|
2431
|
+
const type = scenarioType.toLowerCase();
|
|
2432
|
+
if (type.includes('sustainability') || type.includes('esg') || type.includes('energy') || type.includes('utility')) {
|
|
2433
|
+
return [
|
|
2434
|
+
{ name: 'Google', industry_position: 'Real-time carbon tracking across global data centers', public_evidence_source: 'Google 2023 Environmental Report' },
|
|
2435
|
+
{ name: 'Salesforce', industry_position: 'Net Zero Cloud and Workplace Command Center', public_evidence_source: 'Salesforce 2024 Stakeholder Impact Report' },
|
|
2436
|
+
{ name: 'Siemens', industry_position: 'Integrated GHG accounting via Desigo CC IoT platform', public_evidence_source: 'Siemens Sustainability Report 2024' },
|
|
2437
|
+
];
|
|
2438
|
+
}
|
|
2439
|
+
if (type.includes('supply-chain') || type.includes('logistics') || type.includes('fleet-transportation') || type.includes('vendor')) {
|
|
2440
|
+
return [
|
|
2441
|
+
{ name: 'Amazon', industry_position: 'End-to-end supply visibility across 1,400+ fulfillment centers', public_evidence_source: 'Amazon 2024 Operations Update' },
|
|
2442
|
+
{ name: 'DHL', industry_position: 'AI-optimized logistics with predictive routing', public_evidence_source: 'DHL Trend Radar 2024' },
|
|
2443
|
+
{ name: 'Maersk', industry_position: 'Real-time container tracking and supplier integration', public_evidence_source: 'Maersk Annual Report 2023' },
|
|
2444
|
+
];
|
|
2445
|
+
}
|
|
2446
|
+
if (type.includes('financial-operations') || type.includes('compliance') || type.includes('governance')) {
|
|
2447
|
+
return [
|
|
2448
|
+
{ name: 'Goldman Sachs', industry_position: 'Automated compliance monitoring and audit trails', public_evidence_source: 'Goldman Sachs Technology Report 2024' },
|
|
2449
|
+
{ name: 'JPMorgan Chase', industry_position: 'ML-based risk and compliance automation', public_evidence_source: 'JPMorgan Investor Day 2024' },
|
|
2450
|
+
{ name: 'Morgan Stanley', industry_position: 'Integrated regulatory reporting platform', public_evidence_source: 'Morgan Stanley 2023 Annual Report' },
|
|
2451
|
+
];
|
|
2452
|
+
}
|
|
2453
|
+
if (type.includes('hr-workforce') || type.includes('talent')) {
|
|
2454
|
+
return [
|
|
2455
|
+
{ name: 'IBM', industry_position: 'Watson Talent skills intelligence across 280k employees', public_evidence_source: 'IBM HR Transformation Case Study 2023' },
|
|
2456
|
+
{ name: 'SAP SuccessFactors', industry_position: 'Integrated HR cloud with predictive attrition analytics', public_evidence_source: 'SAP Customer Experience Report 2024' },
|
|
2457
|
+
{ name: 'Workday', industry_position: 'Skills-based workforce planning and internal mobility', public_evidence_source: 'Workday Rising 2023 Keynote' },
|
|
2458
|
+
];
|
|
2459
|
+
}
|
|
2460
|
+
if (type.includes('manufacturing') || type.includes('asset-maintenance') || type.includes('pharma')) {
|
|
2461
|
+
return [
|
|
2462
|
+
{ name: 'GE', industry_position: 'Predix predictive maintenance across industrial assets', public_evidence_source: 'GE Digital 2024 Product Report' },
|
|
2463
|
+
{ name: 'Rockwell Automation', industry_position: 'Connected factory with digital twin simulation', public_evidence_source: 'Rockwell Automation Fair 2023' },
|
|
2464
|
+
{ name: 'Siemens', industry_position: 'MindSphere industrial IoT across 100k+ connected assets', public_evidence_source: 'Siemens Digital Industries Report 2024' },
|
|
2465
|
+
];
|
|
2466
|
+
}
|
|
2467
|
+
if (type.includes('healthcare')) {
|
|
2468
|
+
return [
|
|
2469
|
+
{ name: 'Epic Systems', industry_position: 'Integrated EHR platform serving 250M+ patients', public_evidence_source: 'Epic UGM 2024 Keynote' },
|
|
2470
|
+
{ name: 'Kaiser Permanente', industry_position: 'AI-assisted clinical decision support at scale', public_evidence_source: 'Kaiser Health Research 2023' },
|
|
2471
|
+
{ name: 'Mayo Clinic', industry_position: 'Unified patient data platform with ML diagnostics', public_evidence_source: 'Mayo Clinic Platform 2024' },
|
|
2472
|
+
];
|
|
2473
|
+
}
|
|
2474
|
+
if (type.includes('data-analytics') || type.includes('system-migration')) {
|
|
2475
|
+
return [
|
|
2476
|
+
{ name: 'Databricks', industry_position: 'Unified lakehouse with ML workflow automation', public_evidence_source: 'Databricks Data + AI Summit 2024' },
|
|
2477
|
+
{ name: 'Palantir', industry_position: 'Enterprise-wide data integration and ontology', public_evidence_source: 'Palantir AIPCon 2024' },
|
|
2478
|
+
{ name: 'Snowflake', industry_position: 'Cross-cloud data platform with AI-ready architecture', public_evidence_source: 'Snowflake Summit 2024' },
|
|
2479
|
+
];
|
|
2480
|
+
}
|
|
2481
|
+
if (type.includes('security-identity')) {
|
|
2482
|
+
return [
|
|
2483
|
+
{ name: 'CrowdStrike', industry_position: 'AI-driven endpoint threat detection and response', public_evidence_source: 'CrowdStrike 2024 Global Threat Report' },
|
|
2484
|
+
{ name: 'Okta', industry_position: 'Zero trust identity fabric with continuous verification', public_evidence_source: 'Okta Businesses at Work 2024' },
|
|
2485
|
+
{ name: 'Palo Alto Networks', industry_position: 'Precision AI across network, cloud, and SOC', public_evidence_source: 'Palo Alto Networks Ignite 2024' },
|
|
2486
|
+
];
|
|
2487
|
+
}
|
|
2488
|
+
if (type.includes('customer-relationship')) {
|
|
2489
|
+
return [
|
|
2490
|
+
{ name: 'HubSpot', industry_position: 'Unified CRM with AI-driven lead scoring', public_evidence_source: 'HubSpot INBOUND 2024' },
|
|
2491
|
+
{ name: 'Microsoft Dynamics', industry_position: 'Copilot-integrated sales and service workflows', public_evidence_source: 'Microsoft Ignite 2024' },
|
|
2492
|
+
{ name: 'Salesforce', industry_position: 'Einstein GPT across sales, service, and marketing clouds', public_evidence_source: 'Salesforce Dreamforce 2024' },
|
|
2493
|
+
];
|
|
2494
|
+
}
|
|
2495
|
+
// Generic fallback — reputable enterprise peers
|
|
2496
|
+
return [
|
|
2497
|
+
{ name: 'Deloitte', industry_position: 'Enterprise transformation and AI readiness assessments', public_evidence_source: 'Deloitte Tech Trends 2024' },
|
|
2498
|
+
{ name: 'IBM', industry_position: 'Hybrid cloud and AI-powered enterprise automation', public_evidence_source: 'IBM Think 2024 Keynote' },
|
|
2499
|
+
{ name: 'McKinsey', industry_position: 'QuantumBlack AI transformation engagements', public_evidence_source: 'McKinsey State of AI 2024' },
|
|
2500
|
+
];
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Build capability maturity rows based on scenario type. Always includes
|
|
2504
|
+
* 3 domain-neutral capabilities plus 1-2 domain-specific ones, for 4-5
|
|
2505
|
+
* total rows per analysis.
|
|
2506
|
+
*/
|
|
2507
|
+
function buildCapabilityMaturity(scenarioType, peers) {
|
|
2508
|
+
const type = scenarioType.toLowerCase();
|
|
2509
|
+
// Domain-neutral capabilities — always present
|
|
2510
|
+
const rows = [
|
|
2511
|
+
{
|
|
2512
|
+
capability: 'Data Integration',
|
|
2513
|
+
current_level: 'manual',
|
|
2514
|
+
peer_levels: peerLevels(peers, 'automated', 'ai_optimized', 'automated'),
|
|
2515
|
+
post_pilot_level: 'automated',
|
|
2516
|
+
},
|
|
2517
|
+
{
|
|
2518
|
+
capability: 'Predictive Analytics',
|
|
2519
|
+
current_level: 'none',
|
|
2520
|
+
peer_levels: peerLevels(peers, 'automated', 'ai_optimized', 'rule_based'),
|
|
2521
|
+
post_pilot_level: 'ai_optimized',
|
|
2522
|
+
},
|
|
2523
|
+
{
|
|
2524
|
+
capability: 'Decision Governance',
|
|
2525
|
+
current_level: 'manual',
|
|
2526
|
+
peer_levels: peerLevels(peers, 'rule_based', 'automated', 'automated'),
|
|
2527
|
+
post_pilot_level: 'ai_optimized',
|
|
2528
|
+
},
|
|
2529
|
+
];
|
|
2530
|
+
// Domain-specific additions
|
|
2531
|
+
if (type.includes('sustainability') || type.includes('esg') || type.includes('energy') || type.includes('utility')) {
|
|
2532
|
+
rows.push({
|
|
2533
|
+
capability: 'ESG Reporting (Scope 2)',
|
|
2534
|
+
current_level: 'manual',
|
|
2535
|
+
peer_levels: peerLevels(peers, 'automated', 'ai_optimized', 'automated'),
|
|
2536
|
+
post_pilot_level: 'automated',
|
|
2537
|
+
}, {
|
|
2538
|
+
capability: 'Carbon Tracking',
|
|
2539
|
+
current_level: 'manual',
|
|
2540
|
+
peer_levels: peerLevels(peers, 'automated', 'ai_optimized', 'automated'),
|
|
2541
|
+
post_pilot_level: 'automated',
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
else if (type.includes('supply-chain') || type.includes('logistics') || type.includes('fleet-transportation') || type.includes('vendor')) {
|
|
2545
|
+
rows.push({
|
|
2546
|
+
capability: 'Supplier Visibility',
|
|
2547
|
+
current_level: 'rule_based',
|
|
2548
|
+
peer_levels: peerLevels(peers, 'automated', 'ai_optimized', 'automated'),
|
|
2549
|
+
post_pilot_level: 'automated',
|
|
2550
|
+
}, {
|
|
2551
|
+
capability: 'Logistics Optimization',
|
|
2552
|
+
current_level: 'manual',
|
|
2553
|
+
peer_levels: peerLevels(peers, 'automated', 'ai_optimized', 'automated'),
|
|
2554
|
+
post_pilot_level: 'automated',
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
else if (type.includes('financial-operations') || type.includes('compliance') || type.includes('governance')) {
|
|
2558
|
+
rows.push({
|
|
2559
|
+
capability: 'Regulatory Reporting',
|
|
2560
|
+
current_level: 'manual',
|
|
2561
|
+
peer_levels: peerLevels(peers, 'automated', 'ai_optimized', 'automated'),
|
|
2562
|
+
post_pilot_level: 'automated',
|
|
2563
|
+
}, {
|
|
2564
|
+
capability: 'Audit Automation',
|
|
2565
|
+
current_level: 'manual',
|
|
2566
|
+
peer_levels: peerLevels(peers, 'automated', 'ai_optimized', 'rule_based'),
|
|
2567
|
+
post_pilot_level: 'automated',
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
else if (type.includes('hr-workforce') || type.includes('talent')) {
|
|
2571
|
+
rows.push({
|
|
2572
|
+
capability: 'Workforce Analytics',
|
|
2573
|
+
current_level: 'rule_based',
|
|
2574
|
+
peer_levels: peerLevels(peers, 'automated', 'automated', 'ai_optimized'),
|
|
2575
|
+
post_pilot_level: 'ai_optimized',
|
|
2576
|
+
}, {
|
|
2577
|
+
capability: 'Skills Intelligence',
|
|
2578
|
+
current_level: 'manual',
|
|
2579
|
+
peer_levels: peerLevels(peers, 'ai_optimized', 'automated', 'automated'),
|
|
2580
|
+
post_pilot_level: 'automated',
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
else if (type.includes('manufacturing') || type.includes('asset-maintenance') || type.includes('pharma')) {
|
|
2584
|
+
rows.push({
|
|
2585
|
+
capability: 'Predictive Maintenance',
|
|
2586
|
+
current_level: 'rule_based',
|
|
2587
|
+
peer_levels: peerLevels(peers, 'ai_optimized', 'automated', 'ai_optimized'),
|
|
2588
|
+
post_pilot_level: 'ai_optimized',
|
|
2589
|
+
}, {
|
|
2590
|
+
capability: 'Quality Control',
|
|
2591
|
+
current_level: 'manual',
|
|
2592
|
+
peer_levels: peerLevels(peers, 'automated', 'automated', 'ai_optimized'),
|
|
2593
|
+
post_pilot_level: 'automated',
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
else if (type.includes('healthcare')) {
|
|
2597
|
+
rows.push({
|
|
2598
|
+
capability: 'Clinical Decision Support',
|
|
2599
|
+
current_level: 'manual',
|
|
2600
|
+
peer_levels: peerLevels(peers, 'automated', 'ai_optimized', 'ai_optimized'),
|
|
2601
|
+
post_pilot_level: 'automated',
|
|
2602
|
+
}, {
|
|
2603
|
+
capability: 'Patient Data Integration',
|
|
2604
|
+
current_level: 'rule_based',
|
|
2605
|
+
peer_levels: peerLevels(peers, 'ai_optimized', 'automated', 'automated'),
|
|
2606
|
+
post_pilot_level: 'automated',
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
else if (type.includes('data-analytics') || type.includes('system-migration')) {
|
|
2610
|
+
rows.push({
|
|
2611
|
+
capability: 'Data Pipeline Automation',
|
|
2612
|
+
current_level: 'manual',
|
|
2613
|
+
peer_levels: peerLevels(peers, 'ai_optimized', 'automated', 'ai_optimized'),
|
|
2614
|
+
post_pilot_level: 'automated',
|
|
2615
|
+
}, {
|
|
2616
|
+
capability: 'Self-Service BI',
|
|
2617
|
+
current_level: 'rule_based',
|
|
2618
|
+
peer_levels: peerLevels(peers, 'automated', 'automated', 'ai_optimized'),
|
|
2619
|
+
post_pilot_level: 'ai_optimized',
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
else if (type.includes('security-identity')) {
|
|
2623
|
+
rows.push({
|
|
2624
|
+
capability: 'Threat Detection',
|
|
2625
|
+
current_level: 'rule_based',
|
|
2626
|
+
peer_levels: peerLevels(peers, 'ai_optimized', 'automated', 'ai_optimized'),
|
|
2627
|
+
post_pilot_level: 'ai_optimized',
|
|
2628
|
+
}, {
|
|
2629
|
+
capability: 'Zero Trust Architecture',
|
|
2630
|
+
current_level: 'manual',
|
|
2631
|
+
peer_levels: peerLevels(peers, 'automated', 'ai_optimized', 'automated'),
|
|
2632
|
+
post_pilot_level: 'automated',
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
else {
|
|
2636
|
+
// Generic fallback — one additional row
|
|
2637
|
+
rows.push({
|
|
2638
|
+
capability: 'Enterprise Automation',
|
|
2639
|
+
current_level: 'manual',
|
|
2640
|
+
peer_levels: peerLevels(peers, 'automated', 'automated', 'ai_optimized'),
|
|
2641
|
+
post_pilot_level: 'automated',
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
return rows;
|
|
2645
|
+
}
|
|
2646
|
+
/** Build peer_levels object from an ordered list matching the peers array. */
|
|
2647
|
+
function peerLevels(peers, ...levels) {
|
|
2648
|
+
const result = {};
|
|
2649
|
+
for (let i = 0; i < peers.length; i++) {
|
|
2650
|
+
result[peers[i].name] = levels[i] ?? 'manual';
|
|
2651
|
+
}
|
|
2652
|
+
return result;
|
|
2653
|
+
}
|
|
2654
|
+
/** Derive gap-to-close items from the capability maturity rows. */
|
|
2655
|
+
function buildGapToClose(rows) {
|
|
2656
|
+
return rows.map((row) => {
|
|
2657
|
+
const fromRank = MATURITY_RANK[row.current_level];
|
|
2658
|
+
const toRank = MATURITY_RANK[row.post_pilot_level];
|
|
2659
|
+
const levelsJumped = Math.max(0, toRank - fromRank);
|
|
2660
|
+
// Compare against peers to frame the outcome
|
|
2661
|
+
const peerRanks = Object.values(row.peer_levels).map((l) => MATURITY_RANK[l]);
|
|
2662
|
+
const topPeerRank = peerRanks.length > 0 ? Math.max(...peerRanks) : 0;
|
|
2663
|
+
const medianPeerRank = peerRanks.length > 0
|
|
2664
|
+
? peerRanks.slice().sort((a, b) => a - b)[Math.floor(peerRanks.length / 2)]
|
|
2665
|
+
: 0;
|
|
2666
|
+
let vs;
|
|
2667
|
+
if (toRank >= topPeerRank)
|
|
2668
|
+
vs = 'Matches or exceeds top peer';
|
|
2669
|
+
else if (toRank >= medianPeerRank)
|
|
2670
|
+
vs = 'Reaches peer median';
|
|
2671
|
+
else
|
|
2672
|
+
vs = 'Closes gap to trailing peer';
|
|
2673
|
+
return {
|
|
2674
|
+
capability: row.capability,
|
|
2675
|
+
from: row.current_level,
|
|
2676
|
+
to: row.post_pilot_level,
|
|
2677
|
+
levels_jumped: levelsJumped,
|
|
2678
|
+
vs_top_peer: vs,
|
|
2679
|
+
};
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
/** Build risks of inaction, including regulation-driven risks when applicable. */
|
|
2683
|
+
function buildRisksOfInaction(query, scenarioType) {
|
|
2684
|
+
const regulations = detectApplicableRegulations(query);
|
|
2685
|
+
const type = scenarioType.toLowerCase();
|
|
2686
|
+
const risks = [];
|
|
2687
|
+
// Regulation-driven risk (if applicable)
|
|
2688
|
+
if (regulations.length > 0) {
|
|
2689
|
+
const primary = regulations[0];
|
|
2690
|
+
risks.push({
|
|
2691
|
+
risk: `Regulatory non-compliance with ${primary.framework}`,
|
|
2692
|
+
driver: primary.requirements,
|
|
2693
|
+
timeframe: 'Immediate — current reporting cycle',
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
// Standard competitive risks
|
|
2697
|
+
risks.push({
|
|
2698
|
+
risk: 'Operating cost disadvantage vs. peers',
|
|
2699
|
+
driver: 'Peers have realized 20-40% efficiency gains through AI-optimized workflows in this capability area',
|
|
2700
|
+
timeframe: '12-18 months to parity if pilot starts now; 3+ years if deferred',
|
|
2701
|
+
});
|
|
2702
|
+
// Domain-triggered risks
|
|
2703
|
+
if (type.includes('sustainability') || type.includes('esg') || type.includes('energy') || type.includes('utility')) {
|
|
2704
|
+
risks.push({
|
|
2705
|
+
risk: 'Talent and sustainability brand erosion',
|
|
2706
|
+
driver: 'Hybrid workforce and investor ESG preferences penalize firms without automated climate reporting',
|
|
2707
|
+
timeframe: 'Ongoing — compounds annually',
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
else if (type.includes('supply-chain') || type.includes('logistics') || type.includes('vendor')) {
|
|
2711
|
+
risks.push({
|
|
2712
|
+
risk: 'Supply continuity risk from single-sourced dependencies',
|
|
2713
|
+
driver: 'Peers have diversified supplier networks with real-time visibility; tier-2+ disruptions are opaque without integration',
|
|
2714
|
+
timeframe: '6-12 months before next tier-1 disruption event',
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
else if (type.includes('financial-operations') || type.includes('compliance')) {
|
|
2718
|
+
risks.push({
|
|
2719
|
+
risk: 'Audit cost escalation and manual control burden',
|
|
2720
|
+
driver: 'Regulators expect automated evidence collection; manual sampling is rejected in most 2024+ audits',
|
|
2721
|
+
timeframe: 'Next annual audit cycle',
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
else if (type.includes('healthcare') || type.includes('pharma')) {
|
|
2725
|
+
risks.push({
|
|
2726
|
+
risk: 'Patient outcome and safety gaps vs. peer benchmarks',
|
|
2727
|
+
driver: 'Peer institutions use AI-assisted decision support with documented outcome improvements',
|
|
2728
|
+
timeframe: 'Measurable within 12-24 months via published quality scores',
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
else {
|
|
2732
|
+
risks.push({
|
|
2733
|
+
risk: 'Tech-debt accumulation and integration complexity',
|
|
2734
|
+
driver: 'Peer platforms have consolidated fragmented tools; each year of delay adds ~15% to eventual migration cost',
|
|
2735
|
+
timeframe: 'Compounds annually',
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
return risks;
|
|
2739
|
+
}
|
|
2740
|
+
/**
|
|
2741
|
+
* Build a competitive analysis deterministically from a query + extracted
|
|
2742
|
+
* scenario. Used when the LLM doesn't return a `competitive_analysis`
|
|
2743
|
+
* field, or for queries that don't flow through the LLM synthesis path.
|
|
2744
|
+
*
|
|
2745
|
+
* Guarantees (ADR-PIPELINE-062 §4):
|
|
2746
|
+
* - peer_firms are sorted alphabetically
|
|
2747
|
+
* - capability_maturity rows are sorted by worst-current-level first
|
|
2748
|
+
* - deterministic: same (query, scenario_type) produces the same result
|
|
2749
|
+
* - always non-empty: at minimum 3 peers, 4 capability rows, 2 risks
|
|
2750
|
+
*/
|
|
2751
|
+
export function buildFallbackCompetitiveAnalysis(query, extracted) {
|
|
2752
|
+
const peers = inferPeerFirms(extracted.scenario_type).slice()
|
|
2753
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
2754
|
+
const capabilityRows = buildCapabilityMaturity(extracted.scenario_type, peers).slice()
|
|
2755
|
+
.sort((a, b) => MATURITY_RANK[a.current_level] - MATURITY_RANK[b.current_level]);
|
|
2756
|
+
const gapToClose = buildGapToClose(capabilityRows);
|
|
2757
|
+
const risks = buildRisksOfInaction(query, extracted.scenario_type);
|
|
2758
|
+
const sources = [
|
|
2759
|
+
...peers.map((p) => p.public_evidence_source),
|
|
2760
|
+
...detectApplicableRegulations(query).map((r) => `${r.framework} — ${r.applicability}`),
|
|
2761
|
+
'CBRE 2024 Global Workplace Insights',
|
|
2762
|
+
];
|
|
2763
|
+
return {
|
|
2764
|
+
peer_firms: peers,
|
|
2765
|
+
capability_maturity: capabilityRows,
|
|
2766
|
+
gap_to_close: gapToClose,
|
|
2767
|
+
risks_of_inaction: risks,
|
|
2768
|
+
sources,
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
/** Render the competitive analysis as markdown lines (ADR-PIPELINE-062). */
|
|
2772
|
+
function renderCompetitiveSection(ca) {
|
|
2773
|
+
const out = ['## Competitive Position', ''];
|
|
2774
|
+
// Peer firms intro
|
|
2775
|
+
const peerNames = ca.peer_firms.map((p) => p.name).join(', ');
|
|
2776
|
+
out.push(`**Peer firms analyzed:** ${peerNames}.`, '');
|
|
2777
|
+
// Capability maturity table
|
|
2778
|
+
out.push('### Capability Maturity Comparison', '');
|
|
2779
|
+
out.push('Scale: None → Manual → Rule-Based → Automated → AI-Optimized', '');
|
|
2780
|
+
const peerHeaders = ca.peer_firms.map((p) => p.name).join(' | ');
|
|
2781
|
+
out.push(`| Capability | Current | ${peerHeaders} | Post-Pilot |`);
|
|
2782
|
+
out.push(`|---|---|${ca.peer_firms.map(() => '---').join('|')}|---|`);
|
|
2783
|
+
for (const row of ca.capability_maturity) {
|
|
2784
|
+
const peerCells = ca.peer_firms
|
|
2785
|
+
.map((p) => maturityLabel(row.peer_levels[p.name] ?? 'manual'))
|
|
2786
|
+
.join(' | ');
|
|
2787
|
+
out.push(`| **${row.capability}** | ${maturityLabel(row.current_level)} | ${peerCells} | ${maturityLabel(row.post_pilot_level)} |`);
|
|
2788
|
+
}
|
|
2789
|
+
out.push('');
|
|
2790
|
+
// Gap to close
|
|
2791
|
+
out.push('### Gap-to-Close Summary', '');
|
|
2792
|
+
for (const gap of ca.gap_to_close) {
|
|
2793
|
+
const jumpLabel = gap.levels_jumped === 0 ? '(no change)' : `+${gap.levels_jumped} level${gap.levels_jumped > 1 ? 's' : ''}`;
|
|
2794
|
+
out.push(`- **${gap.capability}**: ${maturityLabel(gap.from)} → ${maturityLabel(gap.to)} ${jumpLabel}. ${gap.vs_top_peer}.`);
|
|
2795
|
+
}
|
|
2796
|
+
out.push('');
|
|
2797
|
+
// Risks of inaction
|
|
2798
|
+
out.push('### Cost of Inaction — Competitive Risks', '');
|
|
2799
|
+
for (const r of ca.risks_of_inaction) {
|
|
2800
|
+
out.push(`- **${r.risk}**`);
|
|
2801
|
+
out.push(` - *Driver:* ${r.driver}`);
|
|
2802
|
+
out.push(` - *Timeframe:* ${r.timeframe}`);
|
|
2803
|
+
}
|
|
2804
|
+
out.push('');
|
|
2805
|
+
// Sources
|
|
2806
|
+
if (ca.sources.length > 0) {
|
|
2807
|
+
out.push(`*Sources: ${ca.sources.join('; ')}*`, '');
|
|
2808
|
+
}
|
|
2809
|
+
return out;
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Extract a competitive analysis from LLM output, or fall back to the
|
|
2813
|
+
* deterministic builder. Returns a valid CompetitiveAnalysis in all cases.
|
|
2814
|
+
*/
|
|
2815
|
+
function getOrBuildCompetitiveAnalysis(query, simData, extracted) {
|
|
2816
|
+
// Check if the LLM returned a competitive_analysis field
|
|
2817
|
+
const raw = simData['competitive_analysis'];
|
|
2818
|
+
if (raw && typeof raw === 'object') {
|
|
2819
|
+
const obj = raw;
|
|
2820
|
+
const peers = Array.isArray(obj['peer_firms']) ? obj['peer_firms'] : [];
|
|
2821
|
+
const maturity = Array.isArray(obj['capability_maturity']) ? obj['capability_maturity'] : [];
|
|
2822
|
+
if (peers.length >= 3 && maturity.length >= 3) {
|
|
2823
|
+
// Shape looks valid — trust it but still enforce alphabetical peer sort
|
|
2824
|
+
// for deterministic rendering
|
|
2825
|
+
return raw;
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
// Fall back to deterministic builder
|
|
2829
|
+
return buildFallbackCompetitiveAnalysis(query, extracted);
|
|
2830
|
+
}
|
|
2831
|
+
// ============================================================================
|
|
2832
|
+
// ADR-PIPELINE-061: Domain-Adaptive Phase Gate Criteria
|
|
2833
|
+
// ============================================================================
|
|
2834
|
+
//
|
|
2835
|
+
// Replaces the 3-line hardcoded phase_gates array with a structured builder
|
|
2836
|
+
// that emits quantified, measurable go/no-go criteria per gate with derived
|
|
2837
|
+
// decision authority. Thresholds come from phase-gate-thresholds.ts.
|
|
2838
|
+
import { PHASE_GATE_THRESHOLDS, pct } from './phase-gate-thresholds.js';
|
|
2839
|
+
/**
|
|
2840
|
+
* Derive the decision authority for each gate from the extracted stakeholders.
|
|
2841
|
+
* - Gate 1 (technical): default CTO + VP <primary dept> unless CIO/CTO explicit
|
|
2842
|
+
* - Gate 2 (business): default CFO + CTO; escalates to Steering if present
|
|
2843
|
+
* - Gate 3 (scale): always Steering Committee unless explicitly Board-led
|
|
2844
|
+
*/
|
|
2845
|
+
function deriveDecisionAuthority(extracted, gateIndex) {
|
|
2846
|
+
const stakeholderText = extracted.stakeholders.join(' ').toLowerCase();
|
|
2847
|
+
const hasCFO = /\bcfo|finance\s+lead|finance\s+director/.test(stakeholderText);
|
|
2848
|
+
const hasBoard = /\bboard\b/.test(stakeholderText);
|
|
2849
|
+
const hasSteering = /steering/.test(stakeholderText);
|
|
2850
|
+
const hasVPOps = /vp\s*operations|vice\s*president/.test(stakeholderText);
|
|
2851
|
+
const hasCTO = /\bcto\b|\bcio\b|chief\s*technology/.test(stakeholderText);
|
|
2852
|
+
if (gateIndex === 0) {
|
|
2853
|
+
// Gate 1: Technical Validation — CTO + VP Ops (or the highest technical authority present)
|
|
2854
|
+
if (hasCTO && hasVPOps)
|
|
2855
|
+
return 'CTO + VP Operations';
|
|
2856
|
+
if (hasCTO)
|
|
2857
|
+
return 'CTO + Engineering Lead';
|
|
2858
|
+
return 'CTO + VP Operations';
|
|
2859
|
+
}
|
|
2860
|
+
if (gateIndex === 1) {
|
|
2861
|
+
// Gate 2: Business Validation — CFO + CTO is the default
|
|
2862
|
+
if (hasCFO && hasCTO)
|
|
2863
|
+
return 'CFO + CTO';
|
|
2864
|
+
if (hasCFO)
|
|
2865
|
+
return 'CFO + CTO';
|
|
2866
|
+
if (hasSteering)
|
|
2867
|
+
return 'CFO + Steering Committee';
|
|
2868
|
+
return 'CFO + CTO';
|
|
2869
|
+
}
|
|
2870
|
+
// Gate 3: Scale Readiness — escalates to Steering or Board
|
|
2871
|
+
if (hasBoard)
|
|
2872
|
+
return 'Board + Steering Committee';
|
|
2873
|
+
if (hasSteering)
|
|
2874
|
+
return 'Steering Committee';
|
|
2875
|
+
return 'Steering Committee';
|
|
2876
|
+
}
|
|
2877
|
+
/**
|
|
2878
|
+
* Build quantified phase gate criteria driven by the scenario context.
|
|
2879
|
+
* Replaces the 3 hardcoded generic gates per ADR-PIPELINE-061.
|
|
2880
|
+
*
|
|
2881
|
+
* @param query - The original user query (used for regulation detection)
|
|
2882
|
+
* @param extracted - Parsed scenario fields
|
|
2883
|
+
* @param primarySystem - The primary target system (ERP, platform, etc.)
|
|
2884
|
+
*/
|
|
2885
|
+
export function buildPhaseGates(query, extracted, primarySystem) {
|
|
2886
|
+
const t = PHASE_GATE_THRESHOLDS;
|
|
2887
|
+
const regulations = detectApplicableRegulations(query);
|
|
2888
|
+
const hasCompliance = regulations.length > 0;
|
|
2889
|
+
// ---- Gate 1: Technical Validation ----
|
|
2890
|
+
const gate1Criteria = [
|
|
2891
|
+
{
|
|
2892
|
+
metric: `${primarySystem} API field coverage`,
|
|
2893
|
+
threshold: `>=${pct(t.erpFieldCoveragePct)} of required fields validated`,
|
|
2894
|
+
data_source: `${primarySystem} integration test results`,
|
|
2895
|
+
},
|
|
2896
|
+
{
|
|
2897
|
+
metric: 'Scoring/model accuracy vs. manual baseline',
|
|
2898
|
+
threshold: `>=${pct(t.scoringAccuracyPct)} agreement on a ${extracted.domain_entities[0] ? extracted.domain_entities[0] : 'sample'} dataset`,
|
|
2899
|
+
data_source: 'Side-by-side comparison with domain expert review',
|
|
2900
|
+
},
|
|
2901
|
+
{
|
|
2902
|
+
metric: 'System uptime during pilot',
|
|
2903
|
+
threshold: `>=${pct(t.systemUptimePct)} during business hours`,
|
|
2904
|
+
data_source: 'Health check endpoint logs + alert history',
|
|
2905
|
+
},
|
|
2906
|
+
{
|
|
2907
|
+
metric: 'Audit trail integrity',
|
|
2908
|
+
threshold: `${pct(t.auditIntegrityPct)} hash chain verification pass`,
|
|
2909
|
+
data_source: 'Automated audit verification endpoint (/api/v1/audit/verify)',
|
|
2910
|
+
},
|
|
2911
|
+
];
|
|
2912
|
+
if (hasCompliance) {
|
|
2913
|
+
gate1Criteria.push({
|
|
2914
|
+
metric: `Regulatory mapping (${regulations.map(r => r.framework).slice(0, 2).join(', ')})`,
|
|
2915
|
+
threshold: 'All mandatory controls mapped to implementation artifacts',
|
|
2916
|
+
data_source: 'Compliance matrix cross-referenced with ADRs and test coverage',
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
// ---- Gate 2: Business Validation ----
|
|
2920
|
+
const gate2Criteria = [
|
|
2921
|
+
{
|
|
2922
|
+
metric: 'Realized savings vs. projected (pilot actuals)',
|
|
2923
|
+
threshold: `Within +/-${pct(t.savingsVariancePct)} of simulation base case`,
|
|
2924
|
+
data_source: 'Finance reconciliation of actual vs. projected savings',
|
|
2925
|
+
},
|
|
2926
|
+
{
|
|
2927
|
+
metric: 'User adoption in pilot department',
|
|
2928
|
+
threshold: `>=${pct(t.userAdoptionPct)} of target users active (weekly)`,
|
|
2929
|
+
data_source: 'Usage telemetry (API request logs, decision submissions)',
|
|
2930
|
+
},
|
|
2931
|
+
{
|
|
2932
|
+
metric: 'False positive rate on recommendations',
|
|
2933
|
+
threshold: `<${pct(t.falsePositiveRatePct)} of recommendations flagged as incorrect`,
|
|
2934
|
+
data_source: 'Post-decision audit by domain expert team',
|
|
2935
|
+
},
|
|
2936
|
+
{
|
|
2937
|
+
metric: `${primarySystem} transaction success rate`,
|
|
2938
|
+
threshold: `>=${pct(t.erpTransactionSuccessPct)} (excluding planned dry-run failures)`,
|
|
2939
|
+
data_source: 'Circuit breaker metrics + ERP writeback response logs',
|
|
2940
|
+
},
|
|
2941
|
+
];
|
|
2942
|
+
// ---- Gate 3: Scale Readiness ----
|
|
2943
|
+
const gate3Criteria = [
|
|
2944
|
+
{
|
|
2945
|
+
metric: 'Projected enterprise-wide ROI',
|
|
2946
|
+
threshold: `>=${pct(t.projectedRoiMultiplier)} (with sensitivity analysis, directional confidence >=90%)`,
|
|
2947
|
+
data_source: 'Updated Monte Carlo / multi-variable sensitivity with pilot actuals',
|
|
2948
|
+
},
|
|
2949
|
+
{
|
|
2950
|
+
metric: 'Security audit findings',
|
|
2951
|
+
threshold: `${t.securityCriticalFindings} critical/high findings open at gate review`,
|
|
2952
|
+
data_source: 'Security scan report (SAST + DAST + dependency audit)',
|
|
2953
|
+
},
|
|
2954
|
+
{
|
|
2955
|
+
metric: 'Change management readiness',
|
|
2956
|
+
threshold: `>=${pct(t.changeManagementReadinessPct)} of target users trained and signed off`,
|
|
2957
|
+
data_source: 'HR/training completion records + department sign-off',
|
|
2958
|
+
},
|
|
2959
|
+
{
|
|
2960
|
+
metric: 'Database performance at projected scale',
|
|
2961
|
+
threshold: `<${t.dbLatencyP95Ms}ms p95 query latency at projected data volume`,
|
|
2962
|
+
data_source: 'Load test results against production-equivalent database',
|
|
2963
|
+
},
|
|
2964
|
+
];
|
|
2965
|
+
if (hasCompliance) {
|
|
2966
|
+
gate3Criteria.push({
|
|
2967
|
+
metric: 'Regulatory sign-off',
|
|
2968
|
+
threshold: `${regulations.map(r => r.framework).slice(0, 2).join(' + ')} compliance evidence complete`,
|
|
2969
|
+
data_source: 'Compliance officer review + external auditor attestation',
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
return [
|
|
2973
|
+
{
|
|
2974
|
+
after_phase: 'phase-1',
|
|
2975
|
+
gate: 'Technical Validation',
|
|
2976
|
+
criteria: gate1Criteria,
|
|
2977
|
+
decision_authority: deriveDecisionAuthority(extracted, 0),
|
|
2978
|
+
overall_decision: 'Proceed to Phase 2 (Prototype Build) or request remediation sprint before continuing',
|
|
2979
|
+
},
|
|
2980
|
+
{
|
|
2981
|
+
after_phase: 'phase-2',
|
|
2982
|
+
gate: 'Business Validation',
|
|
2983
|
+
criteria: gate2Criteria,
|
|
2984
|
+
decision_authority: deriveDecisionAuthority(extracted, 1),
|
|
2985
|
+
overall_decision: 'Proceed to Phase 3 (Pilot Expansion) or extend Phase 2 for additional tuning',
|
|
2986
|
+
},
|
|
2987
|
+
{
|
|
2988
|
+
after_phase: 'phase-3',
|
|
2989
|
+
gate: 'Scale Readiness',
|
|
2990
|
+
criteria: gate3Criteria,
|
|
2991
|
+
decision_authority: deriveDecisionAuthority(extracted, 2),
|
|
2992
|
+
overall_decision: 'Approve full enterprise deployment, phase by region, or defer with documented rationale',
|
|
2993
|
+
},
|
|
2994
|
+
];
|
|
2995
|
+
}
|
|
2242
2996
|
// ============================================================================
|
|
2243
2997
|
// Roadmap Artifact Builder
|
|
2244
2998
|
// ============================================================================
|
|
@@ -2303,6 +3057,43 @@ export function buildRoadmapArtifact(query, simulationResult, platformResults) {
|
|
|
2303
3057
|
],
|
|
2304
3058
|
},
|
|
2305
3059
|
];
|
|
3060
|
+
// ADR-PIPELINE-071: Week-4 consensus gate on contested runs.
|
|
3061
|
+
const roadmapConsensus = populateConsensusSnapshot(simulationResult, platformResults);
|
|
3062
|
+
const consensusGateInfo = consensusGate(roadmapConsensus);
|
|
3063
|
+
// ADR-PIPELINE-079: Resolve pilot start date and project absolute dates
|
|
3064
|
+
// onto every phase + gate so the roadmap anchors to real calendar dates.
|
|
3065
|
+
const pilotStart = resolvePilotStart(query, null, null);
|
|
3066
|
+
const { datedPhases, datedGates } = projectDates(pilotStart.startDate, defaultPhases.map(p => ({
|
|
3067
|
+
id: p['id'] ?? 'phase',
|
|
3068
|
+
name: p['name'] ?? 'Phase',
|
|
3069
|
+
duration: p['duration'] ?? '3 weeks',
|
|
3070
|
+
})));
|
|
3071
|
+
// Enrich the default phases with absolute dates
|
|
3072
|
+
const enrichedPhases = defaultPhases.map((phase, i) => {
|
|
3073
|
+
const dated = datedPhases[i];
|
|
3074
|
+
if (!dated)
|
|
3075
|
+
return phase;
|
|
3076
|
+
return {
|
|
3077
|
+
...phase,
|
|
3078
|
+
startDate: dated.startDate,
|
|
3079
|
+
endDate: dated.endDate,
|
|
3080
|
+
durationWeeks: dated.durationWeeks,
|
|
3081
|
+
};
|
|
3082
|
+
});
|
|
3083
|
+
// Enrich the phase gates with absolute dates
|
|
3084
|
+
const gates = buildPhaseGates(query, extracted, primarySystem);
|
|
3085
|
+
const enrichedGates = gates.map((gate) => {
|
|
3086
|
+
const afterPhase = gate.after_phase;
|
|
3087
|
+
const datedGate = datedGates.find(g => g.afterPhase === afterPhase);
|
|
3088
|
+
return {
|
|
3089
|
+
after_phase: gate.after_phase,
|
|
3090
|
+
gate: gate.gate,
|
|
3091
|
+
criteria: gate.criteria,
|
|
3092
|
+
decision_authority: gate.decision_authority,
|
|
3093
|
+
overall_decision: gate.overall_decision,
|
|
3094
|
+
...(datedGate ? { gate_date: datedGate.gateDate } : {}),
|
|
3095
|
+
};
|
|
3096
|
+
});
|
|
2306
3097
|
return {
|
|
2307
3098
|
metadata: {
|
|
2308
3099
|
title: `Roadmap: ${query}`,
|
|
@@ -2311,7 +3102,13 @@ export function buildRoadmapArtifact(query, simulationResult, platformResults) {
|
|
|
2311
3102
|
estimated_duration: timeline,
|
|
2312
3103
|
status: 'draft',
|
|
2313
3104
|
},
|
|
2314
|
-
|
|
3105
|
+
// ADR-PIPELINE-079: absolute pilot start date with source metadata
|
|
3106
|
+
pilot_start: {
|
|
3107
|
+
date: pilotStart.startDate,
|
|
3108
|
+
source: pilotStart.source,
|
|
3109
|
+
reasoning: pilotStart.reasoning,
|
|
3110
|
+
},
|
|
3111
|
+
phases: enrichedPhases,
|
|
2315
3112
|
success_criteria: safeArray(simData, 'recommendations').map(r => String(r)),
|
|
2316
3113
|
// ADR-PIPELINE-024: Always include domain-appropriate risks
|
|
2317
3114
|
risk_factors: generateDomainRisks(query, extracted).map(r => ({
|
|
@@ -2328,12 +3125,16 @@ export function buildRoadmapArtifact(query, simulationResult, platformResults) {
|
|
|
2328
3125
|
cost_estimate: extractAgentData(platformResults, 'costops', 'forecast'),
|
|
2329
3126
|
implementation_plan: extractAgentData(platformResults, 'copilot', 'planner'),
|
|
2330
3127
|
resource_requirements: extractAgentData(platformResults, 'costops', 'budget'),
|
|
2331
|
-
// ADR-PIPELINE-
|
|
2332
|
-
phase_gates:
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
3128
|
+
// ADR-PIPELINE-061: Quantified phase gate criteria with derived decision authority
|
|
3129
|
+
phase_gates: enrichedGates,
|
|
3130
|
+
// ADR-PIPELINE-071: Consensus tier metadata + Week-4 gate when contested.
|
|
3131
|
+
consensus: {
|
|
3132
|
+
tier: roadmapConsensus.tier,
|
|
3133
|
+
agreement_pct: roadmapConsensus.agreementLevelPct,
|
|
3134
|
+
precision_pct: roadmapConsensus.precisionConfidencePct,
|
|
3135
|
+
directional_pct: roadmapConsensus.directionalConfidencePct,
|
|
3136
|
+
},
|
|
3137
|
+
consensus_gate: consensusGateInfo,
|
|
2337
3138
|
};
|
|
2338
3139
|
}
|
|
2339
3140
|
// ============================================================================
|
|
@@ -2377,6 +3178,48 @@ export function buildRiskAssessment(query, simulationResult, platformResults) {
|
|
|
2377
3178
|
});
|
|
2378
3179
|
}
|
|
2379
3180
|
}
|
|
3181
|
+
// ADR-PIPELINE-071: Inject the analytical-uncertainty risk on contested
|
|
3182
|
+
// runs so the heat map names the dissent rather than dressing it up as
|
|
3183
|
+
// tone-of-voice.
|
|
3184
|
+
const riskConsensus = populateConsensusSnapshot(simulationResult, platformResults);
|
|
3185
|
+
const uncertaintyRisk = analyticalUncertaintyRisk(riskConsensus);
|
|
3186
|
+
if (uncertaintyRisk) {
|
|
3187
|
+
const already = domainRisks.some(r => r.risk.toLowerCase().includes('analytical uncertainty'));
|
|
3188
|
+
if (!already) {
|
|
3189
|
+
domainRisks.push({
|
|
3190
|
+
risk: uncertaintyRisk.risk,
|
|
3191
|
+
category: uncertaintyRisk.category,
|
|
3192
|
+
likelihood: uncertaintyRisk.likelihood,
|
|
3193
|
+
impact: uncertaintyRisk.impact,
|
|
3194
|
+
score: uncertaintyRisk.score,
|
|
3195
|
+
mitigation: uncertaintyRisk.mitigation,
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
// ADR-PIPELINE-070: Mandatory Workforce Impact rows when the engagement
|
|
3200
|
+
// is labor-intensive. Template rows are merged with agent-supplied rows;
|
|
3201
|
+
// a row supplied by an agent with the same risk text wins.
|
|
3202
|
+
const domainAnalysis = extractDomainAnalysis(simData, query);
|
|
3203
|
+
const laborProfile = classifyLaborIntensity(domainAnalysis, query);
|
|
3204
|
+
if (isLaborIntensive(laborProfile)) {
|
|
3205
|
+
for (const wf of workforceRiskTemplate(laborProfile)) {
|
|
3206
|
+
const already = domainRisks.some(r => r.risk.toLowerCase().includes(wf.risk.toLowerCase().slice(0, 20)));
|
|
3207
|
+
if (already)
|
|
3208
|
+
continue;
|
|
3209
|
+
const score = (wf.likelihood === 'High' ? 3 : wf.likelihood === 'Medium' ? 2 : 1) *
|
|
3210
|
+
(wf.severity === 'High' ? 3 : wf.severity === 'Medium' ? 2 : 1);
|
|
3211
|
+
domainRisks.push({
|
|
3212
|
+
risk: wf.risk,
|
|
3213
|
+
category: 'Workforce',
|
|
3214
|
+
likelihood: wf.likelihood,
|
|
3215
|
+
impact: wf.severity,
|
|
3216
|
+
score,
|
|
3217
|
+
mitigation: `${wf.mitigation} *Template risk — refine with HR during pilot scoping*`,
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
// Re-sort so high-score workforce risks bubble to the top of the heat map.
|
|
3221
|
+
domainRisks.sort((a, b) => b.score - a.score);
|
|
3222
|
+
}
|
|
2380
3223
|
const maxScore = domainRisks[0]?.score ?? 0;
|
|
2381
3224
|
const overallRisk = maxScore >= 6 ? 'MEDIUM' : maxScore >= 3 ? 'LOW-MEDIUM' : 'LOW';
|
|
2382
3225
|
// Security findings
|
|
@@ -2419,12 +3262,183 @@ export function buildRiskAssessment(query, simulationResult, platformResults) {
|
|
|
2419
3262
|
...sentinelAgents.map(a => `sentinel/${a.agent}`),
|
|
2420
3263
|
...shieldAgents.map(a => `shield/${a.agent}`),
|
|
2421
3264
|
].filter(Boolean),
|
|
3265
|
+
workforce_impact: isLaborIntensive(laborProfile)
|
|
3266
|
+
? {
|
|
3267
|
+
intensity: laborProfile.intensity,
|
|
3268
|
+
sector: laborProfile.sector,
|
|
3269
|
+
unionization_risk: laborProfile.unionizationRisk,
|
|
3270
|
+
role_types: laborProfile.roleTypes,
|
|
3271
|
+
region_multipliers: laborProfile.regionMultipliers,
|
|
3272
|
+
reasoning: laborProfile.reasoning,
|
|
3273
|
+
}
|
|
3274
|
+
: null,
|
|
3275
|
+
consensus_tier: {
|
|
3276
|
+
tier: riskConsensus.tier,
|
|
3277
|
+
agreement_pct: riskConsensus.agreementLevelPct,
|
|
3278
|
+
precision_pct: riskConsensus.precisionConfidencePct,
|
|
3279
|
+
directional_pct: riskConsensus.directionalConfidencePct,
|
|
3280
|
+
dissenting_agents: riskConsensus.dissentingAgents,
|
|
3281
|
+
load_bearing_assumptions: riskConsensus.loadBearingAssumptions,
|
|
3282
|
+
},
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
/**
|
|
3286
|
+
* Select 5-7 sensitivity variables based on the scenario type. The first
|
|
3287
|
+
* four are domain-neutral; the remainder are domain-triggered so the output
|
|
3288
|
+
* is tailored to the scenario.
|
|
3289
|
+
*/
|
|
3290
|
+
function selectSensitivityVariables(scenarioType) {
|
|
3291
|
+
const base = [
|
|
3292
|
+
{ variable: 'Input price variance', range: '±15%', impactFraction: 0.15, direction: 'bidirectional' },
|
|
3293
|
+
{ variable: 'Adoption rate', range: '40% → 70%', impactFraction: 0.20, direction: 'bidirectional' },
|
|
3294
|
+
{ variable: 'Implementation timeline', range: '±3 months', impactFraction: 0.25, direction: 'downside_only' },
|
|
3295
|
+
{ variable: 'Scope expansion', range: 'Pilot → Enterprise', impactFraction: 0.60, direction: 'upside_only' },
|
|
3296
|
+
];
|
|
3297
|
+
// Domain-specific additions (1-3 variables depending on scenario)
|
|
3298
|
+
const type = scenarioType.toLowerCase();
|
|
3299
|
+
if (type.includes('sustainability') || type.includes('esg') || type.includes('energy') || type.includes('utility')) {
|
|
3300
|
+
base.push({ variable: 'Data coverage', range: '70% → 95%', impactFraction: 0.20, direction: 'downside_only' }, { variable: 'Carbon price', range: '$30 → $100/t', impactFraction: 0.18, direction: 'upside_only' });
|
|
3301
|
+
}
|
|
3302
|
+
else if (type.includes('supply-chain') || type.includes('logistics') || type.includes('fleet-transportation') || type.includes('vendor')) {
|
|
3303
|
+
base.push({ variable: 'Supplier reliability', range: '90% → 99.9%', impactFraction: 0.22, direction: 'bidirectional' }, { variable: 'Lead-time variance', range: '±2 weeks', impactFraction: 0.12, direction: 'downside_only' });
|
|
3304
|
+
}
|
|
3305
|
+
else if (type.includes('financial-operations') || type.includes('compliance') || type.includes('governance') || type.includes('audit')) {
|
|
3306
|
+
base.push({ variable: 'Audit cost multiplier', range: '1× → 2.5×', impactFraction: 0.18, direction: 'downside_only' }, { variable: 'Regulatory scope change', range: '0 → 2 new frameworks', impactFraction: 0.15, direction: 'downside_only' });
|
|
3307
|
+
}
|
|
3308
|
+
else if (type.includes('hr-workforce') || type.includes('talent')) {
|
|
3309
|
+
base.push({ variable: 'Attrition rate', range: '5% → 20%', impactFraction: 0.24, direction: 'downside_only' }, { variable: 'Training completion', range: '60% → 90%', impactFraction: 0.14, direction: 'upside_only' });
|
|
3310
|
+
}
|
|
3311
|
+
else if (type.includes('manufacturing') || type.includes('asset-maintenance') || type.includes('pharma')) {
|
|
3312
|
+
base.push({ variable: 'Equipment uptime', range: '92% → 98%', impactFraction: 0.20, direction: 'bidirectional' }, { variable: 'Yield variance', range: '±5pp', impactFraction: 0.16, direction: 'bidirectional' });
|
|
3313
|
+
}
|
|
3314
|
+
else if (type.includes('healthcare')) {
|
|
3315
|
+
base.push({ variable: 'Patient volume', range: '±15%', impactFraction: 0.18, direction: 'bidirectional' }, { variable: 'Payer mix shift', range: '±10pp', impactFraction: 0.14, direction: 'downside_only' });
|
|
3316
|
+
}
|
|
3317
|
+
else if (type.includes('data-analytics') || type.includes('system-migration') || type.includes('security')) {
|
|
3318
|
+
base.push({ variable: 'Data migration completeness', range: '85% → 99%', impactFraction: 0.22, direction: 'upside_only' }, { variable: 'Integration complexity', range: '1× → 2×', impactFraction: 0.18, direction: 'downside_only' });
|
|
3319
|
+
}
|
|
3320
|
+
else {
|
|
3321
|
+
// Generic fallback — ensures 5+ rows even for unknown scenario types
|
|
3322
|
+
base.push({ variable: 'Market conditions', range: '±15%', impactFraction: 0.15, direction: 'bidirectional' }, { variable: 'Organizational readiness', range: '60% → 90%', impactFraction: 0.18, direction: 'downside_only' });
|
|
3323
|
+
}
|
|
3324
|
+
return base;
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* Build the sensitivity table from specs. Returns rows sorted by absolute
|
|
3328
|
+
* impact magnitude (rank 1 = biggest driver). Deterministic for a given
|
|
3329
|
+
* (baseSavings, scenarioType) pair.
|
|
3330
|
+
*/
|
|
3331
|
+
export function buildSensitivityTable(baseSavings, scenarioType, laborProfile) {
|
|
3332
|
+
if (!(baseSavings > 0) || !isFinite(baseSavings))
|
|
3333
|
+
return [];
|
|
3334
|
+
const specs = selectSensitivityVariables(scenarioType);
|
|
3335
|
+
// ADR-PIPELINE-070: When the engagement is labor-intensive, prepend a
|
|
3336
|
+
// workforce variable so change-management overruns are visible in the
|
|
3337
|
+
// tornado. The variable is downside-only — overruns shrink savings.
|
|
3338
|
+
if (laborProfile && isLaborIntensive(laborProfile)) {
|
|
3339
|
+
const wfRow = workforceSensitivityRow(laborProfile);
|
|
3340
|
+
if (wfRow && !specs.some(s => s.variable === wfRow.variable)) {
|
|
3341
|
+
specs.push({
|
|
3342
|
+
variable: wfRow.variable,
|
|
3343
|
+
range: wfRow.range,
|
|
3344
|
+
impactFraction: wfRow.impactPctOfPilot,
|
|
3345
|
+
direction: 'downside_only',
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
const rows = specs.map((spec) => {
|
|
3350
|
+
const magnitude = Math.round(baseSavings * spec.impactFraction);
|
|
3351
|
+
const signedImpact = spec.direction === 'downside_only' ? -magnitude :
|
|
3352
|
+
spec.direction === 'upside_only' ? +magnitude :
|
|
3353
|
+
+magnitude; // bidirectional: store positive magnitude; low/high handled below
|
|
3354
|
+
// Low/high cases depend on direction.
|
|
3355
|
+
let lowCaseUsd;
|
|
3356
|
+
let highCaseUsd;
|
|
3357
|
+
if (spec.direction === 'upside_only') {
|
|
3358
|
+
lowCaseUsd = Math.round(baseSavings);
|
|
3359
|
+
highCaseUsd = Math.round(baseSavings + magnitude);
|
|
3360
|
+
}
|
|
3361
|
+
else if (spec.direction === 'downside_only') {
|
|
3362
|
+
lowCaseUsd = Math.round(baseSavings - magnitude);
|
|
3363
|
+
highCaseUsd = Math.round(baseSavings);
|
|
3364
|
+
}
|
|
3365
|
+
else {
|
|
3366
|
+
lowCaseUsd = Math.round(baseSavings - magnitude);
|
|
3367
|
+
highCaseUsd = Math.round(baseSavings + magnitude);
|
|
3368
|
+
}
|
|
3369
|
+
return {
|
|
3370
|
+
variable: spec.variable,
|
|
3371
|
+
range: spec.range,
|
|
3372
|
+
baselineUsd: Math.round(baseSavings),
|
|
3373
|
+
lowCaseUsd,
|
|
3374
|
+
highCaseUsd,
|
|
3375
|
+
impactUsd: magnitude,
|
|
3376
|
+
impactPercent: (signedImpact / baseSavings) * 100,
|
|
3377
|
+
rank: 0, // assigned after sort
|
|
3378
|
+
direction: spec.direction,
|
|
3379
|
+
};
|
|
3380
|
+
});
|
|
3381
|
+
// Sort by absolute magnitude, assign 1-based ranks
|
|
3382
|
+
rows.sort((a, b) => b.impactUsd - a.impactUsd);
|
|
3383
|
+
return rows.map((row, idx) => ({ ...row, rank: idx + 1 }));
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Render the sensitivity rows as a markdown table, ASCII tornado chart,
|
|
3387
|
+
* and a dynamic key-takeaway paragraph linking the top 2 drivers to the
|
|
3388
|
+
* pilot design.
|
|
3389
|
+
*/
|
|
3390
|
+
function renderSensitivitySection(rows, baselineTotalUsd) {
|
|
3391
|
+
const out = [];
|
|
3392
|
+
out.push('## Sensitivity Analysis', '');
|
|
3393
|
+
if (rows.length === 0) {
|
|
3394
|
+
out.push('*Baseline savings figure could not be parsed from the financial model — sensitivity table omitted. Provide `fin.revenue` with a recognized currency format to enable tornado ranking.*', '');
|
|
3395
|
+
return out;
|
|
3396
|
+
}
|
|
3397
|
+
const fmtMoney = (n) => {
|
|
3398
|
+
const abs = Math.abs(n);
|
|
3399
|
+
if (abs >= 1_000_000)
|
|
3400
|
+
return `$${(n / 1_000_000).toFixed(2)}M`;
|
|
3401
|
+
if (abs >= 1_000)
|
|
3402
|
+
return `$${(n / 1_000).toFixed(0)}K`;
|
|
3403
|
+
return `$${Math.round(n).toLocaleString()}`;
|
|
2422
3404
|
};
|
|
3405
|
+
out.push(`**Base case:** ${fmtMoney(baselineTotalUsd)} projected annual impact.`, '');
|
|
3406
|
+
out.push(`**Variables tested:** ${rows.length}. Each row varies a single assumption while holding all others constant.`, '');
|
|
3407
|
+
out.push('');
|
|
3408
|
+
// Table
|
|
3409
|
+
out.push('| Rank | Variable | Range | Pessimistic | Optimistic | Impact |');
|
|
3410
|
+
out.push('|------|----------|-------|-------------|------------|--------|');
|
|
3411
|
+
for (const row of rows) {
|
|
3412
|
+
out.push(`| ${row.rank} | ${row.variable} | ${row.range} | ${fmtMoney(row.lowCaseUsd)} | ${fmtMoney(row.highCaseUsd)} | ±${fmtMoney(row.impactUsd)} |`);
|
|
3413
|
+
}
|
|
3414
|
+
out.push('');
|
|
3415
|
+
// ASCII tornado chart (bars proportional to largest impact)
|
|
3416
|
+
out.push('### Tornado Chart', '');
|
|
3417
|
+
out.push('```');
|
|
3418
|
+
const maxImpact = rows[0]?.impactUsd ?? 1;
|
|
3419
|
+
const barWidth = 32;
|
|
3420
|
+
const nameWidth = Math.max(...rows.map(r => r.variable.length));
|
|
3421
|
+
for (const row of rows) {
|
|
3422
|
+
const barLen = Math.max(1, Math.round((row.impactUsd / maxImpact) * barWidth));
|
|
3423
|
+
const bar = '█'.repeat(barLen);
|
|
3424
|
+
const name = row.variable.padEnd(nameWidth);
|
|
3425
|
+
out.push(` ${name} ${bar} ${fmtMoney(row.impactUsd)}`);
|
|
3426
|
+
}
|
|
3427
|
+
out.push('```', '');
|
|
3428
|
+
// Key takeaway — dynamically references top-2 drivers and lowest-impact variable
|
|
3429
|
+
const top1 = rows[0];
|
|
3430
|
+
const top2 = rows[1];
|
|
3431
|
+
const bottom = rows[rows.length - 1];
|
|
3432
|
+
const takeaway = top2
|
|
3433
|
+
? `**Key takeaway:** The business case is most sensitive to **${top1.variable}** (±${fmtMoney(top1.impactUsd)}) and **${top2.variable}** (±${fmtMoney(top2.impactUsd)}). The pilot is designed to reduce uncertainty on these two drivers specifically. **${bottom.variable}** has the smallest impact (±${fmtMoney(bottom.impactUsd)}), meaning the ROI holds even if that estimate is off by ±20%.`
|
|
3434
|
+
: `**Key takeaway:** Only one variable was tested — **${top1.variable}** (±${fmtMoney(top1.impactUsd)}). Add scenario context to enable multi-variable tornado analysis.`;
|
|
3435
|
+
out.push(takeaway, '');
|
|
3436
|
+
return out;
|
|
2423
3437
|
}
|
|
2424
3438
|
// ============================================================================
|
|
2425
3439
|
// ADR-PIPELINE-020: Financial Analysis Renderer (costops agents)
|
|
2426
3440
|
// ============================================================================
|
|
2427
|
-
export function renderFinancialAnalysis(query, simulationResult, platformResults) {
|
|
3441
|
+
export function renderFinancialAnalysis(query, simulationResult, platformResults, runDir) {
|
|
2428
3442
|
const now = new Date().toISOString();
|
|
2429
3443
|
const problemStatement = distillProblemStatement(query);
|
|
2430
3444
|
const simPayload = extractSignalPayload(simulationResult);
|
|
@@ -2438,24 +3452,58 @@ export function renderFinancialAnalysis(query, simulationResult, platformResults
|
|
|
2438
3452
|
const attributionData = extractAgentData(platformResults, 'costops', 'attribution');
|
|
2439
3453
|
const budgetData = extractAgentData(platformResults, 'costops', 'budget');
|
|
2440
3454
|
const tradeoffData = extractAgentData(platformResults, 'costops', 'tradeoff');
|
|
2441
|
-
// ADR-PIPELINE-
|
|
2442
|
-
//
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
3455
|
+
// ADR-PIPELINE-066: 3-tier precedence for financial figures.
|
|
3456
|
+
// Tier 1 — unit-economics.json manifest from the generated prototype
|
|
3457
|
+
// Tier 2 — structured figures from agent results (costops/platform)
|
|
3458
|
+
// Tier 3 — per-employee heuristic fallback (with visible warning banner)
|
|
3459
|
+
const manifestResult = loadUnitEconomics(runDir ?? null);
|
|
3460
|
+
let unitEconomicsSource = 'heuristic';
|
|
3461
|
+
let manifestModel = null;
|
|
3462
|
+
let manifestWarnings = [];
|
|
3463
|
+
if (manifestResult.manifest) {
|
|
3464
|
+
manifestModel = buildFinancialModelFromUnitEconomics(manifestResult.manifest);
|
|
3465
|
+
fin.budget = manifestModel.budget;
|
|
3466
|
+
fin.roi = manifestModel.roi;
|
|
3467
|
+
fin.npv = manifestModel.npv;
|
|
3468
|
+
fin.payback = manifestModel.payback;
|
|
3469
|
+
fin.revenue = manifestModel.revenue;
|
|
3470
|
+
fin.costSavings = manifestModel.costSavings;
|
|
2457
3471
|
fin.hasData = true;
|
|
3472
|
+
fin.isEstimated = false;
|
|
3473
|
+
unitEconomicsSource = 'manifest';
|
|
3474
|
+
const consistency = enforceManifestConsistency(manifestResult.manifest, {
|
|
3475
|
+
measuredSavingsUsd: manifestResult.manifest.annual_measured_savings_usd,
|
|
3476
|
+
enterpriseSavingsUsd: manifestResult.manifest.annual_extrapolated_savings_usd,
|
|
3477
|
+
});
|
|
3478
|
+
if (!consistency.passed)
|
|
3479
|
+
manifestWarnings = consistency.violations;
|
|
3480
|
+
}
|
|
3481
|
+
else if (fin.hasData && !fin.isEstimated && fin.budget && fin.roi && fin.npv) {
|
|
3482
|
+
// Tier 2 — structured agent/costops data already populated the model.
|
|
3483
|
+
unitEconomicsSource = 'agent';
|
|
3484
|
+
}
|
|
3485
|
+
else {
|
|
3486
|
+
// Tier 3 — heuristic fallback. synthesizeFinancials may have already
|
|
3487
|
+
// filled the fields via estimateFinancialsFromQuery (isEstimated=true);
|
|
3488
|
+
// in that case we leave the values alone and just flag the path.
|
|
3489
|
+
if (!fin.hasData || (!fin.budget && !fin.roi && !fin.npv)) {
|
|
3490
|
+
const estimated = estimateFinancialsFromQuery(query);
|
|
3491
|
+
if (!fin.budget)
|
|
3492
|
+
fin.budget = estimated.budget;
|
|
3493
|
+
if (!fin.roi)
|
|
3494
|
+
fin.roi = estimated.roi;
|
|
3495
|
+
if (!fin.npv)
|
|
3496
|
+
fin.npv = estimated.npv;
|
|
3497
|
+
if (!fin.payback)
|
|
3498
|
+
fin.payback = estimated.payback;
|
|
3499
|
+
if (!fin.revenue)
|
|
3500
|
+
fin.revenue = estimated.revenue;
|
|
3501
|
+
if (!fin.costSavings)
|
|
3502
|
+
fin.costSavings = estimated.costSavings;
|
|
3503
|
+
fin.hasData = true;
|
|
3504
|
+
}
|
|
2458
3505
|
fin.isEstimated = true;
|
|
3506
|
+
unitEconomicsSource = 'heuristic';
|
|
2459
3507
|
}
|
|
2460
3508
|
const lines = [
|
|
2461
3509
|
'# Financial Analysis',
|
|
@@ -2463,18 +3511,43 @@ export function renderFinancialAnalysis(query, simulationResult, platformResults
|
|
|
2463
3511
|
`**Date:** ${now}`,
|
|
2464
3512
|
`**Subject:** ${problemStatement}`,
|
|
2465
3513
|
'',
|
|
2466
|
-
'---',
|
|
2467
|
-
'',
|
|
2468
|
-
`## Investment Summary${fin.isEstimated ? ' (Estimated from Organization Profile)' : ''}`, '',
|
|
2469
|
-
'| Metric | Value |',
|
|
2470
|
-
'|--------|-------|',
|
|
2471
|
-
`| Total Investment | ${fin.budget} |`,
|
|
2472
|
-
`| Expected ROI | ${fin.roi} |`,
|
|
2473
|
-
`| 5-Year NPV | ${fin.npv} |`,
|
|
2474
|
-
`| Payback Period | ${fin.payback} |`,
|
|
2475
|
-
`| Revenue / Savings Impact | ${fin.revenue || fin.costSavings} |`,
|
|
2476
|
-
'',
|
|
2477
3514
|
];
|
|
3515
|
+
// ADR-PIPELINE-066 §2: Mandatory fallback warning when the heuristic is used.
|
|
3516
|
+
if (unitEconomicsSource === 'heuristic') {
|
|
3517
|
+
lines.push('> ⚠️ **Financial model: heuristic fallback** — no prototype `unit-economics.json` found.', '> Figures below are derived from a per-employee/industry benchmark, not bottom-up unit economics.', '> Regenerate the prototype and re-run this renderer to replace with measured unit savings.', '');
|
|
3518
|
+
}
|
|
3519
|
+
else if (unitEconomicsSource === 'manifest' && manifestResult.manifest) {
|
|
3520
|
+
const m = manifestResult.manifest;
|
|
3521
|
+
lines.push(`> ✅ **Financial model: prototype unit economics** (sector: ${m.sector}, unit: ${m.domain_unit.label})`, `> Source: \`${manifestResult.inspectedPath ?? 'unit-economics.json'}\` · Method: ${m.extrapolation_method}`, '');
|
|
3522
|
+
if (manifestWarnings.length > 0) {
|
|
3523
|
+
lines.push('> ⚠️ **Unit economics consistency warnings:**', ...manifestWarnings.map(v => `> - ${v}`), '');
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
// ADR-PIPELINE-073: tag every row in the financial-analysis Investment
|
|
3527
|
+
// Summary table with inline fcv comments. Kind-specific scopes so the
|
|
3528
|
+
// extractor + FCR-011 / FCR-012 see authoritative labels.
|
|
3529
|
+
lines.push('---', '', `## Investment Summary${fin.isEstimated ? ' (Estimated from Organization Profile)' : unitEconomicsSource === 'manifest' ? ' (from Prototype Unit Economics)' : ''}`, '', '| Metric | Value |', '|--------|-------|', `| Total Investment | ${fcvTag('investment', 'full-program', 'financial-analysis')} ${fin.budget} |`, `| Expected ROI | ${fcvTag('roi', 'full-program', 'financial-analysis')} ${fin.roi} |`, `| 5-Year NPV | ${fcvTag('npv', 'full-program', 'financial-analysis')} ${fin.npv} |`, `| Payback Period | ${fcvTag('timeline', 'full-program', 'financial-analysis')} ${fin.payback} |`, `| Revenue / Savings Impact | ${fcvTag('savings', 'full-program', 'financial-analysis')} ${fin.revenue || fin.costSavings} |`, '');
|
|
3530
|
+
// ADR-PIPELINE-070: Workforce exposure block when intensity ≥ medium.
|
|
3531
|
+
// Mandatory for hospitality/retail/fleet/healthcare/CRE/etc. — calls out
|
|
3532
|
+
// role count, EMEA union exposure, change-mgmt budget, and reallocation
|
|
3533
|
+
// neutrality so the financial story includes the labor side.
|
|
3534
|
+
const financialDomainAnalysis = extractDomainAnalysis(simData, query);
|
|
3535
|
+
const financialLaborProfile = classifyLaborIntensity(financialDomainAnalysis, query);
|
|
3536
|
+
if (isLaborIntensive(financialLaborProfile)) {
|
|
3537
|
+
const pilotInvestmentUsd = parseUsdRangeMid(fin.budget) ?? 1_000_000;
|
|
3538
|
+
const operationalUnits = manifestResult.manifest
|
|
3539
|
+
? primaryScopeCount(manifestResult.manifest)
|
|
3540
|
+
: undefined;
|
|
3541
|
+
const employeesContext = extractEmployeeCount(query, query.toLowerCase());
|
|
3542
|
+
const exposure = estimateWorkforceExposure(financialLaborProfile, {
|
|
3543
|
+
employees: employeesContext,
|
|
3544
|
+
operationalUnits,
|
|
3545
|
+
pilotInvestmentUsd,
|
|
3546
|
+
});
|
|
3547
|
+
if (exposure) {
|
|
3548
|
+
lines.push('## Workforce Exposure (ADR-PIPELINE-070)', '', '| Dimension | Value |', '|-----------|-------|', `| Roles affected | ~${exposure.rolesAffected.toLocaleString()} ${exposure.primaryRoleLabel} |`, `| EMEA union exposure | ~${exposure.emeaUnionExposure.toLocaleString()} roles (${financialLaborProfile.unionizationRisk} unionization risk) |`, `| Change-management budget | $${(exposure.changeMgmtBudgetLowUsd / 1000).toFixed(0)}K – $${(exposure.changeMgmtBudgetHighUsd / 1000).toFixed(0)}K (${(exposure.changeMgmtPct * 100).toFixed(0)}% of pilot investment) |`, `| Reallocation neutrality | ${exposure.reallocationNeutral ? 'Committed (no headcount reduction during pilot)' : 'Not committed'} |`, `| Sector classification | ${financialLaborProfile.sector} (${financialLaborProfile.intensity} labor intensity) |`, '', '*Source: ADR-PIPELINE-070 labor classifier. Refine role counts and change-management budget with HR during pilot scoping.*', '');
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
2478
3551
|
// ROI Analysis
|
|
2479
3552
|
lines.push('## Return on Investment', '');
|
|
2480
3553
|
if (roiData) {
|
|
@@ -2560,37 +3633,33 @@ export function renderFinancialAnalysis(query, simulationResult, platformResults
|
|
|
2560
3633
|
pessCostStr = fmt(baseVal * 1.4);
|
|
2561
3634
|
}
|
|
2562
3635
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
3636
|
+
// ADR-PIPELINE-073: tag scenario rows so they're not flagged as untagged
|
|
3637
|
+
// money figures. All three variants describe the full-program cost; the
|
|
3638
|
+
// variance is visible in the prose ("±20% cost variance"), not the scope.
|
|
3639
|
+
const scenarioTag = fcvTag('investment', 'full-program', 'financial-analysis');
|
|
3640
|
+
lines.push(`| **Base Case** | Plan executes as modeled | ${scenarioTag} ${baseCostStr} | ${fin.roi || 'Projected'} | 50% |`);
|
|
3641
|
+
lines.push(`| Optimistic | Faster adoption, lower integration costs | ${scenarioTag} ${optCostStr} | ${fin.roi ? fin.roi.replace(/\d+/, m => String(Math.round(parseInt(m) * 1.3))) : 'Above base'} | 25% |`);
|
|
3642
|
+
lines.push(`| Pessimistic | Slower adoption, scope growth | ${scenarioTag} ${pessCostStr} | ${fin.roi ? fin.roi.replace(/\d+/, m => String(Math.round(parseInt(m) * 0.6))) : 'Below base'} | 25% |`);
|
|
2566
3643
|
lines.push('', '*Scenario analysis assumes ±20% cost variance and ±30% timeline variance from base case.*', '');
|
|
2567
|
-
// ADR-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
lines.push('|-----------------|-------------------|-----|---------|');
|
|
3644
|
+
// ADR-PIPELINE-063: Multi-variable sensitivity tornado
|
|
3645
|
+
// Implements ADR-PIPELINE-057 §1 — replaces the old single-variable loop
|
|
3646
|
+
// with a ranked tornado chart across 5-7 domain-selected assumptions.
|
|
3647
|
+
let baseSavingsForSensitivity = 0;
|
|
2572
3648
|
if (fin.revenue) {
|
|
2573
|
-
// Parse the base savings figure for sensitivity calculations
|
|
2574
3649
|
const savingsMatch = fin.revenue.match(/\$([0-9,.]+)([KMB]?)/i);
|
|
2575
3650
|
if (savingsMatch) {
|
|
2576
3651
|
const numStr = savingsMatch[1].replace(/,/g, '');
|
|
2577
3652
|
const suffix = (savingsMatch[2] || '').toUpperCase();
|
|
2578
3653
|
const mult = suffix === 'M' ? 1_000_000 : suffix === 'K' ? 1_000 : suffix === 'B' ? 1_000_000_000 : 1;
|
|
2579
|
-
const
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
const baseInvest = budgetMatch2 ? parseFloat(budgetMatch2[1].replace(/,/g, '')) * (budgetMatch2[2]?.toUpperCase() === 'M' ? 1_000_000 : budgetMatch2[2]?.toUpperCase() === 'K' ? 1_000 : 1) : baseSavings * 0.6;
|
|
2583
|
-
for (const pct of [0.5, 0.75, 1.0, 1.25]) {
|
|
2584
|
-
const savings = baseSavings * pct;
|
|
2585
|
-
const roi = Math.round((savings / baseInvest) * 100);
|
|
2586
|
-
const payback = savings > 0 ? Math.round(baseInvest / (savings / 12)) : 0;
|
|
2587
|
-
const label = pct === 1.0 ? '**Base case**' : pct < 1 ? `Conservative (${Math.round(pct * 100)}%)` : `Aggressive (${Math.round(pct * 100)}%)`;
|
|
2588
|
-
lines.push(`| ${label} | ${fmtS(savings)} | ${roi}% | ${payback} months |`);
|
|
2589
|
-
}
|
|
3654
|
+
const parsed = parseFloat(numStr) * mult;
|
|
3655
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
3656
|
+
baseSavingsForSensitivity = parsed;
|
|
2590
3657
|
}
|
|
2591
3658
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
3659
|
+
// ADR-PIPELINE-070: pass the labor profile so workforce overrun appears
|
|
3660
|
+
// in the tornado when applicable.
|
|
3661
|
+
const sensitivityRows = buildSensitivityTable(baseSavingsForSensitivity, extracted.scenario_type, financialLaborProfile);
|
|
3662
|
+
lines.push(...renderSensitivitySection(sensitivityRows, baseSavingsForSensitivity));
|
|
2594
3663
|
if (lineage.simulationId) {
|
|
2595
3664
|
lines.push(`*Simulation: ${lineage.simulationId}${lineage.traceId ? ` | Trace: ${lineage.traceId}` : ''}*`);
|
|
2596
3665
|
}
|