mcp-tenant-credit-scorer
Version:
MCP server for tenant credit scoring based on S&P corporate methodology
382 lines (344 loc) • 12.3 kB
JavaScript
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);
}
}