@llm-dev-ops/agentics-cli 1.7.0 → 1.7.2

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.
@@ -251,24 +251,62 @@ function estimateFinancialsFromQuery(query) {
251
251
  }
252
252
  }
253
253
  }
254
- // Industry affects per-employee costs
254
+ // Industry affects per-employee costs and savings multiplier
255
255
  const isPharma = lower.includes('pharma') || lower.includes('drug') || lower.includes('gxp') || lower.includes('clinical');
256
256
  const isFinancial = lower.includes('financial') || lower.includes('banking') || lower.includes('insurance');
257
257
  const isRetail = lower.includes('retail') || lower.includes('grocery') || lower.includes('store');
258
- const costPerEmployee = isPharma ? 35 : isFinancial ? 30 : isRetail ? 15 : 25;
259
- const savingsMultiplier = isPharma ? 80 : isFinancial ? 60 : isRetail ? 40 : 50;
258
+ const isUtility = lower.includes('water') || lower.includes('utility') || lower.includes('energy') || lower.includes('grid') || lower.includes('distribution');
259
+ 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
+ const isProfessionalServices = lower.includes('professional services') || lower.includes('consulting') || lower.includes('advisory');
262
+ let industryLabel = 'enterprise';
263
+ let costPerEmployee = 25;
264
+ let savingsMultiplier = 50;
265
+ if (isPharma) {
266
+ costPerEmployee = 35;
267
+ savingsMultiplier = 80;
268
+ industryLabel = 'pharmaceutical';
269
+ }
270
+ else if (isFinancial) {
271
+ costPerEmployee = 30;
272
+ savingsMultiplier = 60;
273
+ industryLabel = 'financial services';
274
+ }
275
+ else if (isRetail) {
276
+ costPerEmployee = 15;
277
+ savingsMultiplier = 40;
278
+ industryLabel = 'retail';
279
+ }
280
+ else if (isUtility) {
281
+ costPerEmployee = 20;
282
+ savingsMultiplier = 55;
283
+ industryLabel = 'utility';
284
+ }
285
+ else if (isHealthcare) {
286
+ costPerEmployee = 30;
287
+ savingsMultiplier = 65;
288
+ industryLabel = 'healthcare';
289
+ }
290
+ else if (isSustainability || isProfessionalServices) {
291
+ costPerEmployee = 25;
292
+ savingsMultiplier = 50;
293
+ industryLabel = isSustainability ? 'sustainability' : 'professional services';
294
+ }
260
295
  const pilotCost = Math.round(employees * costPerEmployee / 1000) * 1000;
261
296
  const annualSavings = Math.round(employees * savingsMultiplier / 1000) * 1000;
262
297
  const paybackMonths = Math.max(6, Math.min(24, Math.round(pilotCost / (annualSavings / 12))));
263
298
  const npv5yr = Math.round(annualSavings * 3.5); // ~10% discount rate, 5 years
264
299
  const roiPct = Math.round((annualSavings / pilotCost) * 100);
