UNPKG

apple-hig-mcp

Version:

High-performance MCP server providing instant access to Apple's Human Interface Guidelines via hybrid static/dynamic content delivery

232 lines • 11 kB
#!/usr/bin/env node /** * Content Validation Script * Validates generated content using Phase 1 quality validation system */ import { promises as fs } from 'fs'; import path from 'path'; import { ContentQualityValidatorService } from '../services/content-quality-validator.service.js'; import { ContentProcessorService } from '../services/content-processor.service.js'; class ContentValidator { qualityValidator; contentProcessor; validationResults = []; constructor() { this.qualityValidator = new ContentQualityValidatorService({ minQualityScore: 0.5, // Reasonable threshold for generated content minConfidence: 0.6, // Decent confidence requirement minContentLength: 200, // Minimum meaningful content maxFallbackRate: 10, // Allow some fallback content minStructureScore: 0.4, // Basic structure requirement minAppleTermsScore: 0.1 // Some Apple terminology expected }); this.contentProcessor = new ContentProcessorService(); } async validateContent() { console.log('šŸ” Starting content validation with Phase 1 quality checks...\n'); const contentDir = 'content'; // Check if content directory exists try { await fs.access(contentDir); } catch { console.log('āš ļø Content directory not found - generating content first...'); console.log(' Run: npm run generate-content'); process.exit(1); } // Validate platforms directory await this.validatePlatformsDirectory(contentDir); // Validate metadata await this.validateMetadata(contentDir); // Generate validation report this.generateValidationReport(); // Exit with appropriate code const hasFailures = this.validationResults.some(r => !r.isValid); if (hasFailures) { console.log('\nāŒ Content validation failed - some files do not meet quality standards'); process.exit(1); } else { console.log('\nāœ… Content validation passed - all files meet quality standards'); } } async validatePlatformsDirectory(contentDir) { const platformsDir = path.join(contentDir, 'platforms'); try { const platforms = await fs.readdir(platformsDir); console.log(`šŸ“ Found ${platforms.length} platform directories: ${platforms.join(', ')}`); for (const platform of platforms) { await this.validatePlatformFiles(platformsDir, platform); } } catch (error) { console.error(`āŒ Error reading platforms directory: ${error}`); throw error; } } async validatePlatformFiles(platformsDir, platform) { const platformDir = path.join(platformsDir, platform); try { const files = await fs.readdir(platformDir); const mdFiles = files.filter(f => f.endsWith('.md')); console.log(` šŸ“„ ${platform}: ${mdFiles.length} markdown files`); for (const file of mdFiles) { await this.validateMarkdownFile(platformDir, file, platform); } } catch (error) { console.error(`āŒ Error reading platform directory ${platform}: ${error}`); throw error; } } async validateMarkdownFile(platformDir, filename, platform) { const filePath = path.join(platformDir, filename); try { const content = await fs.readFile(filePath, 'utf-8'); // Extract front matter and content const { frontMatter, markdownContent } = this.parseFrontMatter(content); // Create mock section for validation const section = { id: path.basename(filename, '.md'), title: frontMatter.title || path.basename(filename, '.md'), url: frontMatter.url || 'https://developer.apple.com/design/human-interface-guidelines/', platform: platform, category: frontMatter.category || 'visual-design', quality: frontMatter.quality }; // Validate content quality const validationResult = await this.qualityValidator.validateContent(markdownContent, section); // Calculate quality score for the content const qualityScore = this.qualityValidator.calculateQualityScore(markdownContent); const quality = { score: qualityScore, confidence: frontMatter.quality?.confidence || 0.7, length: markdownContent.length, structureScore: frontMatter.quality?.structureScore || 0.5, appleTermsScore: frontMatter.quality?.appleTermsScore || 0.2, codeExamplesCount: (markdownContent.match(/```/g) || []).length / 2, imageReferencesCount: (markdownContent.match(/!\[.*?\]/g) || []).length, headingCount: (markdownContent.match(/^#+\s/gm) || []).length, isFallbackContent: frontMatter.quality?.isFallbackContent || false, extractionMethod: frontMatter.quality?.extractionMethod || 'crawlee' }; this.validationResults.push({ file: `${platform}/${filename}`, isValid: validationResult.isValid, quality, issues: validationResult.issues }); const statusEmoji = validationResult.isValid ? 'āœ…' : 'āŒ'; const qualityEmoji = quality.score >= 0.7 ? '🟢' : quality.score >= 0.4 ? '🟔' : 'šŸ”“'; console.log(` ${statusEmoji}${qualityEmoji} ${filename} (quality: ${quality.score.toFixed(3)}, length: ${quality.length})`); if (!validationResult.isValid && validationResult.issues.length > 0) { console.log(` āš ļø Issues: ${validationResult.issues.slice(0, 2).join(', ')}`); } } catch (error) { console.error(`āŒ Error validating file ${filename}: ${error}`); this.validationResults.push({ file: `${platform}/${filename}`, isValid: false, quality: {}, issues: [`File read error: ${error}`] }); } } async validateMetadata(contentDir) { console.log('\nšŸ“Š Validating metadata files...'); const metadataDir = path.join(contentDir, 'metadata'); const requiredFiles = ['search-index.json', 'cross-references.json', 'generation-info.json']; for (const file of requiredFiles) { const filePath = path.join(metadataDir, file); try { await fs.access(filePath); const content = await fs.readFile(filePath, 'utf-8'); JSON.parse(content); // Validate JSON console.log(` āœ… ${file}`); } catch (error) { console.log(` āŒ ${file} - ${error instanceof Error ? error.message : 'Unknown error'}`); } } } parseFrontMatter(content) { const lines = content.split('\n'); if (lines[0] !== '---') { return { frontMatter: {}, markdownContent: content }; } const endIndex = lines.slice(1).findIndex(line => line === '---') + 1; if (endIndex === 0) { return { frontMatter: {}, markdownContent: content }; } const frontMatterText = lines.slice(1, endIndex).join('\n'); const markdownContent = lines.slice(endIndex + 1).join('\n'); try { // Simple YAML-like parsing for basic front matter const frontMatter = {}; frontMatterText.split('\n').forEach(line => { const colonIndex = line.indexOf(':'); if (colonIndex > 0) { const key = line.slice(0, colonIndex).trim(); const value = line.slice(colonIndex + 1).trim(); try { frontMatter[key] = JSON.parse(value); } catch { frontMatter[key] = value.replace(/^["']|["']$/g, ''); } } }); return { frontMatter, markdownContent }; } catch { return { frontMatter: {}, markdownContent: content }; } } generateValidationReport() { console.log('\nšŸ“‹ Validation Summary Report:'); console.log('═'.repeat(50)); const totalFiles = this.validationResults.length; const validFiles = this.validationResults.filter(r => r.isValid).length; const invalidFiles = totalFiles - validFiles; console.log(`šŸ“„ Total files: ${totalFiles}`); console.log(`āœ… Valid files: ${validFiles} (${((validFiles / totalFiles) * 100).toFixed(1)}%)`); console.log(`āŒ Invalid files: ${invalidFiles} (${((invalidFiles / totalFiles) * 100).toFixed(1)}%)`); if (this.validationResults.length > 0) { const averageQuality = this.validationResults .filter(r => r.quality.score !== undefined) .reduce((sum, r) => sum + r.quality.score, 0) / this.validationResults.length; const averageLength = this.validationResults .filter(r => r.quality.length !== undefined) .reduce((sum, r) => sum + r.quality.length, 0) / this.validationResults.length; console.log(`šŸ“Š Average quality score: ${averageQuality.toFixed(3)}`); console.log(`šŸ“ Average content length: ${Math.round(averageLength)} characters`); } // Show worst performing files if (invalidFiles > 0) { console.log('\nšŸ” Files needing attention:'); this.validationResults .filter(r => !r.isValid) .slice(0, 5) .forEach(r => { console.log(` āŒ ${r.file}: ${r.issues.slice(0, 1).join(', ')}`); }); } // Show SLA compliance const stats = this.qualityValidator.getStatistics(); console.log('\nšŸ“ˆ SLA Compliance:'); console.log(`šŸ“Š Extraction success rate: ${stats.extractionSuccessRate.toFixed(1)}% (target: 95%+)`); console.log(`šŸŽÆ Average quality: ${stats.averageQuality.toFixed(3)} (target: 0.6+)`); console.log(`šŸ”„ Fallback usage: ${stats.fallbackUsage}% (target: <10%)`); } } // Run validation if called directly if (import.meta.url === `file://${process.argv[1]}`) { const validator = new ContentValidator(); validator.validateContent().catch(error => { console.error('āŒ Content validation failed:', error); process.exit(1); }); } export { ContentValidator }; //# sourceMappingURL=validate-content.js.map