@llm-dev-ops/agentics-cli 2.3.0 โ†’ 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.
Files changed (53) hide show
  1. package/dist/pipeline/auto-chain.d.ts +117 -0
  2. package/dist/pipeline/auto-chain.d.ts.map +1 -1
  3. package/dist/pipeline/auto-chain.js +1047 -35
  4. package/dist/pipeline/auto-chain.js.map +1 -1
  5. package/dist/pipeline/phase2/phases/prompt-generator.d.ts.map +1 -1
  6. package/dist/pipeline/phase2/phases/prompt-generator.js +152 -6
  7. package/dist/pipeline/phase2/phases/prompt-generator.js.map +1 -1
  8. package/dist/pipeline/phase4-5-pre-render/financial-model.d.ts +51 -0
  9. package/dist/pipeline/phase4-5-pre-render/financial-model.d.ts.map +1 -0
  10. package/dist/pipeline/phase4-5-pre-render/financial-model.js +118 -0
  11. package/dist/pipeline/phase4-5-pre-render/financial-model.js.map +1 -0
  12. package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.d.ts +53 -0
  13. package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.d.ts.map +1 -0
  14. package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.js +130 -0
  15. package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.js.map +1 -0
  16. package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.d.ts +47 -0
  17. package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.d.ts.map +1 -0
  18. package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.js +105 -0
  19. package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.js.map +1 -0
  20. package/dist/pipeline/phase4-5-pre-render/sector-baselines.d.ts +42 -0
  21. package/dist/pipeline/phase4-5-pre-render/sector-baselines.d.ts.map +1 -0
  22. package/dist/pipeline/phase4-5-pre-render/sector-baselines.js +117 -0
  23. package/dist/pipeline/phase4-5-pre-render/sector-baselines.js.map +1 -0
  24. package/dist/pipeline/phase5-build/phases/post-generation-validator.d.ts.map +1 -1
  25. package/dist/pipeline/phase5-build/phases/post-generation-validator.js +341 -1
  26. package/dist/pipeline/phase5-build/phases/post-generation-validator.js.map +1 -1
  27. package/dist/pipeline/types.d.ts +4 -1
  28. package/dist/pipeline/types.d.ts.map +1 -1
  29. package/dist/pipeline/types.js +9 -1
  30. package/dist/pipeline/types.js.map +1 -1
  31. package/dist/synthesis/domain-unit-registry.d.ts +1 -1
  32. package/dist/synthesis/domain-unit-registry.d.ts.map +1 -1
  33. package/dist/synthesis/domain-unit-registry.js +26 -0
  34. package/dist/synthesis/domain-unit-registry.js.map +1 -1
  35. package/dist/synthesis/financial-claim-extractor.d.ts +20 -0
  36. package/dist/synthesis/financial-claim-extractor.d.ts.map +1 -1
  37. package/dist/synthesis/financial-claim-extractor.js +31 -0
  38. package/dist/synthesis/financial-claim-extractor.js.map +1 -1
  39. package/dist/synthesis/financial-consistency-rules.d.ts +4 -0
  40. package/dist/synthesis/financial-consistency-rules.d.ts.map +1 -1
  41. package/dist/synthesis/financial-consistency-rules.js +51 -0
  42. package/dist/synthesis/financial-consistency-rules.js.map +1 -1
  43. package/dist/synthesis/roadmap-dates.d.ts +72 -0
  44. package/dist/synthesis/roadmap-dates.d.ts.map +1 -0
  45. package/dist/synthesis/roadmap-dates.js +203 -0
  46. package/dist/synthesis/roadmap-dates.js.map +1 -0
  47. package/dist/synthesis/simulation-artifact-generator.d.ts.map +1 -1
  48. package/dist/synthesis/simulation-artifact-generator.js +46 -0
  49. package/dist/synthesis/simulation-artifact-generator.js.map +1 -1
  50. package/dist/synthesis/simulation-renderers.d.ts.map +1 -1
  51. package/dist/synthesis/simulation-renderers.js +139 -34
  52. package/dist/synthesis/simulation-renderers.js.map +1 -1
  53. package/package.json +1 -1
@@ -14,8 +14,10 @@
14
14
  * permitted I/O path and never throws.
15
15
  */
16
16
  import { loadUnitEconomics, buildFinancialModelFromUnitEconomics, enforceManifestConsistency, } from './unit-economics-loader.js';