265
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
+ const methodology = `${employees.toLocaleString()} employees × $${costPerEmployee}/employee ${industryLabel} benchmark`;
303
+ const savingsMethod = `${employees.toLocaleString()} employees × $${savingsMultiplier}/employee annual efficiency gain`;
266
304
  return {
267
- budget: `${fmt(pilotCost)} - ${fmt(pilotCost * 1.5)} (estimated based on ${employees.toLocaleString()} employees)`,
268
- roi: `${roiPct}% projected (estimated)`,
269
- npv: `${fmt(npv5yr)} 5-year NPV (estimated, 10% discount rate)`,
270
- payback: `${paybackMonths}-${paybackMonths + 6} months (estimated)`,
271
- revenue: `${fmt(annualSavings)} annual efficiency gains (estimated)`,
305
+ budget: `${fmt(pilotCost)} - ${fmt(pilotCost * 1.5)} (${methodology})`,
306
+ roi: `${roiPct}% projected (${fmt(annualSavings)} savings ÷ ${fmt(pilotCost)} investment)`,
307
+ npv: `${fmt(npv5yr)} 5-year NPV (10% discount rate, ${savingsMethod})`,
308
+ payback: `${paybackMonths}-${paybackMonths + 6} months (break-even when cumulative savings exceed investment)`,
309
+ revenue: `${fmt(annualSavings)} annual efficiency gains (${savingsMethod})`,
272
310
  costSavings: '',
273
311
  hasData: true,
274
312
  isEstimated: true,
@@ -325,17 +363,58 @@ function generateDomainRisks(query, extracted) {
325
363
  const q = query.toLowerCase();
326
364
  const systems = extracted.systems;
327
365
  const primarySystem = systems[0] ?? 'enterprise platform';
328
- // Every project has these
329
- risks.push({
330
- risk: `Data quality and completeness in ${primarySystem} may not meet analytical requirements`,
331
- category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
332
- mitigation: 'Data profiling during discovery phase; validation layer with quarantine for invalid records',
333
- });
334
- risks.push({
335
- risk: `${primarySystem} API integration complexity — rate limits, schema changes, authentication`,
336
- category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
337
- mitigation: `Technical spike to validate ${primarySystem} API capabilities before full commitment`,
338
- });
366
+ // System-specific data quality risks (not generic)
367
+ const isWorkday = /workday/i.test(q);
368
+ const isSAP = /sap|s\/4hana/i.test(q);
369
+ const isMaximo = /maximo|ibm/i.test(q);
370
+ if (isWorkday) {
371
+ risks.push({
372
+ 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`,
373
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
374
+ mitigation: 'Map required fields per Workday module (Expenses, Absence, Worker) during discovery; build validation for missing cost center and location codes',
375
+ });
376
+ risks.push({
377
+ 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'}`,
378
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
379
+ mitigation: 'Implement incremental sync with delta queries; cache frequently accessed worker profiles; use Workday Integration Cloud for bulk data',
380
+ });
381
+ }
382
+ else if (isSAP) {
383
+ risks.push({
384
+ risk: `SAP S/4HANA OData API field mappings differ between on-premise and cloud editions — custom fields may not be accessible via standard APIs`,
385
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
386
+ mitigation: 'Validate API access for each required field during technical spike; plan CDS view development for custom field exposure if needed',
387
+ });
388
+ risks.push({
389
+ risk: `SAP API authentication (OAuth2 + X-CSRF tokens) and transport security requirements add integration complexity`,
390
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
391
+ mitigation: 'Technical spike with SAP Basis team to validate API configuration; use SAP BTP Integration Suite if direct API access is restricted',
392
+ });
393
+ }
394
+ else if (isMaximo) {
395
+ risks.push({
396
+ risk: `IBM Maximo OSLC API may not expose all required asset and maintenance fields — custom attributes require explicit OSLC resource definition`,
397
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
398
+ mitigation: 'Audit Maximo OSLC resource definitions during discovery; validate custom field availability via test queries against staging environment',
399
+ });
400
+ risks.push({
401
+ risk: `Maximo 7.x vs. MAS 8.x OSLC API differences — work order status transitions and custom field schemas differ between versions`,
402
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
403
+ mitigation: 'Confirm Maximo version and OSLC API version during technical spike; build version-aware adapter with feature detection',
404
+ });
405
+ }
406
+ else {
407
+ risks.push({
408
+ risk: `Data quality and completeness in ${primarySystem} may not meet analytical requirements`,
409
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
410
+ mitigation: 'Data profiling during discovery phase; validation layer with quarantine for invalid records',
411
+ });
412
+ risks.push({
413
+ risk: `${primarySystem} API integration complexity — rate limits, schema changes, authentication`,
414
+ category: 'Technical', likelihood: 'Medium', impact: 'High', score: 6,
415
+ mitigation: `Technical spike to validate ${primarySystem} API capabilities before full commitment`,
416
+ });
417
+ }
339
418
  risks.push({
340
419
  risk: 'Scope creep beyond initial prototype boundaries',
341
420
  category: 'Project', likelihood: 'High', impact: 'Medium', score: 6,
@@ -357,12 +436,32 @@ function generateDomainRisks(query, extracted) {
357
436
  mitigation: 'Legal review per jurisdiction during discovery; configurable compliance rules per region',
358
437
  });
359
438
  }
360
- // Sustainability risks
439
+ // Sustainability / emissions risks
361
440
  if (/(?:emission|carbon|sustainab|environmental|energy|waste|esg)/i.test(q)) {
362
441
  risks.push({
363
- risk: 'Emissions factor accuracy — default factors may not match specific operational context',
442
+ risk: 'Emissions factor accuracy — DEFRA/EPA default factors may not match specific operational context (e.g., regional grid mix, fleet composition)',
364
443
  category: 'Data', likelihood: 'Medium', impact: 'Medium', score: 4,
365
- mitigation: 'Allow user-supplied factors with audit trail; flag calculations using default vs. measured values',
444
+ mitigation: 'Allow organization-specific factors with audit trail; flag calculations using default vs. measured values; annual factor review process',
445
+ });
446
+ if (/travel|commut|flight|transport/i.test(q)) {
447
+ risks.push({
448
+ risk: 'Employee behavior change resistance — sustainability nudges may be perceived as restrictive or intrusive by frequent travelers',
449
+ category: 'Organizational', likelihood: 'Medium', impact: 'Medium', score: 4,
450
+ mitigation: 'Frame as insights, not mandates; gamification and positive reinforcement; executive sponsorship for culture shift',
451
+ });
452
+ }
453
+ }
454
+ // Water/utility-specific risks
455
+ if (/(?:water|pipe|leak|pressure|distribution|meter|flow)/i.test(q)) {
456
+ risks.push({
457
+ risk: 'Sensor data gaps — pressure and flow telemetry may have coverage holes in older distribution zones with limited SCADA instrumentation',
458
+ category: 'Data', likelihood: 'High', impact: 'Medium', score: 6,
459
+ mitigation: 'Map sensor coverage per DMA during discovery; define minimum instrumentation requirements for analysis zones',
460
+ });
461
+ risks.push({
462
+ risk: 'Infrastructure intervention timelines — pipe replacement and pressure management projects require permitting and construction windows that may extend beyond prototype evaluation period',
463
+ category: 'Operational', likelihood: 'Medium', impact: 'Medium', score: 4,
464
+ mitigation: 'Focus prototype on detection and prioritization; track intervention recommendations separately from execution timelines',
366
465
  });
367
466
  }
368
467
  // Financial risks
@@ -429,11 +528,16 @@ export function renderExecutiveSummary(query, simulationResult, platformResults)
429
528
  lines.push(`Projected impact: ${impactParts.join(', ')}.`);
430
529
  }
431
530
  }
