@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.
- 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.map +1 -1
- package/dist/pipeline/auto-chain.js +223 -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 +58 -10
- 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 +494 -50
- 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,29 +273,232 @@ 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
|
|
259
|
-
const
|
|
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);
|
|
337
|
+
const npv5yr = Math.round(annualSavings * 3.5);
|
|
264
338
|
const roiPct = Math.round((annualSavings / pilotCost) * 100);
|
|
265
|
-
const
|
|
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)} (
|
|
268
|
-
roi: `${roiPct}% projected (
|
|
269
|
-
npv: `${fmt(npv5yr)} 5-year NPV (
|
|
270
|
-
payback: `${paybackMonths}-${paybackMonths + 6} months (
|
|
271
|
-
revenue: `${fmt(annualSavings)} annual efficiency gains (
|
|
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
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
const
|
|
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
|
-
//
|
|
1011
|
-
|
|
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 >
|
|
1023
|
-
const boundary = cleaned.lastIndexOf('.',
|
|
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
|
-
|
|
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,
|
|
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');
|