UNPKG

mcp-tenant-credit-scorer

Version:

MCP server for tenant credit scoring based on S&P corporate methodology

382 lines (344 loc) 12.3 kB
export class Validator { constructor() { this.validationRules = { scoreConflicts: [ { name: 'Financial vs Liquidity Mismatch', check: (scores) => { if (scores.financialRisk?.score === 5 && scores.liquidity?.score < 3) { return { issue: 'Strong financial risk but weak liquidity', recommendation: 'Review cash management practices', severity: 'high' }; } if (scores.financialRisk?.score < 3 && scores.liquidity?.score === 5) { return { issue: 'Weak financial risk but strong liquidity', recommendation: 'Check for trapped cash or unusual circumstances', severity: 'medium' }; } return null; } }, { name: 'Industry vs Financial Mismatch', check: (scores) => { if (scores.industryRisk?.score <= 2 && scores.financialRisk?.score === 5) { return { issue: 'High-risk industry with minimal financial risk', recommendation: 'Requires exceptional explanation - verify data accuracy', severity: 'high' }; } if (scores.industryRisk?.score === 5 && scores.financialRisk?.score === 1) { return { issue: 'Low-risk industry with aggressive financial risk', recommendation: 'Review for aggressive growth strategy or distress', severity: 'high' }; } return null; } }, { name: 'Competitive vs Margins Mismatch', check: (scores) => { if (scores.competitivePosition?.score === 5 && scores.competitivePosition?.subfactors?.operatingEfficiency?.score < 3) { return { issue: 'Excellent competitive position but below-average margins', recommendation: 'Revalidate competitive advantages', severity: 'medium' }; } if (scores.competitivePosition?.score === 1 && scores.competitivePosition?.subfactors?.operatingEfficiency?.score > 3) { return { issue: 'Weak competitive position but strong margins', recommendation: 'Check for one-time items or unsustainable pricing', severity: 'medium' }; } return null; } }, { name: 'Management Impact Cap', check: (scores) => { if (scores.management?.score === 1) { return { issue: 'Negative management assessment', recommendation: 'Overall score should be capped at 3.0', severity: 'high', cap: 3.0 }; } if (scores.management?.score === 2) { return { issue: 'Concerning management assessment', recommendation: 'Overall score should be capped at 3.5', severity: 'medium', cap: 3.5 }; } return null; } } ], dataQuality: [ { name: 'Missing Critical Data', check: (scores) => { const missing = []; if (!scores.industryRisk) missing.push('Industry Risk'); if (!scores.competitivePosition) missing.push('Competitive Position'); if (!scores.financialRisk) missing.push('Financial Risk'); if (!scores.liquidity) missing.push('Liquidity'); if (!scores.management) missing.push('Management'); if (missing.length > 0) { return { issue: `Missing scores for: ${missing.join(', ')}`, recommendation: 'Cannot complete assessment without all components', severity: 'critical' }; } return null; } }, { name: 'Score Range Validation', check: (scores) => { const outOfRange = []; Object.entries(scores).forEach(([component, data]) => { if (data?.score && (data.score < 1 || data.score > 5)) { outOfRange.push(`${component}: ${data.score}`); } }); if (outOfRange.length > 0) { return { issue: `Scores out of valid range (1-5): ${outOfRange.join(', ')}`, recommendation: 'Review scoring logic', severity: 'critical' }; } return null; } } ] }; } async validateScores(scores) { const validationResults = { isValid: true, conflicts: [], warnings: [], errors: [], scoreCap: null }; // Run data quality checks first for (const rule of this.validationRules.dataQuality) { const result = rule.check(scores); if (result) { if (result.severity === 'critical') { validationResults.isValid = false; validationResults.errors.push({ rule: rule.name, ...result }); } else { validationResults.warnings.push({ rule: rule.name, ...result }); } } } // Only run conflict checks if data is valid if (validationResults.isValid) { for (const rule of this.validationRules.scoreConflicts) { const result = rule.check(scores); if (result) { validationResults.conflicts.push({ rule: rule.name, ...result }); if (result.cap && (!validationResults.scoreCap || result.cap < validationResults.scoreCap)) { validationResults.scoreCap = result.cap; } } } } // Add consistency score validationResults.consistencyScore = this.calculateConsistencyScore(scores, validationResults); // Add recommendations validationResults.recommendations = this.generateRecommendations(scores, validationResults); return validationResults; } calculateConsistencyScore(scores, validationResults) { let consistencyScore = 100; // Deduct for conflicts validationResults.conflicts.forEach(conflict => { if (conflict.severity === 'high') consistencyScore -= 20; else if (conflict.severity === 'medium') consistencyScore -= 10; else consistencyScore -= 5; }); // Deduct for warnings validationResults.warnings.forEach(warning => { consistencyScore -= 5; }); // Check for logical progression const logicalChecks = [ { check: () => { // Business risk should somewhat align with financial risk const businessRisk = (scores.industryRisk?.score + scores.competitivePosition?.score) / 2; const financialRisk = scores.financialRisk?.score; return Math.abs(businessRisk - financialRisk) <= 2; }, deduction: 10 }, { check: () => { // Liquidity should not be more than 2 points different from financial risk return Math.abs(scores.financialRisk?.score - scores.liquidity?.score) <= 2; }, deduction: 10 } ]; logicalChecks.forEach(({ check, deduction }) => { if (!check()) { consistencyScore -= deduction; } }); return Math.max(0, consistencyScore); } generateRecommendations(scores, validationResults) { const recommendations = []; // High-priority recommendations based on conflicts if (validationResults.conflicts.length > 0) { recommendations.push({ priority: 'high', action: 'Address score conflicts', details: 'Review and reconcile conflicting scores before finalizing assessment' }); } // Score-specific recommendations if (scores.financialRisk?.score <= 2) { recommendations.push({ priority: 'high', action: 'Financial risk mitigation', details: 'Consider debt reduction or refinancing to improve coverage ratios' }); } if (scores.liquidity?.score <= 2) { recommendations.push({ priority: 'high', action: 'Liquidity improvement', details: 'Establish committed credit facilities or improve working capital management' }); } if (scores.competitivePosition?.score <= 2) { recommendations.push({ priority: 'medium', action: 'Competitive position enhancement', details: 'Focus on differentiation, customer diversification, or operational efficiency' }); } if (scores.management?.score <= 3) { recommendations.push({ priority: 'medium', action: 'Governance improvement', details: 'Enhance financial reporting transparency and management stability' }); } // Industry-specific recommendations if (scores.industryRisk?.score <= 2) { recommendations.push({ priority: 'medium', action: 'Industry risk mitigation', details: 'Consider diversification or counter-cyclical strategies' }); } return recommendations; } validateFinancialData(financialData) { const validation = { isValid: true, issues: [] }; // Check for required fields const requiredFields = ['revenue', 'ebitda', 'totalDebt', 'currentAssets', 'currentLiabilities']; const missingFields = requiredFields.filter(field => !financialData[field] || financialData[field] === 0); if (missingFields.length > 0) { validation.issues.push({ type: 'missing_data', fields: missingFields, impact: 'Cannot calculate key ratios' }); } // Check for logical consistency if (financialData.ebitda && financialData.revenue) { const margin = (financialData.ebitda / financialData.revenue) * 100; if (margin > 50) { validation.issues.push({ type: 'unusual_margin', value: margin, message: 'EBITDA margin unusually high - verify calculations' }); } if (margin < -20) { validation.issues.push({ type: 'distress_indicator', value: margin, message: 'Negative EBITDA margin indicates financial distress' }); } } // Check debt levels if (financialData.totalDebt && financialData.ebitda && financialData.ebitda > 0) { const debtToEbitda = financialData.totalDebt / financialData.ebitda; if (debtToEbitda > 10) { validation.issues.push({ type: 'high_leverage', value: debtToEbitda, message: 'Extremely high leverage - verify debt figures' }); } } validation.isValid = validation.issues.filter(i => i.type !== 'unusual_margin').length === 0; return validation; } crossValidateScores(scores, peerData) { const validation = { peerComparison: {}, outliers: [] }; // Compare each component to peer median Object.entries(scores).forEach(([component, data]) => { if (data?.score && peerData[component]) { const peerMedian = peerData[component].median; const difference = data.score - peerMedian; validation.peerComparison[component] = { score: data.score, peerMedian: peerMedian, difference: difference, percentile: this.calculatePercentile(data.score, peerData[component].distribution) }; if (Math.abs(difference) > 1.5) { validation.outliers.push({ component: component, message: `Score significantly ${difference > 0 ? 'above' : 'below'} peer median` }); } } }); return validation; } calculatePercentile(score, distribution) { if (!distribution || distribution.length === 0) return 50; const below = distribution.filter(s => s < score).length; return Math.round((below / distribution.length) * 100); } }