import { z } from 'zod';
import { mcpDb } from '../db/supabase.js';
import { MLComparisonEngine, MLComparisonResult } from '../services/ml-comparison-engine.js';
import { BenchmarkAggregator } from '../services/benchmark-aggregator.js';
import { DutchBenchmarkValidator, ValidationResult } from '../services/dutch-benchmark-validator.js';
import { createLogger } from '../utils/logger.js';
import { DatabaseError, ValidationError, ConfigurationError } from '../utils/errors.js';
import { UseCase } from '../schemas/use-case.js';
import { Projection } from '../schemas/projection.js';
import { Project } from '../schemas/project.js';
export const CompareProjectsSchema = z.object({
project_ids: z.array(z.string().uuid()).min(2).max(10),
comparison_metrics: z.array(z.enum([
'roi', 'payback_period', 'npv', 'total_investment',
'monthly_benefit', 'risk_score', 'implementation_complexity',
'success_probability', 'synergies'
])).default(['roi', 'payback_period', 'npv', 'risk_score', 'success_probability']),
time_horizon: z.number().min(12).max(120).default(60), // months
enable_ml_insights: z.boolean().default(true),
include_visualizations: z.boolean().default(false)
});
export type CompareProjectsInput = z.infer<typeof CompareProjectsSchema>;
// Comparison result interface
export interface ComparisonResult {
projects: Array<{
id: string;
name: string;
client: string;
industry: string;
status: string;
metrics: Record<string, number>;
mlInsights?: MLComparisonResult;
dutchMarketValidation?: {
adjustmentsMade: number;
validationIssues: ValidationResult['validationIssues'];
marketInsights: ValidationResult['marketInsights'];
};
benchmarkComparison?: {
metric: string;
projectValue: number;
industryAverage: number;
percentile: number;
}[];
}>;
rankings: {
byMetric: Record<string, string[]>; // metric -> ranked project IDs
overall: string[]; // overall ranking
mlBased?: string[]; // ML-based ranking
};
insights: {
bestPerformer: {
projectId: string;
reason: string;
};
riskiest: {
projectId: string;
risks: string[];
};
quickestPayback: {
projectId: string;
months: number;
};
synergies?: Array<{
projects: string[];
type: string;
value: number;
}>;
};
recommendations: string[];
dutchMarketSummary: {
totalAdjustments: number;
commonIssues: string[];
marketTrends: string[];
citations: Array<{ url: string; title: string }>;
};
visualization?: {
type: string;
data: any;
}[];
}
export async function compareProjects(
input: CompareProjectsInput
): Promise<ComparisonResult> {
const logger = createLogger({ tool: 'compare_projects' });
logger.info('Starting project comparison', {
project_count: input.project_ids.length,
ml_enabled: input.enable_ml_insights
});
try {
// Step 1: Check for Perplexity API key
const perplexityApiKey = process.env.PERPLEXITY_API_KEY;
if (!perplexityApiKey) {
throw new ConfigurationError(
'PERPLEXITY_API_KEY is required for project comparisons. Please configure it in your environment.'
);
}
// Step 2: Validate input
const validatedInput = CompareProjectsSchema.parse(input);
// Step 3: Fetch all project data in parallel
const projectData = await fetchProjectData(validatedInput.project_ids);
// Step 4: Initialize Dutch validator
const dutchValidator = new DutchBenchmarkValidator(perplexityApiKey);
const projectValidations = new Map<string, ValidationResult>();
// Step 5: Validate each project against Dutch benchmarks
logger.info('Validating projects against Dutch market benchmarks');
for (const pd of projectData) {
if (pd.project.id && pd.projections.length > 0) {
const projection = pd.projections[0]; // Use latest projection
// Extract implementation costs from projection
const implementationCosts = projection.implementation_costs || {
software_licenses: projection.calculations?.total_investment * 0.3 || 0,
development_hours: 1000, // Default estimate
training_costs: projection.calculations?.total_investment * 0.1 || 0,
infrastructure: projection.calculations?.total_investment * 0.2 || 0,
ongoing_monthly: 0
};
const validation = await dutchValidator.validateProjectInputs({
industry: pd.project.industry,
useCases: pd.useCases,
implementationCosts,
timelineMonths: projection.timeline_months || 12
});
projectValidations.set(pd.project.id, validation);
if (validation.validationIssues.length > 0) {
logger.info('Validation adjustments for project', {
projectId: pd.project.id,
adjustmentCount: validation.validationIssues.length
});
}
}
}
// Initialize ML engine
const mlEngine = validatedInput.enable_ml_insights ? new MLComparisonEngine() : null;
// Perform ML analysis if enabled
let mlResults: MLComparisonResult[] = [];
if (mlEngine) {
logger.debug('Running ML comparison analysis');
mlResults = await mlEngine.compareProjects(
projectData
.filter(pd => pd.project.id) // Filter out projects without IDs
.map(pd => ({
id: pd.project.id!,
projection: pd.projections[0], // Use latest projection
useCases: pd.useCases,
industry: pd.project.industry,
companySize: 'medium' // Could be retrieved from actual data
}))
);
}
// Calculate metrics for each project
const projectMetrics = calculateProjectMetrics(projectData, validatedInput.comparison_metrics);
// Generate rankings
const rankings = generateRankings(projectMetrics, mlResults);
// Identify insights
const insights = generateInsights(projectData, projectMetrics, mlResults);
// Generate recommendations including Dutch market insights
const recommendations = generateRecommendations(
projectData,
projectMetrics,
mlResults,
projectValidations
);
// Add Dutch market-specific recommendations
const dutchRecommendations = generateDutchMarketRecommendations(projectValidations);
recommendations.push(...dutchRecommendations);
// Create visualizations if requested
const visualizations = validatedInput.include_visualizations
? generateVisualizations(projectMetrics, mlResults)
: undefined;
// Generate Dutch market summary
const dutchMarketSummary = generateDutchMarketSummary(projectValidations);
// Build the result
const result: ComparisonResult = {
projects: projectData
.filter(pd => pd.project.id) // Filter out projects without IDs
.map((pd, index) => {
const projectId = pd.project.id!;
const mlResult = mlResults.find(ml => ml.projectId === projectId);
const validation = projectValidations.get(projectId);
return {
id: projectId,
name: pd.project.project_name,
client: pd.project.client_name,
industry: pd.project.industry,
status: pd.project.status || 'active',
metrics: projectMetrics.get(projectId)!,
mlInsights: mlResult,
dutchMarketValidation: validation ? {
adjustmentsMade: validation.validationIssues.length,
validationIssues: validation.validationIssues,
marketInsights: validation.marketInsights
} : undefined
};
}),
rankings,
insights,
recommendations,
dutchMarketSummary,
visualization: visualizations
};
logger.info('Comparison completed with Dutch market validation', {
top_performer: insights.bestPerformer.projectId,
ml_predictions_generated: mlResults.length,
dutch_validations_performed: projectValidations.size,
total_adjustments: dutchMarketSummary.totalAdjustments
});
return result;
} catch (error) {
logger.error('Comparison failed', error as Error);
if (error instanceof ValidationError) {
throw error;
}
if (error instanceof DatabaseError) {
throw new DatabaseError(
'Failed to fetch project data. Please ensure all project IDs are valid.',
{ error: (error as Error).message }
);
}
throw new Error(
`Unexpected error in project comparison: ${(error as Error).message}`
);
}
}
/**
* Fetch all project data including projections and use cases
*/
async function fetchProjectData(projectIds: string[]): Promise<Array<{
project: Project;
projections: Projection[];
useCases: UseCase[];
}>> {
const logger = createLogger({ component: 'fetchProjectData' });
// Fetch all data in parallel
const results = await Promise.all(
projectIds.map(async (projectId) => {
// Fetch project
const { data: project, error: projectError } = await mcpDb
.from('projects')
.select('*')
.eq('id', projectId)
.single();
if (projectError || !project) {
throw new DatabaseError(`Project not found: ${projectId}`, {
error: projectError?.message
});
}
// Fetch projections
const { data: projections, error: projectionError } = await mcpDb
.from('projections')
.select('*')
.eq('project_id', projectId)
.order('created_at', { ascending: false });
if (projectionError || !projections || projections.length === 0) {
throw new DatabaseError(`No projections found for project: ${projectId}`, {
error: projectionError?.message
});
}
// Fetch use cases
const { data: useCases, error: useCaseError } = await mcpDb
.from('use_cases')
.select('*')
.eq('project_id', projectId);
if (useCaseError || !useCases) {
throw new DatabaseError(`Failed to fetch use cases for project: ${projectId}`, {
error: useCaseError?.message
});
}
return { project, projections, useCases };
})
);
logger.debug('Fetched project data', { count: results.length });
return results;
}
/**
* Calculate metrics for each project
*/
function calculateProjectMetrics(
projectData: Array<any>,
requestedMetrics: string[]
): Map<string, Record<string, number>> {
const metricsMap = new Map<string, Record<string, number>>();
for (const pd of projectData) {
const projection = pd.projections[0]; // Use latest projection
const metrics: Record<string, number> = {};
// Financial metrics
if (requestedMetrics.includes('roi')) {
metrics.roi = projection.calculations.five_year_roi;
}
if (requestedMetrics.includes('payback_period')) {
metrics.payback_period = projection.calculations.payback_period_months || 999;
}
if (requestedMetrics.includes('npv')) {
metrics.npv = projection.calculations.net_present_value;
}
if (requestedMetrics.includes('total_investment')) {
metrics.total_investment = projection.calculations.total_investment;
}
if (requestedMetrics.includes('monthly_benefit')) {
metrics.monthly_benefit = calculateMonthlyBenefit(pd.useCases);
}
// Risk and complexity metrics
if (requestedMetrics.includes('risk_score')) {
metrics.risk_score = calculateRiskScore(pd.useCases, projection);
}
if (requestedMetrics.includes('implementation_complexity')) {
metrics.implementation_complexity = calculateComplexity(pd.useCases);
}
if (pd.project.id) {
metricsMap.set(pd.project.id, metrics);
}
}
return metricsMap;
}
/**
* Generate rankings based on metrics and ML results
*/
function generateRankings(
projectMetrics: Map<string, Record<string, number>>,
mlResults: MLComparisonResult[]
): any {
const rankings: any = {
byMetric: {},
overall: [],
mlBased: []
};
// Get all projects
const projectIds = Array.from(projectMetrics.keys());
// Rank by each metric
const firstMetrics = projectMetrics.values().next().value;
const metrics = firstMetrics ? Object.keys(firstMetrics) : [];
for (const metric of metrics) {
const sorted = [...projectIds].sort((a, b) => {
const aValue = projectMetrics.get(a)![metric];
const bValue = projectMetrics.get(b)![metric];
// Lower is better for these metrics
if (['payback_period', 'risk_score', 'implementation_complexity'].includes(metric)) {
return aValue - bValue;
}
// Higher is better for others
return bValue - aValue;
});
rankings.byMetric[metric] = sorted;
}
// Calculate overall ranking (weighted average of ranks)
const weights: Record<string, number> = {
roi: 0.3,
payback_period: 0.2,
npv: 0.2,
risk_score: 0.15,
total_investment: 0.15
};
const overallScores = new Map<string, number>();
for (const projectId of projectIds) {
let score = 0;
let totalWeight = 0;
for (const [metric, weight] of Object.entries(weights)) {
if (rankings.byMetric[metric]) {
const rank = rankings.byMetric[metric].indexOf(projectId) + 1;
score += rank * weight;
totalWeight += weight;
}
}
overallScores.set(projectId, score / totalWeight);
}
rankings.overall = [...projectIds].sort((a, b) =>
overallScores.get(a)! - overallScores.get(b)!
);
// ML-based ranking if available
if (mlResults.length > 0) {
rankings.mlBased = mlResults
.sort((a, b) => a.ranking.overall - b.ranking.overall)
.map(r => r.projectId);
}
return rankings;
}
/**
* Generate insights from the comparison
*/
function generateInsights(
projectData: Array<any>,
projectMetrics: Map<string, Record<string, number>>,
mlResults: MLComparisonResult[]
): any {
const insights: any = {
bestPerformer: { projectId: '', reason: '' },
riskiest: { projectId: '', risks: [] },
quickestPayback: { projectId: '', months: 0 }
};
// Find best performer
let bestROI = -Infinity;
for (const [projectId, metrics] of projectMetrics.entries()) {
if (metrics.roi > bestROI) {
bestROI = metrics.roi;
insights.bestPerformer.projectId = projectId;
insights.bestPerformer.reason = `Highest ROI of ${bestROI.toFixed(1)}%`;
}
}
// Find riskiest project
let highestRisk = -Infinity;
let riskiestId = '';
for (const mlResult of mlResults) {
if (mlResult.mlPredictions.riskScore > highestRisk) {
highestRisk = mlResult.mlPredictions.riskScore;
riskiestId = mlResult.projectId;
}
}
if (riskiestId) {
const mlResult = mlResults.find(r => r.projectId === riskiestId)!;
insights.riskiest.projectId = riskiestId;
insights.riskiest.risks = mlResult.mlPredictions.keyRiskFactors
.filter(r => r.impact === 'high')
.map(r => r.factor);
}
// Find quickest payback
let quickestPayback = Infinity;
for (const [projectId, metrics] of projectMetrics.entries()) {
if (metrics.payback_period < quickestPayback) {
quickestPayback = metrics.payback_period;
insights.quickestPayback.projectId = projectId;
insights.quickestPayback.months = quickestPayback;
}
}
// Extract synergies if available
if (mlResults.length > 0) {
const allSynergies: any[] = [];
for (const result of mlResults) {
if (result.mlPredictions.synergies) {
for (const synergy of result.mlPredictions.synergies) {
allSynergies.push({
projects: [result.projectId, synergy.withProject],
type: synergy.type,
value: synergy.estimatedValue
});
}
}
}
// Deduplicate synergies
const uniqueSynergies = allSynergies.filter((s, index) =>
allSynergies.findIndex(s2 =>
s2.projects.sort().join(',') === s.projects.sort().join(',')
) === index
);
if (uniqueSynergies.length > 0) {
insights.synergies = uniqueSynergies;
}
}
return insights;
}
/**
* Generate recommendations based on analysis
*/
function generateRecommendations(
projectData: Array<any>,
projectMetrics: Map<string, Record<string, number>>,
mlResults: MLComparisonResult[],
validations: Map<string, ValidationResult>
): string[] {
const recommendations: string[] = [];
// ML-based recommendations
for (const mlResult of mlResults) {
if (mlResult.recommendation === 'strongly_recommend') {
const project = projectData.find(pd => pd.project.id === mlResult.projectId);
recommendations.push(
`Strongly recommend proceeding with "${project.project.project_name}" - ` +
`ML analysis shows ${(mlResult.mlPredictions.successProbability * 100).toFixed(0)}% success probability`
);
} else if (mlResult.recommendation === 'not_recommended') {
const project = projectData.find(pd => pd.project.id === mlResult.projectId);
recommendations.push(
`Consider deferring "${project.project.project_name}" - ` +
`High risk score (${mlResult.mlPredictions.riskScore.toFixed(1)}/10) and low success probability`
);
}
}
// Validation-based recommendations
for (const [projectId, validation] of validations.entries()) {
const project = projectData.find(pd => pd.project.id === projectId);
if (validation.validationIssues.filter(i => i.severity === 'warning').length > 3) {
recommendations.push(
`Review assumptions for "${project.project.project_name}" - ` +
`Multiple values exceed Dutch market norms and have been adjusted`
);
}
// Check for significant adjustments
const significantAdjustments = validation.validationIssues.filter(
i => i.originalValue / i.adjustedValue > 1.5 || i.adjustedValue / i.originalValue > 1.5
);
if (significantAdjustments.length > 0) {
recommendations.push(
`"${project.project.project_name}" had ${significantAdjustments.length} significant adjustments ` +
`to align with Dutch market realities. Consider reviewing the business case.`
);
}
}
// Portfolio recommendations
const totalInvestment = Array.from(projectMetrics.values())
.reduce((sum, m) => sum + (m.total_investment || 0), 0);
if (totalInvestment > 5000000) {
recommendations.push(
`Total portfolio investment exceeds $5M. Consider phased implementation ` +
`starting with quick-win projects to generate early ROI`
);
}
// Synergy recommendations
const synergies = mlResults
.flatMap(r => r.mlPredictions.synergies || [])
.filter((s, index, self) =>
self.findIndex(s2 => s2.withProject === s.withProject) === index
);
if (synergies.length > 0) {
const totalSynergyValue = synergies.reduce((sum, s) => sum + s.estimatedValue, 0);
recommendations.push(
`Identified synergies worth $${totalSynergyValue.toLocaleString()}. ` +
`Consider bundling related projects for maximum value`
);
}
return recommendations;
}
/**
* Generate visualization data
*/
function generateVisualizations(
projectMetrics: Map<string, Record<string, number>>,
mlResults: MLComparisonResult[]
): any[] {
const visualizations = [];
// ROI vs Risk scatter plot
const scatterData = {
type: 'scatter',
data: {
datasets: [{
label: 'Projects',
data: Array.from(projectMetrics.entries()).map(([projectId, metrics]) => {
const mlResult = mlResults.find(r => r.projectId === projectId);
return {
x: metrics.roi,
y: mlResult?.mlPredictions.riskScore || metrics.risk_score || 5,
label: projectId
};
})
}]
},
options: {
scales: {
x: { title: { text: 'ROI (%)' } },
y: { title: { text: 'Risk Score' } }
}
}
};
visualizations.push(scatterData);
// Timeline comparison
const timelineData = {
type: 'timeline',
data: Array.from(projectMetrics.entries()).map(([projectId, metrics]) => ({
projectId,
paybackPeriod: metrics.payback_period,
totalDuration: 60 // 5 years
}))
};
visualizations.push(timelineData);
return visualizations;
}
/**
* Format benchmark comparison for display
*/
function formatBenchmarkComparison(
projectMetrics: Record<string, number>,
benchmarks: any[]
): any[] {
const comparisons = [];
for (const benchmark of benchmarks) {
const metricName = benchmark.metric.toLowerCase().replace(/\s+/g, '_');
const projectValue = projectMetrics[metricName];
if (projectValue !== undefined) {
// Calculate percentile
const percentile = calculatePercentile(projectValue, benchmark.range);
comparisons.push({
metric: benchmark.metric,
projectValue,
industryAverage: benchmark.recommendedValue,
percentile
});
}
}
return comparisons;
}
/**
* Helper functions
*/
function calculateMonthlyBenefit(useCases: UseCase[]): number {
return useCases.reduce((sum, uc) => {
const volume = uc.current_state.volume_per_month;
const costPerTransaction = uc.current_state.cost_per_transaction;
const automation = uc.future_state.automation_percentage;
return sum + (volume * costPerTransaction * automation);
}, 0);
}
function calculateRiskScore(useCases: UseCase[], projection: Projection): number {
// Simple risk calculation based on complexity and timeline
const avgComplexity = useCases.reduce((sum, uc) =>
sum + (uc.implementation?.complexity_score || 5), 0
) / useCases.length;
const timelineRisk = Math.min(10, projection.timeline_months / 6);
return (avgComplexity + timelineRisk) / 2;
}
function calculateComplexity(useCases: UseCase[]): number {
return useCases.reduce((sum, uc) =>
sum + (uc.implementation?.complexity_score || 5), 0
) / useCases.length;
}
function calculatePercentile(value: number, range: any): number {
if (value <= range.min) return 0;
if (value >= range.max) return 100;
if (value <= range.p25) {
return (value - range.min) / (range.p25 - range.min) * 25;
} else if (value <= range.p75) {
return 25 + (value - range.p25) / (range.p75 - range.p25) * 50;
} else {
return 75 + (value - range.p75) / (range.max - range.p75) * 25;
}
}
/**
* Generate Dutch market-specific recommendations
*/
function generateDutchMarketRecommendations(
validations: Map<string, ValidationResult>
): string[] {
const recommendations: string[] = [];
// Analyze common issues across all projects
const allIssues: string[] = [];
const industries = new Set<string>();
validations.forEach((validation, projectId) => {
validation.validationIssues.forEach(issue => {
if (issue.severity === 'warning' || issue.severity === 'error') {
allIssues.push(issue.field);
}
});
validation.marketInsights.forEach(insight => {
industries.add(insight.metric);
});
});
// Generate recommendations based on common patterns
if (allIssues.filter(i => i.includes('monthly_cost_reduction')).length > 0) {
recommendations.push(
'Consider phased rollouts aligned with Dutch market maturity levels to achieve more realistic savings targets.'
);
}
if (allIssues.filter(i => i.includes('timelineMonths')).length > 0) {
recommendations.push(
'Account for Dutch regulatory requirements (GDPR, works council) which typically add 2-3 months to implementation timelines.'
);
}
// Add industry-specific Dutch recommendations
recommendations.push(
'Leverage Netherlands-specific AI funding opportunities through RVO and regional development agencies.',
'Ensure compliance with the upcoming EU AI Act which will impact Dutch implementations from 2024 onwards.',
'Consider partnering with Dutch knowledge institutions (TNO, universities) for R&D tax benefits (WBSO).'
);
return recommendations;
}
/**
* Generate Dutch market summary from validations
*/
function generateDutchMarketSummary(
validations: Map<string, ValidationResult>
): ComparisonResult['dutchMarketSummary'] {
let totalAdjustments = 0;
const commonIssues = new Map<string, number>();
const allMarketTrends: string[] = [];
const allCitations: Array<{ url: string; title: string }> = [];
// Aggregate data from all validations
validations.forEach(validation => {
totalAdjustments += validation.validationIssues.length;
// Count common issues
validation.validationIssues.forEach(issue => {
const key = issue.reason.split('.')[0]; // Get first sentence as key
commonIssues.set(key, (commonIssues.get(key) || 0) + 1);
});
// Collect market trends
validation.marketInsights.forEach(insight => {
allMarketTrends.push(`${insight.metric}: ${insight.trend}`);
});
// Collect citations
validation.citations.forEach(citation => {
if (!allCitations.some(c => c.url === citation.url)) {
allCitations.push(citation);
}
});
});
// Get top 3 most common issues
const sortedIssues = Array.from(commonIssues.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([issue, _]) => issue);
// Get unique market trends
const uniqueTrends = [...new Set(allMarketTrends)].slice(0, 5);
return {
totalAdjustments,
commonIssues: sortedIssues,
marketTrends: uniqueTrends,
citations: allCitations.slice(0, 10) // Limit to 10 most relevant citations
};
}