@llm-dev-ops/agentics-cli 1.7.1 → 1.8.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.
@@ -73,6 +73,24 @@ export function computeFleetMetrics(results) {
73
73
  // ============================================================================
74
74
  // Extract signal payload from nested agent response
75
75
  // ============================================================================
76
+ /**
77
+ * ADR-PIPELINE-033: Extract simulation and trace IDs for decision lineage.
78
+ * These IDs create an auditable chain from recommendation back to the analysis.
79
+ */
80
+ function extractSimulationLineage(simulationResult) {
81
+ const payload = extractSignalPayload(simulationResult);
82
+ const data = payload.data ?? {};
83
+ const simId = safeString(data, 'simulation_id')
84
+ || safeString(data, 'execution_id')
85
+ || safeString(data, 'id')
86
+ || safeString(data, 'sim_id')
87
+ || '';
88
+ const traceId = safeString(data, 'trace_id')
89
+ || safeString(data, 'correlation_id')
90
+ || safeString(data, 'run_id')
91
+ || '';
92
+ return { simulationId: simId, traceId };
93
+ }
76
94
  function extractSignalPayload(response) {
77
95
  // Agent responses often have: { signal: { payload: { type, data } } }
78
96
  const signalPayload = safeGet(response, 'signal', 'payload');
@@ -232,14 +250,18 @@ export function synthesizeFinancials(simData, platformResults, query) {
232
250
  * All clearly labeled as estimates.
233
251
  */
234
252
  function estimateFinancialsFromQuery(query) {
235
- // Extract employee count — simple string search, no regex
236
- let employees = 10000;
237
253
  const lower = query.toLowerCase();
254
+ const fmt = (n) => n >= 1_000_000 ? `$${(n / 1_000_000).toFixed(1)}M` : `$${(n / 1000).toFixed(0)}K`;
255
+ // ── ADR-PIPELINE-029 §2: Try domain-specific bottom-up model first ──
256
+ const domainModel = tryDomainSpecificFinancials(query, lower);
257
+ if (domainModel)
258
+ return domainModel;
259
+ // ── Fallback: per-employee heuristic with visible methodology ──
260
+ let employees = 10000;
238
261
  const empIdx = lower.indexOf('employees');
239
262
  const staffIdx = lower.indexOf('staff');
240
263
  const targetIdx = empIdx !== -1 ? empIdx : staffIdx;
241
264
  if (targetIdx > 5) {
242
- // Walk backward from "employees" to find the number
243
265
  const before = query.substring(Math.max(0, targetIdx - 30), targetIdx).trim();
244
266
  const words = before.split(/\s+/);
245
267
  for (let i = words.length - 1; i >= 0; i--) {
@@ -251,29 +273,232 @@ function estimateFinancialsFromQuery(query) {
251
273
  }
252
274
  }
253
275
  }
254
- // Industry affects per-employee costs
255
- const isPharma = lower.includes('pharma') || lower.includes('drug') || lower.includes('gxp') || lower.includes('clinical');
276
+ // Industry detection
277
+ const isPharma = lower.includes('pharma') || lower.includes('drug') || lower.includes('gxp');
256
278
  const isFinancial = lower.includes('financial') || lower.includes('banking') || lower.includes('insurance');
257
279
  const isRetail = lower.includes('retail') || lower.includes('grocery') || lower.includes('store');
258
- const costPerEmployee = isPharma ? 35 : isFinancial ? 30 : isRetail ? 15 : 25;
259
- const savingsMultiplier = isPharma ? 80 : isFinancial ? 60 : isRetail ? 40 : 50;
280
+ const isUtility = lower.includes('water') || lower.includes('utility') || lower.includes('energy') || lower.includes('grid');
281
+ const isHealthcare = lower.includes('healthcare') || lower.includes('hospital') || lower.includes('patient');
282
+ const isSustainability = lower.includes('sustainability') || lower.includes('emissions') || lower.includes('carbon') || lower.includes('esg');
283
+ const isProfessionalServices = lower.includes('professional services') || lower.includes('consulting') || lower.includes('advisory');
284
+ const isManufacturing = lower.includes('manufacturing') || lower.includes('factory') || lower.includes('production line');
285
+ const isConsumerGoods = lower.includes('consumer goods') || lower.includes('cpg') || lower.includes('fmcg') || lower.includes('packaging');
286
+ let industryLabel = 'enterprise';
287
+ let costPerEmployee = 25;
288
+ let savingsMultiplier = 50;
289
+ if (isPharma) {
290
+ costPerEmployee = 35;
291
+ savingsMultiplier = 80;
292
+ industryLabel = 'pharmaceutical';
293
+ }
294
+ else if (isFinancial) {
295
+ costPerEmployee = 30;
296
+ savingsMultiplier = 60;
297
+ industryLabel = 'financial services';
298
+ }
299
+ else if (isRetail) {
300
+ costPerEmployee = 15;
301
+ savingsMultiplier = 40;
302
+ industryLabel = 'retail';
303
+ }
304
+ else if (isUtility) {
305
+ costPerEmployee = 20;
306
+ savingsMultiplier = 55;
307
+ industryLabel = 'utility infrastructure';
308
+ }
309
+ else if (isHealthcare) {
310
+ costPerEmployee = 30;
311
+ savingsMultiplier = 65;
312
+ industryLabel = 'healthcare';
313
+ }
314
+ else if (isManufacturing) {
315
+ costPerEmployee = 22;
316
+ savingsMultiplier = 55;
317
+ industryLabel = 'manufacturing';
318
+ }
319
+ else if (isConsumerGoods) {
320
+ costPerEmployee = 20;
321
+ savingsMultiplier = 45;
322
+ industryLabel = 'consumer goods';
323
+ }
324
+ else if (isSustainability) {
325
+ costPerEmployee = 25;
326
+ savingsMultiplier = 50;
327
+ industryLabel = 'sustainability';
328
+ }
329
+ else if (isProfessionalServices) {
330
+ costPerEmployee = 25;
331
+ savingsMultiplier = 50;
332
+ industryLabel = 'professional services';
333
+ }
260
334
  const pilotCost = Math.round(employees * costPerEmployee / 1000) * 1000;
261
335
  const annualSavings = Math.round(employees * savingsMultiplier / 1000) * 1000;
262
336
  const paybackMonths = Math.max(6, Math.min(24, Math.round(pilotCost / (annualSavings / 12))));
263
- const npv5yr = Math.round(annualSavings * 3.5); // ~10% discount rate, 5 years
337
+ const npv5yr = Math.round(annualSavings * 3.5);
264
338
  const roiPct = Math.round((annualSavings / pilotCost) * 100);
265
- const fmt = (n) => n >= 1_000_000 ? `$${(n / 1_000_000).toFixed(1)}M` : `$${(n / 1000).toFixed(0)}K`;
339
+ const methodology = `${employees.toLocaleString()} employees × $${costPerEmployee}/employee ${industryLabel} benchmark`;
340
+ const savingsMethod = `${employees.toLocaleString()} employees × $${savingsMultiplier}/employee annual efficiency gain`;
266
341
  return {
267
- budget: `${fmt(pilotCost)} - ${fmt(pilotCost * 1.5)} (estimated based on ${employees.toLocaleString()} employees)`,
268
- roi: `${roiPct}% projected (estimated)`,
269
- npv: `${fmt(npv5yr)} 5-year NPV (estimated, 10% discount rate)`,
270
- payback: `${paybackMonths}-${paybackMonths + 6} months (estimated)`,
271
- revenue: `${fmt(annualSavings)} annual efficiency gains (estimated)`,
342
+ budget: `${fmt(pilotCost)} - ${fmt(pilotCost * 1.5)} (${methodology})`,
343
+ roi: `${roiPct}% projected (${fmt(annualSavings)} savings ÷ ${fmt(pilotCost)} investment)`,
344
+ npv: `${fmt(npv5yr)} 5-year NPV (10% discount rate, ${savingsMethod})`,
345
+ payback: `${paybackMonths}-${paybackMonths + 6} months (break-even when cumulative savings exceed investment)`,
346
+ revenue: `${fmt(annualSavings)} annual efficiency gains (${savingsMethod})`,
272
347
  costSavings: '',
273
348
  hasData: true,
274
349
  isEstimated: true,
275
350
  };
276
351
  }
352
+ /**
353
+ * ADR-PIPELINE-029 §2: Domain-specific bottom-up financial models.
354
+ * These use query signals to build unit-economics-based estimates
355
+ * rather than generic per-employee heuristics.
356
+ *
357
+ * Returns null if no domain-specific model applies — caller falls back
358
+ * to per-employee heuristic.
359
+ */
360
+ function tryDomainSpecificFinancials(query, lower) {
361
+ const fmt = (n) => n >= 1_000_000 ? `$${(n / 1_000_000).toFixed(1)}M` : `$${(n / 1000).toFixed(0)}K`;
362
+ const employees = extractEmployeeCount(query, lower);
363
+ // ── Packaging / Supplier Waste ──
364
+ if (/packag|supplier.*waste|excess.*material|supplier.*sustainab/i.test(lower)) {
365
+ // Bottom-up: suppliers × avg excess rate × cost per shipment × annual volume
366
+ const supplierMatch = lower.match(/(\d+)[,.]?\d*\s*(?:suppliers?|vendors?)/);
367
+ const supplierCount = supplierMatch ? parseInt(supplierMatch[1], 10) : estimateSupplierCount(employees);
368
+ const avgExcessRate = 0.18; // 18% industry average excess packaging
369
+ const avgCostPerShipment = 4.50; // avg packaging cost per shipment, USD
370
+ const annualShipmentsPerSupplier = 2400; // ~200/month
371
+ const totalShipments = supplierCount * annualShipmentsPerSupplier;
372
+ const addressableWaste = totalShipments * avgExcessRate * avgCostPerShipment;
373
+ const achievableReduction = 0.35; // 35% of addressable waste through optimization
374
+ const annualSavings = Math.round(addressableWaste * achievableReduction / 1000) * 1000;
375
+ const implCost = Math.round(Math.max(200000, annualSavings * 0.6) / 1000) * 1000;
376
+ const roiPct = Math.round((annualSavings / implCost) * 100);
377
+ const paybackMonths = Math.max(6, Math.min(18, Math.round(implCost / (annualSavings / 12))));
378
+ const methodology = `${supplierCount} suppliers × ${(annualShipmentsPerSupplier).toLocaleString()} shipments/yr × ` +
379
+ `${(avgExcessRate * 100).toFixed(0)}% excess rate × $${avgCostPerShipment.toFixed(2)}/shipment × ` +
380
+ `${(achievableReduction * 100).toFixed(0)}% achievable reduction`;
381
+ return {
382
+ budget: `${fmt(implCost)} - ${fmt(implCost * 1.4)} (implementation + ${supplierCount}-supplier rollout)`,
383
+ roi: `${roiPct}% projected (${fmt(annualSavings)} annual savings ÷ ${fmt(implCost)} investment; ${methodology})`,
384
+ npv: `${fmt(Math.round(annualSavings * 3.5))} 5-year NPV (10% discount rate, ${fmt(addressableWaste)} total addressable waste)`,
385
+ payback: `${paybackMonths}-${paybackMonths + 4} months (break-even at ${Math.round(achievableReduction * avgExcessRate * 100)}% packaging reduction)`,
386
+ revenue: `${fmt(annualSavings)} annual waste reduction savings (${methodology})`,
387
+ costSavings: `${fmt(addressableWaste)} total addressable packaging waste annually`,
388
+ hasData: true,
389
+ isEstimated: true,
390
+ };
391
+ }
392
+ // ── Water Loss / Utility Network ──
393
+ if (/water\s*loss|non.?revenue\s*water|leak|pipe.*aging|distribution\s*network/i.test(lower)) {
394
+ // Bottom-up: system volume × loss rate × water cost × achievable reduction
395
+ const popMatch = lower.match(/(\d+)[,.]?\d*\s*(?:customer|connection|household|meter)/);
396
+ const customers = popMatch ? parseInt(popMatch[1], 10) : Math.round(employees * 4.5); // ~4.5 customers per employee
397
+ const avgDailyVolumeM3 = customers * 0.4; // 400 liters/customer/day
398
+ const lossRate = 0.22; // 22% industry average NRW
399
+ const waterCostPerM3 = 1.85;
400
+ const dailyLossM3 = avgDailyVolumeM3 * lossRate;
401
+ const annualLossCost = dailyLossM3 * 365 * waterCostPerM3;
402
+ const achievableReduction = 0.30; // 30% of losses through targeted intervention
403
+ const annualSavings = Math.round(achievableReduction * annualLossCost / 1000) * 1000;
404
+ const implCost = Math.round(Math.max(300000, annualSavings * 0.5) / 1000) * 1000;
405
+ const roiPct = Math.round((annualSavings / implCost) * 100);
406
+ const paybackMonths = Math.max(6, Math.min(18, Math.round(implCost / (annualSavings / 12))));
407
+ const methodology = `${customers.toLocaleString()} connections × ${(lossRate * 100).toFixed(0)}% NRW rate × ` +
408
+ `$${waterCostPerM3}/m³ × ${(achievableReduction * 100).toFixed(0)}% achievable reduction`;
409
+ return {
410
+ budget: `${fmt(implCost)} - ${fmt(implCost * 1.4)} (AI platform + sensor integration for ${customers.toLocaleString()} connections)`,
411
+ roi: `${roiPct}% projected (${fmt(annualSavings)} annual water savings ÷ ${fmt(implCost)} investment; ${methodology})`,
412
+ npv: `${fmt(Math.round(annualSavings * 3.5))} 5-year NPV (10% discount rate, ${fmt(annualLossCost)} annual water loss cost)`,
413
+ payback: `${paybackMonths}-${paybackMonths + 4} months (break-even at ${Math.round(achievableReduction * lossRate * 100)}% loss reduction)`,
414
+ revenue: `${fmt(annualSavings)} annual water loss reduction (${methodology})`,
415
+ costSavings: `${fmt(annualLossCost)} total addressable water loss cost annually`,
416
+ hasData: true,
417
+ isEstimated: true,
418
+ };
419
+ }
420
+ // ── Travel / Emissions / Sustainability ──
421
+ if (/travel.*emission|carbon.*footprint|sustainab.*travel|business\s*travel.*reduc|commut.*emission/i.test(lower)) {
422
+ // Bottom-up: travelers × avg trips × emissions per trip × carbon cost × achievable reduction
423
+ const travelerPct = 0.35; // 35% of employees are business travelers
424
+ const travelers = Math.round(employees * travelerPct);
425
+ const avgTripsPerYear = 6;
426
+ const avgEmissionsPerTripKg = 850; // kg CO2e (mix of domestic + international)
427
+ const carbonPricePerTonne = 75; // $/tonne CO2e (social cost of carbon)
428
+ const avgTripCost = 2200; // $ per trip
429
+ const totalEmissionsKg = travelers * avgTripsPerYear * avgEmissionsPerTripKg;
430
+ const totalTravelCost = travelers * avgTripsPerYear * avgTripCost;
431
+ const achievableReduction = 0.25; // 25% through virtual-first + rail substitution
432
+ const emissionsSavingsKg = totalEmissionsKg * achievableReduction;
433
+ const travelCostSavings = totalTravelCost * achievableReduction * 0.6; // 60% cost recovery (some virtual, some rail)
434
+ const carbonValueSavings = (emissionsSavingsKg / 1000) * carbonPricePerTonne;
435
+ const annualSavings = Math.round((travelCostSavings + carbonValueSavings) / 1000) * 1000;
436
+ const implCost = Math.round(Math.max(200000, annualSavings * 0.4) / 1000) * 1000;
437
+ const roiPct = Math.round((annualSavings / implCost) * 100);
438
+ const paybackMonths = Math.max(4, Math.min(14, Math.round(implCost / (annualSavings / 12))));
439
+ const methodology = `${travelers.toLocaleString()} travelers × ${avgTripsPerYear} trips/yr × ` +
440
+ `${avgEmissionsPerTripKg} kg CO2e/trip × ${(achievableReduction * 100).toFixed(0)}% reduction`;
441
+ return {
442
+ budget: `${fmt(implCost)} - ${fmt(implCost * 1.3)} (platform + integration with travel/expense systems)`,
443
+ roi: `${roiPct}% projected (${fmt(annualSavings)} combined savings ÷ ${fmt(implCost)} investment; ${methodology})`,
444
+ npv: `${fmt(Math.round(annualSavings * 3.5))} 5-year NPV (10% discount rate, ${fmt(travelCostSavings)} travel + ${fmt(carbonValueSavings)} carbon value)`,
445
+ payback: `${paybackMonths}-${paybackMonths + 4} months (${Math.round(emissionsSavingsKg / 1000)} tonnes CO2e/yr reduction)`,
446
+ revenue: `${fmt(annualSavings)} annual combined savings (${fmt(travelCostSavings)} travel cost + ${fmt(carbonValueSavings)} carbon value at $${carbonPricePerTonne}/tonne)`,
447
+ costSavings: `${Math.round(emissionsSavingsKg / 1000).toLocaleString()} tonnes CO2e annual reduction (${methodology})`,
448
+ hasData: true,
449
+ isEstimated: true,
450
+ };
451
+ }
452
+ // ── Procurement / Supplier Management ──
453
+ if (/procurement.*optim|supplier.*manag|spend.*analy|purchas.*order|sourcing/i.test(lower)) {
454
+ const spendPct = 0.40; // 40% of revenue typically goes to procurement
455
+ const estimatedRevenue = employees * 150000; // rough revenue/employee
456
+ const totalSpend = estimatedRevenue * spendPct;
457
+ const addressableSpend = totalSpend * 0.30; // 30% of spend is addressable
458
+ const savingsRate = 0.05; // 5% savings on addressable spend
459
+ const annualSavings = Math.round(addressableSpend * savingsRate / 1000) * 1000;
460
+ const implCost = Math.round(Math.max(300000, annualSavings * 0.5) / 1000) * 1000;
461
+ const roiPct = Math.round((annualSavings / implCost) * 100);
462
+ const paybackMonths = Math.max(6, Math.min(18, Math.round(implCost / (annualSavings / 12))));
463
+ const methodology = `${fmt(totalSpend)} est. procurement spend × 30% addressable × 5% optimization rate`;
464
+ return {
465
+ budget: `${fmt(implCost)} - ${fmt(implCost * 1.4)} (AI analytics + ERP integration)`,
466
+ roi: `${roiPct}% projected (${fmt(annualSavings)} savings ÷ ${fmt(implCost)} investment; ${methodology})`,
467
+ npv: `${fmt(Math.round(annualSavings * 3.5))} 5-year NPV (10% discount rate, ${fmt(addressableSpend)} addressable spend)`,
468
+ payback: `${paybackMonths}-${paybackMonths + 4} months (break-even at ${(savingsRate * 100).toFixed(1)}% of addressable spend)`,
469
+ revenue: `${fmt(annualSavings)} annual procurement savings (${methodology})`,
470
+ costSavings: `${fmt(addressableSpend)} addressable procurement spend annually`,
471
+ hasData: true,
472
+ isEstimated: true,
473
+ };
474
+ }
475
+ return null; // No domain-specific model — caller uses per-employee fallback
476
+ }
477
+ /** Extract employee count from query text. Returns default 10000 if not found. */
478
+ function extractEmployeeCount(query, lower) {
479
+ let employees = 10000;
480
+ const empIdx = lower.indexOf('employees');
481
+ const staffIdx = lower.indexOf('staff');
482
+ const targetIdx = empIdx !== -1 ? empIdx : staffIdx;
483
+ if (targetIdx > 5) {
484
+ const before = query.substring(Math.max(0, targetIdx - 30), targetIdx).trim();
485
+ const words = before.split(/\s+/);
486
+ for (let i = words.length - 1; i >= 0; i--) {
487
+ const cleaned = (words[i] ?? '').replace(/,/g, '').replace(/approximately/i, '').trim();
488
+ const num = parseInt(cleaned, 10);
489
+ if (num > 100 && num < 10_000_000) {
490
+ employees = num;
491
+ break;
492
+ }
493
+ }
494
+ }
495
+ return employees;
496
+ }
497
+ /** Estimate supplier count from employee count for procurement scenarios. */
498
+ function estimateSupplierCount(employees) {
499
+ // Rough: 1 active supplier per 50-100 employees for large enterprises
500
+ return Math.max(20, Math.round(employees / 75));
501
+ }
277
502
  // extractFinancials is now synthesizeFinancials — kept as export for external callers
278
503
  export { synthesizeFinancials as extractFinancials };
279
504
  /**
@@ -325,17 +550,92 @@ function generateDomainRisks(query, extracted) {
325
550
  const q = query.toLowerCase();
326
551
  const systems = extracted.systems;
327
552
  const primarySystem = systems[0] ?? 'enterprise platform';
328
- // Every project has these
329
- risks.push({
330
- risk: `Data quality and completeness in ${primarySystem} may not meet analytical requirements`,
331
- category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
332
- mitigation: 'Data profiling during discovery phase; validation layer with quarantine for invalid records',
333
- });
334
- risks.push({
335
- risk: `${primarySystem} API integration complexity — rate limits, schema changes, authentication`,
336
- category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
337
- mitigation: `Technical spike to validate ${primarySystem} API capabilities before full commitment`,
338
- });
553
+ // System-specific data quality risks (not generic)
554
+ const isWorkday = /workday/i.test(q);
555
+ const isSAP = /sap|s\/4hana/i.test(q);
556
+ const isMaximo = /maximo|ibm\s+maximo/i.test(q);
557
+ const isCoupa = /coupa/i.test(q);
558
+ const isOracle = /oracle\s+(fusion|erp|cloud)/i.test(q);
559
+ const isNetSuite = /netsuite/i.test(q);
560
+ if (isWorkday) {
561
+ risks.push({
562
+ risk: `Workday custom report and RaaS API data may have field-level gaps — expense categories, cost centers, and worker location data are often inconsistent across regions`,
563
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
564
+ mitigation: 'Map required fields per Workday module (Expenses, Absence, Worker) during discovery; build validation for missing cost center and location codes',
565
+ });
566
+ risks.push({
567
+ risk: `Workday API rate limits (currently 10 req/sec for RaaS, 20 req/sec for REST) may constrain data sync for ${extracted.stakeholders.length > 2 ? 'large employee populations' : 'batch operations'}`,
568
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
569
+ mitigation: 'Implement incremental sync with delta queries; cache frequently accessed worker profiles; use Workday Integration Cloud for bulk data',
570
+ });
571
+ }
572
+ else if (isSAP) {
573
+ risks.push({
574
+ risk: `SAP S/4HANA OData API field mappings differ between on-premise and cloud editions — custom fields may not be accessible via standard APIs`,
575
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
576
+ mitigation: 'Validate API access for each required field during technical spike; plan CDS view development for custom field exposure if needed',
577
+ });
578
+ risks.push({
579
+ risk: `SAP API authentication (OAuth2 + X-CSRF tokens) and transport security requirements add integration complexity`,
580
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
581
+ mitigation: 'Technical spike with SAP Basis team to validate API configuration; use SAP BTP Integration Suite if direct API access is restricted',
582
+ });
583
+ }
584
+ else if (isMaximo) {
585
+ risks.push({
586
+ risk: `IBM Maximo OSLC API may not expose all required asset and maintenance fields — custom attributes require explicit OSLC resource definition`,
587
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
588
+ mitigation: 'Audit Maximo OSLC resource definitions during discovery; validate custom field availability via test queries against staging environment',
589
+ });
590
+ risks.push({
591
+ risk: `Maximo 7.x vs. MAS 8.x OSLC API differences — work order status transitions and custom field schemas differ between versions`,
592
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
593
+ mitigation: 'Confirm Maximo version and OSLC API version during technical spike; build version-aware adapter with feature detection',
594
+ });
595
+ }
596
+ else if (isCoupa) {
597
+ risks.push({
598
+ risk: `Coupa API pagination limits (50 records/page default) and supplier data completeness — custom fields, contract metadata, and sustainability scores may not be populated for all suppliers`,
599
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
600
+ mitigation: 'Audit Coupa object availability during discovery; implement cursor-based pagination with retry; validate supplier field population rates before analysis',
601
+ });
602
+ risks.push({
603
+ risk: `Coupa approval workflow integration — custom approval chains for AI-recommended supplier actions must align with existing procurement approval hierarchies`,
604
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
605
+ mitigation: 'Map existing Coupa approval workflows during discovery; implement as Coupa custom form or external approval with writeback, not parallel workflow',
606
+ });
607
+ }
608
+ else if (isOracle) {
609
+ risks.push({
610
+ risk: `Oracle ERP Cloud REST API versioning — endpoint schemas change between quarterly updates, and custom flex fields require explicit BI publisher configuration`,
611
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
612
+ mitigation: 'Pin to specific Oracle API version; implement schema validation on each response; plan flex field configuration with Oracle admin team',
613
+ });
614
+ risks.push({
615
+ risk: `Oracle ERP Cloud integration security — OAuth2 with JWT assertion flow and fine-grained role requirements for each API endpoint`,
616
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
617
+ mitigation: 'Technical spike with Oracle admin to configure integration user with minimum required roles; document JWT certificate rotation procedure',
618
+ });
619
+ }
620
+ else if (isNetSuite) {
621
+ risks.push({
622
+ risk: `NetSuite SuiteTalk/REST API token-based auth and record-level concurrency — optimistic locking may cause write conflicts during batch sync`,
623
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
624
+ mitigation: 'Implement retry with exponential backoff for concurrency errors; use RESTlet for custom record operations where SuiteTalk is restrictive',
625
+ });
626
+ }
627
+ else {
628
+ risks.push({
629
+ risk: `Data quality and completeness in ${primarySystem} may not meet analytical requirements`,
630
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
631
+ mitigation: 'Data profiling during discovery phase; validation layer with quarantine for invalid records',
632
+ });
633
+ risks.push({
634
+ risk: `${primarySystem} API integration complexity — rate limits, schema changes, authentication`,
635
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
636
+ mitigation: `Technical spike to validate ${primarySystem} API capabilities before full commitment`,
637
+ });
638
+ }
339
639
  risks.push({
340
640
  risk: 'Scope creep beyond initial prototype boundaries',
341
641
  category: 'Project', likelihood: 'High', impact: 'Medium', score: 6,
@@ -357,12 +657,37 @@ function generateDomainRisks(query, extracted) {
357
657
  mitigation: 'Legal review per jurisdiction during discovery; configurable compliance rules per region',
358
658
  });
359
659
  }
360
- // Sustainability risks
660
+ // Sustainability / emissions risks
361
661
  if (/(?:emission|carbon|sustainab|environmental|energy|waste|esg)/i.test(q)) {
362
662
  risks.push({
363
- risk: 'Emissions factor accuracy — default factors may not match specific operational context',
663
+ risk: 'Emissions factor accuracy — DEFRA/EPA default factors may not match specific operational context (e.g., regional grid mix, fleet composition)',
364
664
  category: 'Data', likelihood: 'Medium', impact: 'Medium', score: 4,
365
- mitigation: 'Allow user-supplied factors with audit trail; flag calculations using default vs. measured values',
665
+ mitigation: 'Allow organization-specific factors with audit trail; flag calculations using default vs. measured values; annual factor review process',
666
+ });
667
+ if (/travel|commut|flight|transport/i.test(q)) {
668
+ risks.push({
669
+ risk: 'Employee behavior change resistance — sustainability nudges may be perceived as restrictive or intrusive by frequent travelers',
670
+ category: 'Organizational', likelihood: 'Medium', impact: 'Medium', score: 4,
671
+ mitigation: 'Frame as insights, not mandates; gamification and positive reinforcement; executive sponsorship for culture shift',
672
+ });
673
+ }
674
+ }
675
+ // Water/utility-specific risks — require strong water/utility signal to avoid template leakage
676
+ // "flow" alone can match workflow, "distribution" alone can match supply chain
677
+ const isWaterDomain = /water\s*(loss|distribution|utility|network|treatment|pipe)/i.test(q)
678
+ || (/\bpipe\b/i.test(q) && /\b(leak|pressure|aging|infrastructure)\b/i.test(q))
679
+ || /\bnon.?revenue\s*water\b/i.test(q)
680
+ || /\bDMA\b/.test(query); // District Metered Area — only appears in water utility context
681
+ if (isWaterDomain) {
682
+ risks.push({
683
+ risk: 'Sensor data gaps — pressure and flow telemetry may have coverage holes in older distribution zones with limited SCADA instrumentation',
684
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
685
+ mitigation: 'Map sensor coverage per DMA during discovery; define minimum instrumentation requirements for analysis zones',
686
+ });
687
+ risks.push({
688
+ risk: 'Infrastructure intervention timelines — pipe replacement and pressure management projects require permitting and construction windows that may extend beyond prototype evaluation period',
689
+ category: 'Operational', likelihood: 'Medium', impact: 'Medium', score: 4,
690
+ mitigation: 'Focus prototype on detection and prioritization; track intervention recommendations separately from execution timelines',
366
691
  });
367
692
  }
368
693
  // Financial risks
@@ -373,6 +698,27 @@ function generateDomainRisks(query, extracted) {
373
698
  mitigation: 'Sensitivity analysis on key assumptions; quarterly re-validation of financial model',
374
699
  });
375
700
  }
701
+ // Packaging / supplier waste risks
702
+ if (/(?:packag|supplier.*waste|excess.*material|over.?pack)/i.test(q)) {
703
+ risks.push({
704
+ risk: 'Supplier resistance to packaging changes — established suppliers may push back on packaging standard modifications that require retooling or new materials',
705
+ category: 'Organizational', likelihood: 'Medium', impact: 'High', score: 6,
706
+ mitigation: 'Phased approach starting with willing suppliers; demonstrate cost savings to incentivize adoption; contractual compliance timelines',
707
+ });
708
+ risks.push({
709
+ risk: 'Product damage from packaging reduction — reducing protective packaging below threshold increases transit damage risk for fragile items',
710
+ category: 'Operational', likelihood: 'Low', impact: 'High', score: 3,
711
+ mitigation: 'Category-specific fragility benchmarks; pilot with low-fragility categories first; damage rate monitoring during rollout',
712
+ });
713
+ }
714
+ // Procurement / supplier management risks
715
+ if (/(?:procurement|supplier.*manag|sourcing|spend.*analy|purchas)/i.test(q)) {
716
+ risks.push({
717
+ risk: 'Supplier data quality — supplier master data may have inconsistent categorization, duplicate records, or stale contact information across regions',
718
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
719
+ mitigation: 'Supplier data cleansing sprint during discovery; define minimum data quality thresholds before analysis; flag low-confidence results',
720
+ });
721
+ }
376
722
  // Always include vendor dependency
377
723
  risks.push({
378
724
  risk: 'Technology vendor dependency — platform availability, pricing changes, or API deprecation',
@@ -388,6 +734,7 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
388
734
  const now = new Date().toISOString();
389
735
  const simPayload = extractSignalPayload(simulationResult);
390
736
  const simData = simPayload.data ?? {};
737
+ const lineage = extractSimulationLineage(simulationResult);
391
738
  const successProb = extractSuccessProbability(simData);
392
739
  const timeline = safeString(simData, 'timeline_estimate') || safeString(simData, 'timeline') || '12-18 weeks';
393
740
  const problemStatement = distillProblemStatement(query);
@@ -429,11 +776,16 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
429
776
  lines.push(`Projected impact: ${impactParts.join(', ')}.`);
430
777
  }
431
778
  }
432
- // ADR-PIPELINE-025 §4: Success probability with decomposition
433
- const techFeasibility = Math.min(0.95, successProb + 0.04);
434
- const orgReadiness = Math.max(0.70, successProb - 0.03);
435
- const dataAvailability = successProb;
436
- const integrationFeasibility = Math.max(0.75, successProb - 0.01);
779
+ // ADR-PIPELINE-025 §4: Success probability with domain-aware decomposition
780
+ // Technical feasibility is higher (standard APIs/patterns), organizational readiness is lower
781
+ // for large workforces, data availability depends on ERP maturity, integration depends on system count
782
+ const hasLargeWorkforce = extracted.stakeholders.length > 3 || query.length > 800;
783
+ const hasMultipleSystems = extracted.systems.length > 1;
784
+ const hasComplexConstraints = extracted.constraints.length > 2;
785
+ const techFeasibility = Math.min(0.95, successProb + (hasMultipleSystems ? 0.02 : 0.06));
786
+ const orgReadiness = Math.max(0.65, successProb - (hasLargeWorkforce ? 0.08 : 0.03) - (hasComplexConstraints ? 0.03 : 0));
787
+ const dataAvailability = Math.max(0.70, successProb - (hasMultipleSystems ? 0.04 : 0.01));
788
+ const integrationFeasibility = Math.max(0.70, successProb - (extracted.systems.length * 0.02));
437
789
  lines.push(`Success probability: **${(successProb * 100).toFixed(0)}%** (Technical ${(techFeasibility * 100).toFixed(0)}%, Organizational ${(orgReadiness * 100).toFixed(0)}%, Data ${(dataAvailability * 100).toFixed(0)}%, Integration ${(integrationFeasibility * 100).toFixed(0)}%) | Timeline: ${timeline}`, '');
438
790
  // The Opportunity — cost of inaction
439
791
  lines.push('## The Opportunity', '');
@@ -500,9 +852,12 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
500
852
  lines.push(step);
501
853
  }
502
854
  lines.push('');
503
- // Provenance
855
+ // Provenance — ADR-PIPELINE-033: include simulation lineage for decision traceability
504
856
  const sources = platformResults.filter(r => r.status >= 200 && r.status < 300).map(r => `${r.domain}/${r.agent}`);
505
857
  lines.push('---', '');
858
+ if (lineage.simulationId) {
859
+ lines.push(`*Simulation: ${lineage.simulationId}${lineage.traceId ? ` | Trace: ${lineage.traceId}` : ''}*`);
860
+ }
506
861
  lines.push(`*Sources: ${sources.slice(0, 8).join(', ')}${sources.length > 8 ? ` + ${sources.length - 8} more` : ''}*`);
507
862
  lines.push(`*Generated: ${now}*`);
508
863
  return lines.join('\n');
@@ -512,6 +867,7 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
512
867
  // ============================================================================
513
868
  export function renderDecisionMemo(query, simulationResult, platformResults) {
514
869
  const now = new Date().toISOString();
870
+ const lineage = extractSimulationLineage(simulationResult);
515
871
  const simPayload = extractSignalPayload(simulationResult);
516
872
  const simData = simPayload.data ?? {};
517
873
  const successProb = extractSuccessProbability(simData);
@@ -558,11 +914,21 @@ export function renderDecisionMemo(query, simulationResult, platformResults) {
558
914
  '## Options Considered', '',
559
915
  '| Option | Description | Investment | Timeline | Risk Level | Recommendation |',
560
916
  '|--------|-------------|-----------|----------|------------|---------------|',
561
- `| **A. Scoped Pilot** | Validate with subset of data and ${extracted.domain_entities.slice(0, 2).join(', ') || 'core entities'} | ${fin.budget || 'TBD'} (pilot scope) | 8-12 weeks | Low | **Recommended** |`,
562
- `| **B. Full Deployment** | Enterprise-wide rollout across all ${extracted.systems.length > 0 ? extracted.systems.join(', ') : 'systems'} | ${fin.budget || 'TBD'} | ${safeString(simData, 'timeline_estimate') || '16-24 weeks'} | Medium | After pilot validation |`,
563
- `| **C. Do Nothing** | Continue with manual processes | $0 | N/A | High | Not recommended — costs compound |`,
564
- '',
565
917
  ];
918
+ // Compute pilot cost as ~30% of full deployment
919
+ const fullBudget = fin.budget || 'TBD';
920
+ let pilotBudget = fullBudget;
921
+ const budgetNum = fin.budget?.match(/\$([0-9,.]+)([KMB]?)/i);
922
+ if (budgetNum) {
923
+ const num = parseFloat(budgetNum[1].replace(/,/g, ''));
924
+ const suffix = (budgetNum[2] || '').toUpperCase();
925
+ const mult = suffix === 'M' ? 1_000_000 : suffix === 'K' ? 1_000 : suffix === 'B' ? 1_000_000_000 : 1;
926
+ const fullVal = num * mult;
927
+ const pilotVal = fullVal * 0.3;
928
+ const fmt = (n) => n >= 1_000_000 ? `$${(n / 1_000_000).toFixed(1)}M` : `$${(n / 1000).toFixed(0)}K`;
929
+ pilotBudget = `${fmt(pilotVal)} - ${fmt(pilotVal * 1.5)}`;
930
+ }
931
+ 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'} |`, '');
566
932
  // Financial Impact
567
933
  lines.push('## Financial Impact', '');
568
934
  if (fin.hasData) {
@@ -655,9 +1021,12 @@ export function renderDecisionMemo(query, simulationResult, platformResults) {
655
1021
  lines.push(`| ${row.name} | ${row.role} | ${row.impact} | ${row.action} |`);
656
1022
  }
657
1023
  lines.push('');
658
- // Provenance
1024
+ // Provenance — ADR-PIPELINE-033: include simulation lineage
659
1025
  const sources = platformResults.filter(r => r.status >= 200 && r.status < 300).map(r => `${r.domain}/${r.agent}`);
660
1026
  lines.push('---', '');
1027
+ if (lineage.simulationId) {
1028
+ lines.push(`*Simulation: ${lineage.simulationId}${lineage.traceId ? ` | Trace: ${lineage.traceId}` : ''}*`);
1029
+ }
661
1030
  lines.push(`*Sources: ${sources.slice(0, 6).join(', ')}${sources.length > 6 ? ` + ${sources.length - 6} more` : ''}*`);
662
1031
  lines.push(`*Generated: ${now}*`);
663
1032
  return lines.join('\n');
@@ -746,16 +1115,28 @@ const STAKEHOLDER_PATTERNS = [
746
1115
  [/\bunion\s*(representative|rep|delegate|leadership)\b/i, 'Union Representatives'],
747
1116
  // Finance & Procurement
748
1117
  [/\bfinance\s*(team|department|controller|director|manager)\b/i, 'Finance / CFO'],
749
- [/\bprocurement\s*(team|manager|officer)\b/i, 'Procurement'],
1118
+ [/\bprocurement\s*(team|manager|officer|director|lead|head)\b/i, 'Procurement Leadership'],
1119
+ [/\bsupplier\s*(relationship|management)\s*(manager|team|lead)?\b/i, 'Supplier Relationship Management'],
1120
+ [/\bcategory\s*(manager|management|lead)\b/i, 'Category Management'],
1121
+ [/\bpackaging\s*(engineer|team|specialist|manager)\b/i, 'Packaging Engineering'],
1122
+ [/\bsourcing\s*(team|manager|specialist|director)\b/i, 'Strategic Sourcing'],
1123
+ // Sustainability / ESG
1124
+ [/\bsustainability\s*(team|officer|director|manager|lead|vp)\b/i, 'Sustainability Leadership'],
1125
+ [/\besg\s*(team|reporting|officer|analyst|director)\b/i, 'ESG Reporting'],
1126
+ [/\btravel\s*(policy|manager|team|coordinator)\b/i, 'Travel Policy Management'],
1127
+ [/\bcarbon\s*(manager|team|officer|analyst)\b/i, 'Carbon Management'],
1128
+ [/\benvironment(?:al)?\s*(team|officer|manager|director|health|safety)\b/i, 'Environmental Health & Safety'],
750
1129
  // Governance & Compliance
751
1130
  [/\bcompliance\s*(team|officer|department|manager)\b/i, 'Compliance & Regulatory Affairs'],
752
1131
  [/\bregulat(?:ory|or)\s*(affairs|team|compliance|body|bodies|commission)?\b/i, 'Regulatory Affairs'],
753
1132
  [/\bsecurity\s*(team|officer|department)\b/i, 'Security Team'],
754
1133
  [/\bauditor[s]?\b/i, 'Auditors'],
1134
+ [/\bquality\s*(assurance|team|manager|engineer|control)\b/i, 'Quality Assurance'],
755
1135
  // General
756
1136
  [/\bmanager[s]?\b/i, 'Team Managers'],
757
1137
  [/\bemployee[s]?\b/i, 'Workforce'],
758
1138
  [/\bvendor[s]?\b/i, 'External Vendors'],
1139
+ [/\bsupplier[s]?\b/i, 'Suppliers'],
759
1140
  [/\bcustomer[s]?\b/i, 'Customers / Ratepayers'],
760
1141
  [/\bstakeholder[s]?\b/i, 'Stakeholders'],
761
1142
  ];
@@ -1007,28 +1388,87 @@ function extractScenarioFromQuery(query) {
1007
1388
  * capitalizes the first word, and truncates to a readable length.
1008
1389
  */
1009
1390
  function distillProblemStatement(query) {
1010
- // Remove common preambles
1011
- let cleaned = query
1391
+ // ADR-PIPELINE-030 §1: Extract a consulting-grade problem statement.
1392
+ // Strategy: try targeted extraction first, then fall back to stripping.
1393
+ // ── Strategy 1: Extract the core objective directly ──
1394
+ // "We believe AI could help us better understand and reduce unnecessary packaging"
1395
+ // "Ideally, the system would review travel and expense patterns"
1396
+ // These sentences ARE the problem statement — use them directly when found.
1397
+ const objectivePatterns = [
1398
+ /(?:we believe|we think)\s+(?:AI|the system|a system|an AI|this)\s+(?:could|can|would|should|might)\s+(?:help\s+(?:us|the\s+\w+)\s+)?(.{30,250}?)(?:\.\s|$)/i,
1399
+ /(?:ideally),?\s+the\s+system\s+would:?\s*\n?\s*-?\s*(.{20,200}?)(?:\n-|\.\s|$)/i,
1400
+ /(?:the goal|the objective|our goal|our objective)\s+is\s+(?:to\s+)?(.{20,200}?)(?:\.\s|$)/i,
1401
+ /(?:what we want|what we need)\s+(?:is|to)\s+(.{20,200}?)(?:\.\s|$)/i,
1402
+ ];
1403
+ for (const pat of objectivePatterns) {
1404
+ const m = query.match(pat);
1405
+ if (m?.[1]) {
1406
+ let objective = m[1].trim().replace(/[.!?,;:]+$/, '');
1407
+ // Clean leading articles/conjunctions
1408
+ objective = objective.replace(/^(?:to\s+)?(?:better\s+)?/i, '');
1409
+ if (objective.length > 20) {
1410
+ return capitalize(objective).slice(0, 150);
1411
+ }
1412
+ }
1413
+ }
1414
+ // ── Strategy 2: Extract from "One of the challenges..." ──
1415
+ const challengeMatch = query.match(/one of the (?:challenges|issues|problems)\s+(?:we face|I face|they face)\s+is\s+(.{20,250}?)(?:\.\s|$)/i);
1416
+ if (challengeMatch?.[1]) {
1417
+ return capitalize(challengeMatch[1].trim().replace(/[.!?,;:]+$/, '')).slice(0, 150);
1418
+ }
1419
+ // ── Strategy 3: Strip biographical context iteratively ──
1420
+ let cleaned = query;
1421
+ // Keep stripping context sentences until we hit something that isn't biographical
1422
+ const bioPatterns = [
1423
+ /^I\s+(?:lead|manage|run|oversee|am responsible for|work in|head)\s+[^.]+\.\s*/i,
1424
+ /^(?:We are|Our (?:company|organization|firm|team) is)\s+[^.]+\.\s*/i,
1425
+ /^(?:Our|The)\s+(?:finance|HR|procurement|operations|IT|supply chain)[^.]+\.\s*/i,
1426
+ /^(?:We have|We operate|We work with|We serve)\s+[^.]+\.\s*/i,
1427
+ /^(?:This includes|These include|This covers)\s+[^.]+\.\s*/i,
1428
+ ];
1429
+ for (let i = 0; i < 5; i++) { // max 5 biographical sentences
1430
+ let stripped = false;
1431
+ for (const pat of bioPatterns) {
1432
+ if (pat.test(cleaned)) {
1433
+ cleaned = cleaned.replace(pat, '');
1434
+ stripped = true;
1435
+ break;
1436
+ }
1437
+ }
1438
+ if (!stripped)
1439
+ break;
1440
+ }
1441
+ // Strip context-setting sentences
1442
+ if (/^(right now|currently|at present|today|however|but|while|although)/i.test(cleaned)) {
1443
+ const nextSentence = cleaned.replace(/^[^.]+\.\s*/, '');
1444
+ if (nextSentence.length > 30)
1445
+ cleaned = nextSentence;
1446
+ }
1447
+ // Strip intent preambles
1448
+ cleaned = cleaned
1012
1449
  .replace(/^(?:I want to|I'd like to|I need to|We need to|We want to|Please|Can you|Could you)\s+/i, '')
1013
1450
  .replace(/^(?:build|create|design|develop|implement|generate|make)\s+(?:a\s+|an\s+)?/i, '')
1014
1451
  .replace(/^(?:working\s+)?prototype\s+(?:for|of|that)\s+/i, '')
1015
1452
  .replace(/^(?:proof of concept|poc)\s+(?:for|of|that)\s+/i, '')
1016
1453
  .trim();
1017
- // Capitalize first letter
1018
- if (cleaned.length > 0) {
1019
- cleaned = cleaned[0].toUpperCase() + cleaned.slice(1);
1020
- }
1021
1454
  // Truncate at sentence boundary if too long
1022
- if (cleaned.length > 120) {
1023
- const boundary = cleaned.lastIndexOf('.', 120);
1455
+ if (cleaned.length > 150) {
1456
+ const boundary = cleaned.lastIndexOf('.', 150);
1024
1457
  if (boundary > 40) {
1025
1458
  cleaned = cleaned.slice(0, boundary + 1);
1026
1459
  }
1027
1460
  else {
1028
- cleaned = cleaned.slice(0, 120).trim() + '...';
1461
+ const truncated = cleaned.slice(0, 150);
1462
+ const lastSpace = truncated.lastIndexOf(' ');
1463
+ cleaned = (lastSpace > 100 ? truncated.slice(0, lastSpace) : truncated).trim();
1029
1464
  }
1030
1465
  }
1031
- return cleaned || query.slice(0, 120);
1466
+ return capitalize(cleaned) || query.slice(0, 150);
1467
+ }
1468
+ function capitalize(text) {
1469
+ if (text.length === 0)
1470
+ return text;
1471
+ return text[0].toUpperCase() + text.slice(1);
1032
1472
  }
1033
1473
  /**
1034
1474
  * Synthesize agent response data into readable prose bullet points.
@@ -1359,6 +1799,7 @@ export function renderFinancialAnalysis(query, simulationResult, platformResults
1359
1799
  const simPayload = extractSignalPayload(simulationResult);
1360
1800
  const simData = simPayload.data ?? {};
1361
1801
  const fin = synthesizeFinancials(simData, platformResults, query);
1802
+ const lineage = extractSimulationLineage(simulationResult);
1362
1803
  const successProb = extractSuccessProbability(simData);
1363
1804
  const extracted = extractScenarioFromQuery(query);
1364
1805
  const roiData = extractAgentData(platformResults, 'costops', 'roi');
@@ -1492,6 +1933,9 @@ export function renderFinancialAnalysis(query, simulationResult, platformResults
1492
1933
  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% |`);
1493
1934
  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% |`);
1494
1935
  lines.push('', '*Scenario analysis assumes ±20% cost variance and ±30% timeline variance from base case.*', '');
1936
+ if (lineage.simulationId) {
1937
+ lines.push(`*Simulation: ${lineage.simulationId}${lineage.traceId ? ` | Trace: ${lineage.traceId}` : ''}*`);
1938
+ }
1495
1939
  lines.push(`*Sources: ${sources.length > 0 ? sources.join(', ') : 'simulation data + industry benchmarks'}*`);
1496
1940
  lines.push(`*Generated: ${now}*`);
1497
1941
  return lines.join('\n');