432
- // ADR-PIPELINE-025 §4: Success probability with decomposition
433
- const techFeasibility = Math.min(0.95, successProb + 0.04);
434
- const orgReadiness = Math.max(0.70, successProb - 0.03);
435
- const dataAvailability = successProb;
436
- const integrationFeasibility = Math.max(0.75, successProb - 0.01);
531
+ // ADR-PIPELINE-025 §4: Success probability with domain-aware decomposition
532
+ // Technical feasibility is higher (standard APIs/patterns), organizational readiness is lower
533
+ // for large workforces, data availability depends on ERP maturity, integration depends on system count
534
+ const hasLargeWorkforce = extracted.stakeholders.length > 3 || query.length > 800;
535
+ const hasMultipleSystems = extracted.systems.length > 1;
536
+ const hasComplexConstraints = extracted.constraints.length > 2;
537
+ const techFeasibility = Math.min(0.95, successProb + (hasMultipleSystems ? 0.02 : 0.06));
538
+ const orgReadiness = Math.max(0.65, successProb - (hasLargeWorkforce ? 0.08 : 0.03) - (hasComplexConstraints ? 0.03 : 0));
539
+ const dataAvailability = Math.max(0.70, successProb - (hasMultipleSystems ? 0.04 : 0.01));
540
+ const integrationFeasibility = Math.max(0.70, successProb - (extracted.systems.length * 0.02));
437
541
  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
