UNPKG

tarot-mcp-server

Version:

Model Context Protocol server for Rider-Waite tarot card readings

231 lines (230 loc) 9.73 kB
/** * Analytics and insights for the tarot card database */ export class TarotCardAnalytics { cards; constructor(cards) { this.cards = cards; } /** * Generate comprehensive analytics report */ generateReport() { return { overview: this.getDatabaseOverview(), dataQuality: this.getDataQualityReport(), contentAnalysis: this.getContentAnalysis(), recommendations: this.generateRecommendations() }; } /** * Get database overview statistics */ getDatabaseOverview() { const arcanaDistribution = {}; const suitDistribution = {}; const elementDistribution = {}; for (const card of this.cards) { // Count arcana arcanaDistribution[card.arcana] = (arcanaDistribution[card.arcana] || 0) + 1; // Count suits if (card.suit) { suitDistribution[card.suit] = (suitDistribution[card.suit] || 0) + 1; } // Count elements if (card.element) { elementDistribution[card.element] = (elementDistribution[card.element] || 0) + 1; } } return { totalCards: this.cards.length, completionRate: this.calculateCompletionRate(), arcanaDistribution, suitDistribution, elementDistribution }; } /** * Analyze data quality and completeness */ getDataQualityReport() { const requiredFields = ['keywords', 'meanings', 'symbolism', 'astrology', 'numerology', 'description']; const incompleteCards = []; const missingFields = {}; let totalKeywords = 0; let totalSymbols = 0; for (const card of this.cards) { const missing = []; // Check required fields for (const field of requiredFields) { if (field === 'keywords') { if (!card.keywords?.upright?.length || !card.keywords?.reversed?.length) { missing.push('keywords'); } else { totalKeywords += card.keywords.upright.length + card.keywords.reversed.length; } } else if (field === 'meanings') { if (!card.meanings?.upright || !card.meanings?.reversed) { missing.push('meanings'); } } else if (field === 'symbolism') { if (!card.symbolism?.length) { missing.push('symbolism'); } else { totalSymbols += card.symbolism.length; } } else { const value = card[field]; if (!value || value === '(placeholder)') { missing.push(field); } } } if (missing.length > 0) { incompleteCards.push(card.name || card.id); missingFields[card.name || card.id] = missing; } } return { completeCards: this.cards.length - incompleteCards.length, incompleteCards, averageKeywordsPerCard: totalKeywords / this.cards.length, averageSymbolsPerCard: totalSymbols / this.cards.length, missingFields }; } /** * Analyze card content and themes */ getContentAnalysis() { const keywordCounts = {}; const uprightKeywords = {}; const reversedKeywords = {}; const themes = {}; let totalDescriptionLength = 0; let totalMeaningLength = 0; let longestDesc = { card: '', length: 0 }; let shortestDesc = { card: '', length: Infinity }; for (const card of this.cards) { // Count keywords for (const keyword of card.keywords.upright) { keywordCounts[keyword] = (keywordCounts[keyword] || 0) + 1; uprightKeywords[keyword] = (uprightKeywords[keyword] || 0) + 1; } for (const keyword of card.keywords.reversed) { keywordCounts[keyword] = (keywordCounts[keyword] || 0) + 1; reversedKeywords[keyword] = (reversedKeywords[keyword] || 0) + 1; } // Analyze themes this.analyzeThemes(card, themes); // Length statistics const descLength = card.description.length; totalDescriptionLength += descLength; if (descLength > longestDesc.length) { longestDesc = { card: card.name, length: descLength }; } if (descLength < shortestDesc.length) { shortestDesc = { card: card.name, length: descLength }; } // Calculate average meaning length const meanings = Object.values(card.meanings.upright); const avgMeaningLength = meanings.reduce((sum, meaning) => sum + meaning.length, 0) / meanings.length; totalMeaningLength += avgMeaningLength; } const totalKeywords = Object.values(keywordCounts).reduce((sum, count) => sum + count, 0); return { mostCommonKeywords: Object.entries(keywordCounts) .map(([keyword, count]) => ({ keyword, count, percentage: (count / totalKeywords) * 100 })) .sort((a, b) => b.count - a.count) .slice(0, 20), keywordsByOrientation: { upright: Object.entries(uprightKeywords) .map(([keyword, count]) => ({ keyword, count })) .sort((a, b) => b.count - a.count) .slice(0, 10), reversed: Object.entries(reversedKeywords) .map(([keyword, count]) => ({ keyword, count })) .sort((a, b) => b.count - a.count) .slice(0, 10) }, thematicAnalysis: themes, lengthStatistics: { averageDescriptionLength: totalDescriptionLength / this.cards.length, averageMeaningLength: totalMeaningLength / this.cards.length, longestDescription: longestDesc, shortestDescription: shortestDesc.length === Infinity ? { card: 'None', length: 0 } : shortestDesc } }; } /** * Generate recommendations for database improvement */ generateRecommendations() { const recommendations = []; const qualityReport = this.getDataQualityReport(); const contentAnalysis = this.getContentAnalysis(); // Data quality recommendations if (qualityReport.incompleteCards.length > 0) { recommendations.push(`Complete data for ${qualityReport.incompleteCards.length} incomplete cards`); } if (qualityReport.averageKeywordsPerCard < 8) { recommendations.push('Consider adding more keywords per card for better searchability'); } if (qualityReport.averageSymbolsPerCard < 4) { recommendations.push('Add more symbolic interpretations to enhance card meanings'); } // Content recommendations if (contentAnalysis.lengthStatistics.averageDescriptionLength < 100) { recommendations.push('Consider expanding card descriptions for more detailed imagery'); } // Balance recommendations const overview = this.getDatabaseOverview(); if (overview.completionRate < 100) { recommendations.push(`Database is ${overview.completionRate.toFixed(1)}% complete - finish remaining cards`); } if (recommendations.length === 0) { recommendations.push('Database is in excellent condition - no improvements needed!'); } return recommendations; } calculateCompletionRate() { const expectedTotal = 78; // Standard tarot deck return (this.cards.length / expectedTotal) * 100; } analyzeThemes(card, themes) { const themeKeywords = { 'love': ['love', 'romance', 'relationship', 'partnership', 'marriage', 'attraction'], 'career': ['career', 'work', 'job', 'profession', 'business', 'success'], 'money': ['money', 'wealth', 'financial', 'prosperity', 'abundance', 'material'], 'health': ['health', 'healing', 'wellness', 'recovery', 'vitality', 'energy'], 'spirituality': ['spiritual', 'divine', 'sacred', 'enlightenment', 'wisdom', 'intuition'], 'conflict': ['conflict', 'struggle', 'challenge', 'difficulty', 'opposition', 'tension'], 'growth': ['growth', 'development', 'progress', 'advancement', 'evolution', 'learning'], 'creativity': ['creativity', 'artistic', 'inspiration', 'imagination', 'expression', 'innovation'] }; const allText = [ ...card.keywords.upright, ...card.keywords.reversed, card.description, ...Object.values(card.meanings.upright), ...Object.values(card.meanings.reversed) ].join(' ').toLowerCase(); for (const [theme, keywords] of Object.entries(themeKeywords)) { for (const keyword of keywords) { if (allText.includes(keyword)) { themes[theme] = (themes[theme] || 0) + 1; break; // Count each theme only once per card } } } } }