17
+ import { fcvTag, countFcvTags } from './financial-claim-extractor.js';
17
18
  import { classifyLaborIntensity, isLaborIntensive, workforceRiskTemplate, workforceRaciTemplate, workforceSensitivityRow, estimateWorkforceExposure, } from './domain-labor-classifier.js';
18
19
  import { populateConsensusSnapshot, consensusBanner, consensusOpeningParagraph, whyNotMoreConfidentSection, consensusGate, analyticalUncertaintyRisk, } from './consensus-tiers.js';
20
+ import { resolvePilotStart, projectDates, } from './roadmap-dates.js';
19
21
  // ============================================================================
20
22
  // Helper: safe extraction from unknown agent responses
21
23
  // ============================================================================
@@ -1238,6 +1240,24 @@ function extractCoreDomainNoun(query) {
1238
1240
  */
1239
1241
  const SYNTHESIS_BASE_RULES = `You are a senior partner at McKinsey & Company rewriting an enterprise decision document.
1240
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
+
1241
1261
  ABSOLUTE RULES โ€” violating any of these means the document fails:
1242
1262
  1. Lead with the recommendation and quantified impact (Pyramid Principle)
1243
1263
  2. Every financial figure MUST show its derivation (unit ร— rate ร— volume = total)
@@ -1250,7 +1270,7 @@ ABSOLUTE RULES โ€” violating any of these means the document fails:
1250
1270
  9. No sentence fragments, no echoing the user's query, no template placeholders
1251
1271
  10. Every paragraph must add insight or evidence, not just rearrange data points
1252
1272
 
1253
- 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.
1254
1274
  Do NOT change financial figures โ€” keep the same numbers but improve how they're presented and justified.
1255
1275
  Output ONLY the rewritten document โ€” no preamble, no commentary.`;
1256
1276
  const SYNTHESIS_TYPE_INSTRUCTIONS = {
@@ -1294,6 +1314,11 @@ export async function synthesizeExecutiveDocument(skeleton, query, documentType,
1294
1314
  '',
1295
1315
  `DOCUMENT TO REWRITE:\n${skeleton}`,
1296
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);
1297
1322
  // Tier 1: Claude binary (uses user's Claude subscription, no API key needed)
1298
1323
  try {
1299
1324
  const { execSync } = await import('node:child_process');
@@ -1305,8 +1330,17 @@ export async function synthesizeExecutiveDocument(skeleton, query, documentType,
1305
1330
  try {
1306
1331
  const result = execSync(`${claudeBin} --print < "${tmpFile}"`, { encoding: 'utf-8', timeout: 90_000, maxBuffer: 1_000_000, shell: '/bin/sh' });
1307
1332
  if (result && result.trim().length > skeleton.length * 0.3) {
1308
- process.stderr.write(`[exec-docs] LLM synthesis succeeded for ${documentType} (${result.trim().length} chars)\n`);
1309
- return result.trim();
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;
1310
1344
  }
1311
1345
  }
1312
1346
  finally {
@@ -1342,7 +1376,16 @@ export async function synthesizeExecutiveDocument(skeleton, query, documentType,
1342
1376
  const parsed = JSON.parse(result);
1343
1377
  const text = parsed?.content?.[0]?.text;
1344
1378
  if (text && text.length > skeleton.length * 0.3) {
1345
- process.stderr.write(`[exec-docs] API synthesis succeeded for ${documentType} (${text.length} chars)\n`);
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`);
1346
1389
  return text;
1347
1390
  }
1348
1391
  }
@@ -1404,15 +1447,22 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
1404
1447
  lines.push('', opening);
1405
1448
  }
1406
1449
  if (fin.hasData) {
1407
- const impactParts = [];
1408
- if (fin.budget)
1409
- impactParts.push(`${fin.budget} investment`);
1410
- if (fin.payback)
1411
- impactParts.push(`${fin.payback} payback`);
1412
- if (fin.npv)
1413
- impactParts.push(`${fin.npv} 5-year NPV`);
1414
- if (impactParts.length > 0) {
1415
- lines.push(`Projected impact: ${impactParts.join(', ')}.`);
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('');
1416
1466
  }
1417
1467
  }
1418
1468
  // ADR-PIPELINE-025 ยง4: Success probability with domain-aware decomposition
@@ -1474,21 +1524,25 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
1474
1524
  }
1475
1525
  lines.push('');
1476
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.
1477
1531
  if (fin.hasData) {
1478
1532
  lines.push('## Financial Impact', '');
1479
1533
  lines.push('| Metric | Value |', '|--------|-------|');
1480
1534
  if (fin.budget)
1481
- lines.push(`| Total Investment | ${fin.budget} |`);
1535
+ lines.push(`| Total Investment | ${fcvTag('investment', 'full-program', 'executive-summary')} ${fin.budget} |`);
1482
1536
  if (fin.roi)
1483
- lines.push(`| Expected ROI | ${fin.roi} |`);
1537
+ lines.push(`| Expected ROI | ${fcvTag('roi', 'full-program', 'executive-summary')} ${fin.roi} |`);
1484
1538
  if (fin.npv)
1485
- lines.push(`| 5-Year NPV | ${fin.npv} |`);
1539
+ lines.push(`| 5-Year NPV | ${fcvTag('npv', 'full-program', 'executive-summary')} ${fin.npv} |`);
1486
1540
  if (fin.payback)
1487
- lines.push(`| Payback Period | ${fin.payback} |`);
1541
+ lines.push(`| Payback Period | ${fcvTag('timeline', 'full-program', 'executive-summary')} ${fin.payback} |`);
1488
1542
  if (fin.revenue)
1489
- lines.push(`| Revenue Impact | ${fin.revenue} |`);
1543
+ lines.push(`| Revenue Impact | ${fcvTag('savings', 'full-program', 'executive-summary')} ${fin.revenue} |`);
1490
1544
  if (fin.costSavings)
1491
- lines.push(`| Cost Savings | ${fin.costSavings} |`);
1545
+ lines.push(`| Cost Savings | ${fcvTag('savings', 'full-program', 'executive-summary')} ${fin.costSavings} |`);
1492
1546
  lines.push('');
1493
1547
  }
1494
1548
  // Risk Profile โ€” never empty
@@ -1626,23 +1680,28 @@ export function renderDecisionMemo(query, simulationResult, platformResults) {
1626
1680
  const fmt = (n) => n >= 1_000_000 ? `$${(n / 1_000_000).toFixed(1)}M` : `$${(n / 1000).toFixed(0)}K`;
1627
1681
  pilotBudget = `${fmt(pilotVal)} - ${fmt(pilotVal * 1.5)}`;
1628
1682
  }
1629
- lines.push(`| **A. Scoped Pilot** | Validate with subset of data and ${extracted.domain_entities.slice(0, 2).join(', ') || 'core entities'} | ${pilotBudget} | 8-12 weeks | Low | **Recommended** |`, `| **B. Full Deployment** | Enterprise-wide rollout across all ${extracted.systems.length > 0 ? extracted.systems.join(', ') : 'systems'} | ${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 ? fin.costSavings + ' annual waste continues' : fin.revenue ? fin.revenue + ' in annual savings foregone' : 'costs compound without intervention'} |`, '');
1630
- // Financial Impact
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).
1631
1690
  lines.push('## Financial Impact', '');
1632
1691
  if (fin.hasData) {
1633
1692
  lines.push('| Metric | Value |', '|--------|-------|');
1634
1693
  if (fin.budget)
1635
- lines.push(`| Total Investment | ${fin.budget} |`);
1694
+ lines.push(`| Total Investment | ${fcvTag('investment', 'full-program', 'decision-memo')} ${fin.budget} |`);
1636
1695
  if (fin.roi)
1637
- lines.push(`| Expected ROI | ${fin.roi} |`);
1696
+ lines.push(`| Expected ROI | ${fcvTag('roi', 'full-program', 'decision-memo')} ${fin.roi} |`);
1638
1697
  if (fin.npv)
1639
- lines.push(`| 5-Year NPV | ${fin.npv} |`);
1698
+ lines.push(`| 5-Year NPV | ${fcvTag('npv', 'full-program', 'decision-memo')} ${fin.npv} |`);
1640
1699
  if (fin.payback)
1641
- lines.push(`| Payback Period | ${fin.payback} |`);
1700
+ lines.push(`| Payback Period | ${fcvTag('timeline', 'full-program', 'decision-memo')} ${fin.payback} |`);
1642
1701
  if (fin.revenue)
1643
- lines.push(`| Revenue Impact | ${fin.revenue} |`);
1702
+ lines.push(`| Revenue Impact | ${fcvTag('savings', 'full-program', 'decision-memo')} ${fin.revenue} |`);
1644
1703
  if (fin.costSavings)
1645
- lines.push(`| Cost Savings | ${fin.costSavings} |`);
1704
+ lines.push(`| Cost Savings | ${fcvTag('savings', 'full-program', 'decision-memo')} ${fin.costSavings} |`);
1646
1705
  lines.push('');
1647
1706
  }
1648
1707
  else {
@@ -3001,6 +3060,40 @@ export function buildRoadmapArtifact(query, simulationResult, platformResults) {
3001
3060
  // ADR-PIPELINE-071: Week-4 consensus gate on contested runs.
3002
3061
  const roadmapConsensus = populateConsensusSnapshot(simulationResult, platformResults);
3003
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
+ });
3004
3097
  return {
3005
3098
  metadata: {
3006
3099
  title: `Roadmap: ${query}`,
@@ -3009,7 +3102,13 @@ export function buildRoadmapArtifact(query, simulationResult, platformResults) {
3009
3102
  estimated_duration: timeline,
3010
3103
  status: 'draft',
3011
3104
  },
3012
- phases: defaultPhases,
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,
3013
3112
  success_criteria: safeArray(simData, 'recommendations').map(r => String(r)),
3014
3113
  // ADR-PIPELINE-024: Always include domain-appropriate risks
3015
3114
  risk_factors: generateDomainRisks(query, extracted).map(r => ({
@@ -3027,8 +3126,7 @@ export function buildRoadmapArtifact(query, simulationResult, platformResults) {
3027
3126
  implementation_plan: extractAgentData(platformResults, 'copilot', 'planner'),
3028
3127
  resource_requirements: extractAgentData(platformResults, 'costops', 'budget'),
3029
3128
  // ADR-PIPELINE-061: Quantified phase gate criteria with derived decision authority
3030
- // (Replaces the old 3-line hardcoded generic gates from ADR-PIPELINE-024.)
3031
- phase_gates: buildPhaseGates(query, extracted, primarySystem),
3129
+ phase_gates: enrichedGates,
3032
3130
  // ADR-PIPELINE-071: Consensus tier metadata + Week-4 gate when contested.
3033
3131
  consensus: {
3034
3132
  tier: roadmapConsensus.tier,
@@ -3425,7 +3523,10 @@ export function renderFinancialAnalysis(query, simulationResult, platformResults
3425
3523
  lines.push('> โš ๏ธ **Unit economics consistency warnings:**', ...manifestWarnings.map(v => `> - ${v}`), '');
3426
3524
  }
3427
3525
  }
3428
- lines.push('---', '', `## Investment Summary${fin.isEstimated ? ' (Estimated from Organization Profile)' : unitEconomicsSource === 'manifest' ? ' (from Prototype Unit Economics)' : ''}`, '', '| Metric | Value |', '|--------|-------|', `| Total Investment | ${fin.budget} |`, `| Expected ROI | ${fin.roi} |`, `| 5-Year NPV | ${fin.npv} |`, `| Payback Period | ${fin.payback} |`, `| Revenue / Savings Impact | ${fin.revenue || fin.costSavings} |`, '');
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} |`, '');
3429
3530
  // ADR-PIPELINE-070: Workforce exposure block when intensity โ‰ฅ medium.
3430
3531
  // Mandatory for hospitality/retail/fleet/healthcare/CRE/etc. โ€” calls out
3431
3532
  // role count, EMEA union exposure, change-mgmt budget, and reallocation
@@ -3532,9 +3633,13 @@ export function renderFinancialAnalysis(query, simulationResult, platformResults
3532
3633
  pessCostStr = fmt(baseVal * 1.4);
3533
3634
  }
3534
3635
  }
3535
- lines.push(`| **Base Case** | Plan executes as modeled | ${baseCostStr} | ${fin.roi || 'Projected'} | 50% |`);
3536
- lines.push(`| Optimistic | Faster adoption, lower integration costs | ${optCostStr} | ${fin.roi ? fin.roi.replace(/\d+/, m => String(Math.round(parseInt(m) * 1.3))) : 'Above base'} | 25% |`);
3537
- lines.push(`| Pessimistic | Slower adoption, scope growth | ${pessCostStr} | ${fin.roi ? fin.roi.replace(/\d+/, m => String(Math.round(parseInt(m) * 0.6))) : 'Below base'} | 25% |`);
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% |`);
3538
3643
  lines.push('', '*Scenario analysis assumes ยฑ20% cost variance and ยฑ30% timeline variance from base case.*', '');
3539
3644
  // ADR-PIPELINE-063: Multi-variable sensitivity tornado
3540
3645
  // Implements ADR-PIPELINE-057 ยง1 โ€” replaces the old single-variable loop