542
  // The Opportunity — cost of inaction
439
543
  lines.push('## The Opportunity', '');
@@ -558,11 +662,21 @@ export function renderDecisionMemo(query, simulationResult, platformResults) {
558
662
  '## Options Considered', '',
559
663
  '| Option | Description | Investment | Timeline | Risk Level | Recommendation |',
560
664
  '|--------|-------------|-----------|----------|------------|---------------|',
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
665
  ];
666
+ // Compute pilot cost as ~30% of full deployment
667
+ const fullBudget = fin.budget || 'TBD';
668
+ let pilotBudget = fullBudget;
669
+ const budgetNum = fin.budget?.match(/\$([0-9,.]+)([KMB]?)/i);
670
+ if (budgetNum) {
671
+ const num = parseFloat(budgetNum[1].replace(/,/g, ''));
672
+ const suffix = (budgetNum[2] || '').toUpperCase();
673
+ const mult = suffix === 'M' ? 1_000_000 : suffix === 'K' ? 1_000 : suffix === 'B' ? 1_000_000_000 : 1;
674
+ const fullVal = num * mult;
675
+ const pilotVal = fullVal * 0.3;
676
+ const fmt = (n) => n >= 1_000_000 ? `$${(n / 1_000_000).toFixed(1)}M` : `$${(n / 1000).toFixed(0)}K`;
677
+ pilotBudget = `${fmt(pilotVal)} - ${fmt(pilotVal * 1.5)}`;
678
+ }
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 |`, '');
566
680
  // Financial Impact
567
681
  lines.push('## Financial Impact', '');
568
682
  if (fin.hasData) {
@@ -1007,28 +1121,51 @@ function extractScenarioFromQuery(query) {
1007
1121
  * capitalizes the first word, and truncates to a readable length.
1008
1122
  */
1009
1123
  function distillProblemStatement(query) {
1010
- // Remove common preambles
1124
+ // Remove common preambles and context-setting openings
1011
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
1012
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, '')
1013
1134
  .replace(/^(?:build|create|design|develop|implement|generate|make)\s+(?:a\s+|an\s+)?/i, '')
1014
1135
  .replace(/^(?:working\s+)?prototype\s+(?:for|of|that)\s+/i, '')
1015
1136
  .replace(/^(?:proof of concept|poc)\s+(?:for|of|that)\s+/i, '')
1016
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
1140
+ 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
+ const nextSentence = cleaned.replace(/^[^.]+\.\s*/, '');
1143
+ if (nextSentence.length > 30)
1144
+ cleaned = nextSentence;
1145
+ }
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
+ }
1017
1151
  // Capitalize first letter
1018
1152
  if (cleaned.length > 0) {
1019
1153
  cleaned = cleaned[0].toUpperCase() + cleaned.slice(1);
1020
1154
  }
1021
1155
  // Truncate at sentence boundary if too long
1022
- if (cleaned.length > 120) {
1023
- const boundary = cleaned.lastIndexOf('.', 120);
1156
+ if (cleaned.length > 150) {
1157
+ const boundary = cleaned.lastIndexOf('.', 150);
1024
1158
  if (boundary > 40) {
1025
1159
  cleaned = cleaned.slice(0, boundary + 1);
1026
1160
  }
1027
1161
  else {
1028
- cleaned = cleaned.slice(0, 120).trim() + '...';
1162
+ // Truncate at last word boundary
1163
+ const truncated = cleaned.slice(0, 150);
1164
+ const lastSpace = truncated.lastIndexOf(' ');
1165
+ cleaned = (lastSpace > 100 ? truncated.slice(0, lastSpace) : truncated).trim();
1029
1166
  }
1030
1167
  }
1031
- return cleaned || query.slice(0, 120);
1168
+ return cleaned || query.slice(0, 150);
1032
1169
  }
1033
1170
  /**
1034
1171
  * Synthesize agent response data into readable prose bullet points.