@llm-dev-ops/agentics-cli 1.7.2 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/erp.d.ts +8 -1
- package/dist/commands/erp.d.ts.map +1 -1
- package/dist/commands/erp.js +150 -37
- package/dist/commands/erp.js.map +1 -1
- package/dist/pipeline/auto-chain.d.ts +0 -6
- package/dist/pipeline/auto-chain.d.ts.map +1 -1
- package/dist/pipeline/auto-chain.js +325 -9
- package/dist/pipeline/auto-chain.js.map +1 -1
- package/dist/pipeline/phase2/phases/adr-generator.d.ts.map +1 -1
- package/dist/pipeline/phase2/phases/adr-generator.js +214 -9
- package/dist/pipeline/phase2/phases/adr-generator.js.map +1 -1
- package/dist/pipeline/phase2/phases/ddd-generator.d.ts.map +1 -1
- package/dist/pipeline/phase2/phases/ddd-generator.js +117 -1
- package/dist/pipeline/phase2/phases/ddd-generator.js.map +1 -1
- package/dist/pipeline/phase2/phases/prompt-generator.js +76 -4
- package/dist/pipeline/phase2/phases/prompt-generator.js.map +1 -1
- package/dist/pipeline/phase2/phases/research-dossier.d.ts.map +1 -1
- package/dist/pipeline/phase2/phases/research-dossier.js +39 -1
- package/dist/pipeline/phase2/phases/research-dossier.js.map +1 -1
- package/dist/synthesis/ask-artifact-writer.d.ts.map +1 -1
- package/dist/synthesis/ask-artifact-writer.js +37 -2
- package/dist/synthesis/ask-artifact-writer.js.map +1 -1
- package/dist/synthesis/simulation-renderers.d.ts.map +1 -1
- package/dist/synthesis/simulation-renderers.js +355 -48
- package/dist/synthesis/simulation-renderers.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
255
|
-
const isPharma = lower.includes('pharma') || lower.includes('drug') || lower.includes('gxp')
|
|
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')
|
|
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 (
|
|
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 =
|
|
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);
|
|
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
|
-
|
|
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
|
-
//
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
.
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
//
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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');
|