@llm-dev-ops/agentics-cli 1.7.2 → 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,14 +273,16 @@ function estimateFinancialsFromQuery(query) {
251
273
  }
252
274
  }
253
275
  }
254
- // Industry affects per-employee costs and savings multiplier
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 isUtility = lower.includes('water') || lower.includes('utility') || lower.includes('energy') || lower.includes('grid') || lower.includes('distribution');
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');
259
282
  const isSustainability = lower.includes('sustainability') || lower.includes('emissions') || lower.includes('carbon') || lower.includes('esg');
260
- const isHealthcare = lower.includes('healthcare') || lower.includes('hospital') || lower.includes('clinical') || lower.includes('patient');
261
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');
262
286
  let industryLabel = 'enterprise';
263
287
  let costPerEmployee = 25;
264
288
  let savingsMultiplier = 50;
@@ -280,25 +304,38 @@ function estimateFinancialsFromQuery(query) {
280
304
  else if (isUtility) {
281
305
  costPerEmployee = 20;
282
306
  savingsMultiplier = 55;
283
- industryLabel = 'utility';
307
+ industryLabel = 'utility infrastructure';
284
308
  }
285
309
  else if (isHealthcare) {
286
310
  costPerEmployee = 30;
287
311
  savingsMultiplier = 65;
288
312
  industryLabel = 'healthcare';
289
313
  }
290
- else if (isSustainability || isProfessionalServices) {
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) {
291
325
  costPerEmployee = 25;
292
326
  savingsMultiplier = 50;
293
- industryLabel = isSustainability ? 'sustainability' : 'professional services';
327
+ industryLabel = 'sustainability';
328
+ }
329
+ else if (isProfessionalServices) {
330
+ costPerEmployee = 25;
331
+ savingsMultiplier = 50;
332
+ industryLabel = 'professional services';
294
333
  }
295
334
  const pilotCost = Math.round(employees * costPerEmployee / 1000) * 1000;
296
335
  const annualSavings = Math.round(employees * savingsMultiplier / 1000) * 1000;
297
336
  const paybackMonths = Math.max(6, Math.min(24, Math.round(pilotCost / (annualSavings / 12))));
298
- const npv5yr = Math.round(annualSavings * 3.5); // ~10% discount rate, 5 years
337
+ const npv5yr = Math.round(annualSavings * 3.5);
299
338
  const roiPct = Math.round((annualSavings / pilotCost) * 100);
300
- const fmt = (n) => n >= 1_000_000 ? `$${(n / 1_000_000).toFixed(1)}M` : `$${(n / 1000).toFixed(0)}K`;
301
- // Build methodology string so the numbers are defensible
302
339
  const methodology = `${employees.toLocaleString()} employees × $${costPerEmployee}/employee ${industryLabel} benchmark`;
303
340
  const savingsMethod = `${employees.toLocaleString()} employees × $${savingsMultiplier}/employee annual efficiency gain`;
304
341
  return {
@@ -312,6 +349,156 @@ function estimateFinancialsFromQuery(query) {
312
349
  isEstimated: true,
313
350
  };
314
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
+ }
315
502
  // extractFinancials is now synthesizeFinancials — kept as export for external callers
316
503
  export { synthesizeFinancials as extractFinancials };
317
504
  /**
@@ -366,7 +553,10 @@ function generateDomainRisks(query, extracted) {
366
553
  // System-specific data quality risks (not generic)
367
554
  const isWorkday = /workday/i.test(q);
368
555
  const isSAP = /sap|s\/4hana/i.test(q);
369
- const isMaximo = /maximo|ibm/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);
370
560
  if (isWorkday) {
371
561
  risks.push({
372
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`,
@@ -403,6 +593,37 @@ function generateDomainRisks(query, extracted) {
403
593
  mitigation: 'Confirm Maximo version and OSLC API version during technical spike; build version-aware adapter with feature detection',
404
594
  });
405
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
+ }
406
627
  else {
407
628
  risks.push({
408
629
  risk: `Data quality and completeness in ${primarySystem} may not meet analytical requirements`,
@@ -451,8 +672,13 @@ function generateDomainRisks(query, extracted) {
451
672
  });
452
673
  }
453
674
  }
454
- // Water/utility-specific risks
455
- if (/(?:water|pipe|leak|pressure|distribution|meter|flow)/i.test(q)) {
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) {
456
682
  risks.push({
457
683
  risk: 'Sensor data gaps — pressure and flow telemetry may have coverage holes in older distribution zones with limited SCADA instrumentation',
458
684
  category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
@@ -472,6 +698,27 @@ function generateDomainRisks(query, extracted) {
472
698
  mitigation: 'Sensitivity analysis on key assumptions; quarterly re-validation of financial model',
473
699
  });
474
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
+ }
475
722
  // Always include vendor dependency
476
723
  risks.push({
477
724
  risk: 'Technology vendor dependency — platform availability, pricing changes, or API deprecation',
@@ -487,6 +734,7 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
487
734
  const now = new Date().toISOString();
488
735
  const simPayload = extractSignalPayload(simulationResult);
489
736
  const simData = simPayload.data ?? {};
737
+ const lineage = extractSimulationLineage(simulationResult);
490
738
  const successProb = extractSuccessProbability(simData);
491
739
  const timeline = safeString(simData, 'timeline_estimate') || safeString(simData, 'timeline') || '12-18 weeks';
492
740
  const problemStatement = distillProblemStatement(query);
@@ -604,9 +852,12 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
604
852
  lines.push(step);
605
853
  }
606
854
  lines.push('');
607
- // Provenance
855
+ // Provenance — ADR-PIPELINE-033: include simulation lineage for decision traceability
608
856
  const sources = platformResults.filter(r => r.status >= 200 && r.status < 300).map(r => `${r.domain}/${r.agent}`);
609
857
  lines.push('---', '');
858
+ if (lineage.simulationId) {
859
+ lines.push(`*Simulation: ${lineage.simulationId}${lineage.traceId ? ` | Trace: ${lineage.traceId}` : ''}*`);
860
+ }
610
861
  lines.push(`*Sources: ${sources.slice(0, 8).join(', ')}${sources.length > 8 ? ` + ${sources.length - 8} more` : ''}*`);
611
862
  lines.push(`*Generated: ${now}*`);
612
863
  return lines.join('\n');
@@ -616,6 +867,7 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
616
867
  // ============================================================================
617
868
  export function renderDecisionMemo(query, simulationResult, platformResults) {
618
869
  const now = new Date().toISOString();
870
+ const lineage = extractSimulationLineage(simulationResult);
619
871
  const simPayload = extractSignalPayload(simulationResult);
620
872
  const simData = simPayload.data ?? {};
621
873
  const successProb = extractSuccessProbability(simData);
@@ -676,7 +928,7 @@ export function renderDecisionMemo(query, simulationResult, platformResults) {
676
928
  const fmt = (n) => n >= 1_000_000 ? `$${(n / 1_000_000).toFixed(1)}M` : `$${(n / 1000).toFixed(0)}K`;
677
929
  pilotBudget = `${fmt(pilotVal)} - ${fmt(pilotVal * 1.5)}`;
678
930
  }
679
- 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 | N/A | High | Not recommended — costs compound |`, '');
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'} |`, '');
680
932
  // Financial Impact
681
933
  lines.push('## Financial Impact', '');
682
934
  if (fin.hasData) {
@@ -769,9 +1021,12 @@ export function renderDecisionMemo(query, simulationResult, platformResults) {
769
1021
  lines.push(`| ${row.name} | ${row.role} | ${row.impact} | ${row.action} |`);
770
1022
  }
771
1023
  lines.push('');
772
- // Provenance
1024
+ // Provenance — ADR-PIPELINE-033: include simulation lineage
773
1025
  const sources = platformResults.filter(r => r.status >= 200 && r.status < 300).map(r => `${r.domain}/${r.agent}`);
774
1026
  lines.push('---', '');
1027
+ if (lineage.simulationId) {
1028
+ lines.push(`*Simulation: ${lineage.simulationId}${lineage.traceId ? ` | Trace: ${lineage.traceId}` : ''}*`);
1029
+ }
775
1030
  lines.push(`*Sources: ${sources.slice(0, 6).join(', ')}${sources.length > 6 ? ` + ${sources.length - 6} more` : ''}*`);
776
1031
  lines.push(`*Generated: ${now}*`);
777
1032
  return lines.join('\n');
@@ -860,16 +1115,28 @@ const STAKEHOLDER_PATTERNS = [
860
1115
  [/\bunion\s*(representative|rep|delegate|leadership)\b/i, 'Union Representatives'],
861
1116
  // Finance & Procurement
862
1117
  [/\bfinance\s*(team|department|controller|director|manager)\b/i, 'Finance / CFO'],
863
- [/\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'],
864
1129
  // Governance & Compliance
865
1130
  [/\bcompliance\s*(team|officer|department|manager)\b/i, 'Compliance & Regulatory Affairs'],
866
1131
  [/\bregulat(?:ory|or)\s*(affairs|team|compliance|body|bodies|commission)?\b/i, 'Regulatory Affairs'],
867
1132
  [/\bsecurity\s*(team|officer|department)\b/i, 'Security Team'],
868
1133
  [/\bauditor[s]?\b/i, 'Auditors'],
1134
+ [/\bquality\s*(assurance|team|manager|engineer|control)\b/i, 'Quality Assurance'],
869
1135
  // General
870
1136
  [/\bmanager[s]?\b/i, 'Team Managers'],
871
1137
  [/\bemployee[s]?\b/i, 'Workforce'],
872
1138
  [/\bvendor[s]?\b/i, 'External Vendors'],
1139
+ [/\bsupplier[s]?\b/i, 'Suppliers'],
873
1140
  [/\bcustomer[s]?\b/i, 'Customers / Ratepayers'],
874
1141
  [/\bstakeholder[s]?\b/i, 'Stakeholders'],
875
1142
  ];
@@ -1121,37 +1388,69 @@ function extractScenarioFromQuery(query) {
1121
1388
  * capitalizes the first word, and truncates to a readable length.
1122
1389
  */
1123
1390
  function distillProblemStatement(query) {
1124
- // Remove common preambles and context-setting openings
1125
- let cleaned = query
1126
- // Strip "I lead/manage/run..." biographical introductions
1127
- .replace(/^I\s+(?:lead|manage|run|oversee|am responsible for|work in|head)\s+[^.]+\.\s*/i, '')
1128
- // Strip "We are a..." / "Our company is..." organizational descriptions
1129
- .replace(/^(?:We are|Our (?:company|organization|firm|team) is)\s+[^.]+\.\s*/i, '')
1130
- // Strip "One of the challenges..." problem setup
1131
- .replace(/^One of the (?:challenges|issues|problems)\s+(?:we face|I face)\s+is\s+/i, '')
1132
- // Strip standard intent preambles
1133
- .replace(/^(?:I want to|I'd like to|I need to|We need to|We want to|Please|Can you|Could you)\s+/i, '')
1134
- .replace(/^(?:build|create|design|develop|implement|generate|make)\s+(?:a\s+|an\s+)?/i, '')
1135
- .replace(/^(?:working\s+)?prototype\s+(?:for|of|that)\s+/i, '')
1136
- .replace(/^(?:proof of concept|poc)\s+(?:for|of|that)\s+/i, '')
1137
- .trim();
1138
- // If the remaining text still starts with a lowercase conjunction or context word,
1139
- // try to find the first real problem/goal sentence
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
1140
1442
  if (/^(right now|currently|at present|today|however|but|while|although)/i.test(cleaned)) {
1141
- // Skip the context sentence and find the next one
1142
1443
  const nextSentence = cleaned.replace(/^[^.]+\.\s*/, '');
1143
1444
  if (nextSentence.length > 30)
1144
1445
  cleaned = nextSentence;
1145
1446
  }
1146
- // Try to extract the core objective from "We believe..." / "Ideally..." / "The system would..."
1147
- const idealMatch = query.match(/(?:we believe|ideally|the system (?:would|should)|the goal is|the objective is)\s+(.{30,200}?)(?:\.|$)/i);
1148
- if (idealMatch && idealMatch[1] && cleaned.length > 200) {
1149
- cleaned = idealMatch[1].trim();
1150
- }
1151
- // Capitalize first letter
1152
- if (cleaned.length > 0) {
1153
- cleaned = cleaned[0].toUpperCase() + cleaned.slice(1);
1154
- }
1447
+ // Strip intent preambles
1448
+ cleaned = cleaned
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, '')
1450
+ .replace(/^(?:build|create|design|develop|implement|generate|make)\s+(?:a\s+|an\s+)?/i, '')
1451
+ .replace(/^(?:working\s+)?prototype\s+(?:for|of|that)\s+/i, '')
1452
+ .replace(/^(?:proof of concept|poc)\s+(?:for|of|that)\s+/i, '')
1453
+ .trim();
1155
1454
  // Truncate at sentence boundary if too long
1156
1455
  if (cleaned.length > 150) {
1157
1456
  const boundary = cleaned.lastIndexOf('.', 150);
@@ -1159,13 +1458,17 @@ function distillProblemStatement(query) {
1159
1458
  cleaned = cleaned.slice(0, boundary + 1);
1160
1459
  }
1161
1460
  else {
1162
- // Truncate at last word boundary
1163
1461
  const truncated = cleaned.slice(0, 150);
1164
1462
  const lastSpace = truncated.lastIndexOf(' ');
1165
1463
  cleaned = (lastSpace > 100 ? truncated.slice(0, lastSpace) : truncated).trim();
1166
1464
  }
1167
1465
  }
1168
- return cleaned || query.slice(0, 150);
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);
1169
1472
  }
1170
1473
  /**
1171
1474
  * Synthesize agent response data into readable prose bullet points.
@@ -1496,6 +1799,7 @@ export function renderFinancialAnalysis(query, simulationResult, platformResults
1496
1799
  const simPayload = extractSignalPayload(simulationResult);
1497
1800
  const simData = simPayload.data ?? {};
1498
1801
  const fin = synthesizeFinancials(simData, platformResults, query);
1802
+ const lineage = extractSimulationLineage(simulationResult);
1499
1803
  const successProb = extractSuccessProbability(simData);
1500
1804
  const extracted = extractScenarioFromQuery(query);
1501
1805
  const roiData = extractAgentData(platformResults, 'costops', 'roi');
@@ -1629,6 +1933,9 @@ export function renderFinancialAnalysis(query, simulationResult, platformResults
1629
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% |`);
1630
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% |`);
1631
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
+ }
1632
1939
  lines.push(`*Sources: ${sources.length > 0 ? sources.join(', ') : 'simulation data + industry benchmarks'}*`);
1633
1940
  lines.push(`*Generated: ${now}*`);
1634
1941
  return lines.join('\n');