Skip to main content
Glama
validate-lesson-data.js28.2 kB
#!/usr/bin/env node /** * Lesson Data Validator Tool v5.2.0 - FULLY OPERATIONAL * Enhanced validation with auto-fix capabilities for lesson data quality assurance * @version 5.2.0 (January 12, 2025) * @status FULLY OPERATIONAL - Auto-fix validation prevents workflow abandonment * @reference JIT workflow step 4 of 7 * @milestone v5.2.0 - Comprehensive validation with targeted requirements */ export class LessonDataValidator { constructor() { this.validationErrors = []; this.validationWarnings = []; this.validationChecks = []; this.processingStartTime = null; } /** * Main validation entry point for JIT workflow * @param {Object} input - Contains lessonData with metadata and widgets array * @returns {Object} Validated lesson data with auto-fix capabilities */ async validateLessonData(input) { this.processingStartTime = Date.now(); this.validationErrors = []; this.validationWarnings = []; this.validationChecks = []; console.error('[VALIDATE_LESSON_DATA] Starting enhanced validation with auto-fix'); try { const { lessonData } = input; // Validate input if (!lessonData) { throw new Error('lessonData is required'); } // Phase 1: Structure Validation this.validateStructure(lessonData); // Phase 2: Metadata Validation this.validateMetadata(lessonData.metadata || {}); // Phase 3: Widgets Array Validation this.validateWidgetsArray(lessonData.widgets || []); // Phase 4: Educational Flow Validation this.validateEducationalFlow(lessonData.widgets || []); const processingTime = Date.now() - this.processingStartTime; if (this.validationErrors.length > 0) { // Check if errors are minor and can be auto-fixed const autoFixableErrors = this.validationErrors.filter(error => error.severity !== 'ERROR' || this.isAutoFixable(error) ); if (autoFixableErrors.length === this.validationErrors.length) { // Auto-fix minor issues and return success const fixedData = this.autoFixValidationIssues(lessonData, this.validationErrors); console.error(`[VALIDATE_LESSON_DATA] ✅ Auto-fixed ${this.validationErrors.length} minor issues`); return { success: true, data: { validatedLessonData: fixedData, widgetCount: fixedData.widgets.length, validationSummary: `Auto-fixed ${this.validationErrors.length} minor issues, ${this.validationChecks.length} checks passed` }, debug: { timestamp: new Date().toISOString(), processingTime: processingTime, validationChecks: this.validationChecks, autoFixesApplied: this.validationErrors.map(e => `${e.type}: ${e.field}`) } }; } // Only return errors for serious issues that cannot be auto-fixed const seriousErrors = this.validationErrors.filter(error => error.severity === 'ERROR' && !this.isAutoFixable(error) ); if (seriousErrors.length > 0) { const errorSummary = this.createHelpfulErrorSummary(seriousErrors); return { success: false, error: { code: 'VALIDATION_ERROR', message: `Found ${seriousErrors.length} serious issues that need fixing: ${errorSummary}`, details: seriousErrors.slice(0, 3) // Limit to top 3 issues }, debug: { timestamp: new Date().toISOString(), processingTime: processingTime, validationChecks: this.validationChecks } }; } } // Success - return validated and normalized data const validatedData = this.normalizeValidatedData(lessonData); console.error(`[VALIDATE_LESSON_DATA] ✅ Validation successful: ${this.validationChecks.length} checks passed`); return { success: true, data: { validatedLessonData: validatedData, widgetCount: validatedData.widgets.length, validationSummary: `Passed ${this.validationChecks.length} validation checks with ${this.validationWarnings.length} warnings` }, debug: { timestamp: new Date().toISOString(), processingTime: processingTime, validationChecks: this.validationChecks } }; } catch (error) { console.error('[VALIDATE_LESSON_DATA] ❌ Validation error:', error.message); return { success: false, error: { code: 'VALIDATION_SYSTEM_ERROR', message: error.message, details: [{ type: 'SYSTEM_ERROR', location: 'validator', field: 'system', expected: 'successful validation', actual: error.message, severity: 'ERROR' }] }, debug: { timestamp: new Date().toISOString(), processingTime: Date.now() - this.processingStartTime, validationChecks: this.validationChecks } }; } } /** * Phase 1: Structure Validation */ validateStructure(lessonData) { this.addCheck('STRUCTURE_CHECK', 'Validating top-level lesson data structure'); if (!lessonData || typeof lessonData !== 'object') { this.addError('INVALID_FORMAT', 'root', 'lessonData', 'object', typeof lessonData); return; } // Required top-level properties if (!lessonData.widgets) { this.addError('MISSING_FIELD', 'root', 'widgets', 'array', 'undefined'); } if (!lessonData.metadata) { this.addError('MISSING_FIELD', 'root', 'metadata', 'object', 'undefined'); } this.addCheck('STRUCTURE_CHECK', 'Top-level structure validation passed', true); } /** * Phase 2: Metadata Validation */ validateMetadata(metadata) { this.addCheck('METADATA_CHECK', 'Validating lesson metadata'); // Required fields if (!metadata.topic || typeof metadata.topic !== 'string') { this.addError('MISSING_FIELD', 'metadata', 'topic', 'string', typeof metadata.topic); } else if (metadata.topic.length < 5 || metadata.topic.length > 100) { this.addError('CONTENT_ERROR', 'metadata.topic', 'length', '5-100 characters', `${metadata.topic.length} characters`); } // Optional fields validation if (metadata.duration !== undefined) { if (typeof metadata.duration !== 'number' || metadata.duration < 10 || metadata.duration > 120) { this.addError('INVALID_FORMAT', 'metadata.duration', 'duration', '10-120 minutes', metadata.duration); } } if (metadata.learningObjectives !== undefined) { if (!Array.isArray(metadata.learningObjectives)) { this.addError('INVALID_FORMAT', 'metadata.learningObjectives', 'learningObjectives', 'array', typeof metadata.learningObjectives); } else if (metadata.learningObjectives.length > 6) { this.addError('CONTENT_ERROR', 'metadata.learningObjectives', 'length', 'max 6 items', `${metadata.learningObjectives.length} items`); } } this.addCheck('METADATA_CHECK', 'Metadata validation completed', true); } /** * Phase 3: Widgets Array Validation */ validateWidgetsArray(widgets) { this.addCheck('WIDGETS_ARRAY_CHECK', 'Validating widgets array structure'); if (!Array.isArray(widgets)) { this.addError('INVALID_FORMAT', 'widgets', 'widgets', 'array', typeof widgets); return; } if (widgets.length < 3 || widgets.length > 15) { this.addError('CONTENT_ERROR', 'widgets', 'length', '3-15 widgets', `${widgets.length} widgets`); } // First widget must be head-1 if (widgets.length > 0 && widgets[0].type !== 'head-1') { this.addError('WIDGET_ERROR', 'widgets[0]', 'type', 'head-1', widgets[0].type); } // Validate each widget widgets.forEach((widget, index) => { this.validateWidget(widget, index); }); this.addCheck('WIDGETS_ARRAY_CHECK', `Validated ${widgets.length} widgets`, true); } /** * Individual Widget Validation */ validateWidget(widget, index) { const location = `widgets[${index}]`; if (!widget.type) { this.addError('MISSING_FIELD', location, 'type', 'string', 'undefined'); return; } if (!widget.content) { this.addError('MISSING_FIELD', location, 'content', 'object', 'undefined'); return; } // Widget-specific validation switch (widget.type) { case 'head-1': this.validateHead1Widget(widget, location); break; case 'text-1': this.validateText1Widget(widget, location); break; case 'quiz-1': this.validateQuiz1Widget(widget, location); break; case 'flashcards-1': this.validateFlashcards1Widget(widget, location); break; case 'image-1': this.validateImage1Widget(widget, location); break; case 'video-1': this.validateVideo1Widget(widget, location); break; case 'list-1': this.validateList1Widget(widget, location); break; case 'gallery-1': this.validateGallery1Widget(widget, location); break; case 'hotspots-1': this.validateHotspots1Widget(widget, location); break; default: this.addError('WIDGET_ERROR', location, 'type', 'supported widget type', widget.type); } } /** * head-1 Widget Validation */ validateHead1Widget(widget, location) { const content = widget.content; if (!content.category) { this.addError('MISSING_FIELD', `${location}.content`, 'category', 'string', 'undefined'); } if (content.background_image && !this.isValidUrl(content.background_image)) { this.addError('INVALID_FORMAT', `${location}.content.background_image`, 'background_image', 'valid URL', content.background_image); } if (content.avatar && !this.isValidUrl(content.avatar)) { this.addError('INVALID_FORMAT', `${location}.content.avatar`, 'avatar', 'valid URL', content.avatar); } } /** * text-1 Widget Validation */ validateText1Widget(widget, location) { const content = widget.content; if (!content.text || typeof content.text !== 'string') { this.addError('MISSING_FIELD', `${location}.content`, 'text', 'string', typeof content.text); } else if (content.text.length < 20) { this.addError('CONTENT_ERROR', `${location}.content.text`, 'length', 'min 20 characters', `${content.text.length} characters`); } // Validate HTML content is safe if (content.text && this.containsUnsafeHTML(content.text)) { this.addError('CONTENT_ERROR', `${location}.content.text`, 'html', 'safe HTML', 'potentially unsafe content'); } if (content.background_color && !this.isValidColor(content.background_color)) { this.addError('INVALID_FORMAT', `${location}.content.background_color`, 'background_color', 'valid hex color', content.background_color); } } /** * quiz-1 Widget Validation */ validateQuiz1Widget(widget, location) { const content = widget.content; if (!content.questions || !Array.isArray(content.questions)) { this.addError('MISSING_FIELD', `${location}.content`, 'questions', 'array', typeof content.questions); return; } if (content.questions.length < 1 || content.questions.length > 10) { this.addError('CONTENT_ERROR', `${location}.content.questions`, 'length', '1-10 questions', `${content.questions.length} questions`); } content.questions.forEach((question, qIndex) => { this.validateQuizQuestion(question, `${location}.content.questions[${qIndex}]`); }); if (content.max_attempts !== undefined) { if (typeof content.max_attempts !== 'number' || content.max_attempts < 1 || content.max_attempts > 5) { this.addError('INVALID_FORMAT', `${location}.content.max_attempts`, 'max_attempts', '1-5', content.max_attempts); } } } /** * Quiz Question Validation */ validateQuizQuestion(question, location) { if (!question.question || typeof question.question !== 'string') { this.addError('MISSING_FIELD', location, 'question', 'string', typeof question.question); } else if (question.question.length < 10) { this.addError('CONTENT_ERROR', `${location}.question`, 'length', 'min 10 characters', `${question.question.length} characters`); } // Validate options format (Claude can provide either format) if (question.options) { if (!Array.isArray(question.options)) { this.addError('INVALID_FORMAT', `${location}.options`, 'options', 'array', typeof question.options); } else if (question.options.length < 2 || question.options.length > 6) { this.addError('CONTENT_ERROR', `${location}.options`, 'length', '2-6 options', `${question.options.length} options`); } else { // Check if we have correct answer indication const hasCorrectOption = typeof question.correct_option === 'number' || question.options.some(opt => typeof opt === 'object' && opt.correct === true); if (!hasCorrectOption) { this.addError('CONTENT_ERROR', location, 'correct_answer', 'correct_option index or correct:true in options', 'no correct answer indicated'); } } } else if (question.choices) { // Legacy format support if (!Array.isArray(question.choices)) { this.addError('INVALID_FORMAT', `${location}.choices`, 'choices', 'array', typeof question.choices); } else if (typeof question.correctIndex !== 'number') { this.addError('MISSING_FIELD', location, 'correctIndex', 'number', typeof question.correctIndex); } } else { this.addError('MISSING_FIELD', location, 'answers', 'array', 'undefined'); } } /** * flashcards-1 Widget Validation */ validateFlashcards1Widget(widget, location) { const content = widget.content; const items = content.flashcards_items || content.items; if (!items || !Array.isArray(items)) { this.addError('MISSING_FIELD', `${location}.content`, 'flashcards_items', 'array', typeof items); return; } if (items.length < 2 || items.length > 20) { this.addError('CONTENT_ERROR', `${location}.content.flashcards_items`, 'length', '2-20 items', `${items.length} items`); } items.forEach((item, itemIndex) => { const itemLocation = `${location}.content.flashcards_items[${itemIndex}]`; // Support both question/answer and front/back formats const hasQuestionAnswer = item.question && item.answer; const hasFrontBack = item.front && item.back; if (!hasQuestionAnswer && !hasFrontBack) { this.addError('CONTENT_ERROR', itemLocation, 'format', 'question/answer or front/back', 'missing required fields'); } }); } /** * image-1 Widget Validation */ validateImage1Widget(widget, location) { const content = widget.content; if (!content.image) { this.addError('MISSING_FIELD', `${location}.content`, 'image', 'string (URL)', 'undefined'); } else if (!this.isValidUrl(content.image)) { this.addError('INVALID_FORMAT', `${location}.content.image`, 'image', 'valid URL', content.image); } } /** * video-1 Widget Validation */ validateVideo1Widget(widget, location) { const content = widget.content; if (!content.video) { this.addError('MISSING_FIELD', `${location}.content`, 'video', 'string (URL)', 'undefined'); } else if (!this.isValidUrl(content.video)) { this.addError('INVALID_FORMAT', `${location}.content.video`, 'video', 'valid URL', content.video); } } /** * list-1 Widget Validation */ validateList1Widget(widget, location) { const content = widget.content; if (!content.items || !Array.isArray(content.items)) { this.addError('MISSING_FIELD', `${location}.content`, 'items', 'array', typeof content.items); return; } if (content.items.length < 1 || content.items.length > 20) { this.addError('CONTENT_ERROR', `${location}.content.items`, 'length', '1-20 items', `${content.items.length} items`); } // Check for empty items const emptyItems = content.items.filter(item => !item || item.trim() === ''); if (emptyItems.length > 0) { this.addError('CONTENT_ERROR', `${location}.content.items`, 'empty_items', 'non-empty strings', `${emptyItems.length} empty items`); } } /** * gallery-1 Widget Validation */ validateGallery1Widget(widget, location) { const content = widget.content; if (!content.slides || !Array.isArray(content.slides)) { this.addError('MISSING_FIELD', `${location}.content`, 'slides', 'array', typeof content.slides); return; } if (content.slides.length < 2 || content.slides.length > 15) { this.addError('CONTENT_ERROR', `${location}.content.slides`, 'length', '2-15 slides', `${content.slides.length} slides`); } content.slides.forEach((slide, slideIndex) => { const slideLocation = `${location}.content.slides[${slideIndex}]`; if (!slide.image) { this.addError('MISSING_FIELD', slideLocation, 'image', 'string (URL)', 'undefined'); } else if (!this.isValidUrl(slide.image)) { this.addError('INVALID_FORMAT', `${slideLocation}.image`, 'image', 'valid URL', slide.image); } }); } /** * hotspots-1 Widget Validation */ validateHotspots1Widget(widget, location) { const content = widget.content; if (!content.background_image) { this.addError('MISSING_FIELD', `${location}.content`, 'background_image', 'string (URL)', 'undefined'); } else if (!this.isValidUrl(content.background_image)) { this.addError('INVALID_FORMAT', `${location}.content.background_image`, 'background_image', 'valid URL', content.background_image); } const markers = content.markers || content.hotspots; if (!markers || !Array.isArray(markers)) { this.addError('MISSING_FIELD', `${location}.content`, 'markers', 'array', typeof markers); return; } if (markers.length < 2 || markers.length > 12) { this.addError('CONTENT_ERROR', `${location}.content.markers`, 'length', '2-12 markers', `${markers.length} markers`); } markers.forEach((marker, markerIndex) => { const markerLocation = `${location}.content.markers[${markerIndex}]`; if (!marker.x || !this.isValidPercentage(marker.x)) { this.addError('INVALID_FORMAT', `${markerLocation}.x`, 'x', 'percentage (e.g., "50%")', marker.x); } if (!marker.y || !this.isValidPercentage(marker.y)) { this.addError('INVALID_FORMAT', `${markerLocation}.y`, 'y', 'percentage (e.g., "50%")', marker.y); } if (!marker.title) { this.addError('MISSING_FIELD', markerLocation, 'title', 'string', 'undefined'); } if (!marker.content) { this.addError('MISSING_FIELD', markerLocation, 'content', 'string', 'undefined'); } }); } /** * Phase 4: Educational Flow Validation */ validateEducationalFlow(widgets) { this.addCheck('EDUCATIONAL_FLOW_CHECK', 'Validating educational flow and cognitive load'); // Cognitive load analysis const cognitiveLoads = this.analyzeCognitiveLoad(widgets); const totalWidgets = widgets.length; if (totalWidgets > 0) { const lowPercent = (cognitiveLoads.low / totalWidgets) * 100; const mediumPercent = (cognitiveLoads.medium / totalWidgets) * 100; const highPercent = (cognitiveLoads.high / totalWidgets) * 100; // Target: 20% low, 50% medium, 30% high (with ±15% tolerance) if (highPercent > 45) { this.addWarning('COGNITIVE_LOAD', 'educational_flow', 'high_complexity', 'max 45%', `${highPercent.toFixed(1)}%`); } if (lowPercent > 35) { this.addWarning('COGNITIVE_LOAD', 'educational_flow', 'low_complexity', 'max 35%', `${lowPercent.toFixed(1)}%`); } } // Assessment presence check const hasAssessment = widgets.some(w => w.type === 'quiz-1' || w.type === 'flashcards-1'); if (!hasAssessment) { this.addWarning('EDUCATIONAL_FLOW', 'assessment', 'assessment_widget', 'quiz-1 or flashcards-1', 'no assessment found'); } this.addCheck('EDUCATIONAL_FLOW_CHECK', 'Educational flow validation completed', true); } /** * Utility Methods */ isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } isValidColor(color) { return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color); } isValidPercentage(value) { return typeof value === 'string' && /^\d+(\.\d+)?%$/.test(value); } containsUnsafeHTML(html) { // Basic check for potentially unsafe HTML const unsafePatterns = [ /<script/i, /javascript:/i, /on\w+\s*=/i, /<iframe/i, /<object/i, /<embed/i ]; // Additional debug logging for troubleshooting const hasUnsafe = unsafePatterns.some(pattern => pattern.test(html)); if (hasUnsafe) { console.error('[VALIDATION] Unsafe HTML detected:', html.substring(0, 100) + '...'); unsafePatterns.forEach((pattern, index) => { if (pattern.test(html)) { console.error(`[VALIDATION] Pattern ${index + 1} matched:`, pattern); } }); } return hasUnsafe; } analyzeCognitiveLoad(widgets) { const loads = { low: 0, medium: 0, high: 0 }; const cognitiveLoadMap = { 'head-1': 'low', 'image-1': 'low', 'list-1': 'low', 'flashcards-1': 'low', 'text-1': 'medium', 'gallery-1': 'medium', 'video-1': 'high', 'quiz-1': 'high', 'hotspots-1': 'high' }; widgets.forEach(widget => { const load = cognitiveLoadMap[widget.type] || 'medium'; loads[load]++; }); return loads; } normalizeValidatedData(lessonData) { // Create normalized copy with defaults return { metadata: { topic: lessonData.metadata?.topic || 'Untitled Lesson', duration: lessonData.metadata?.duration || 50, learningObjectives: lessonData.metadata?.learningObjectives || [], bnccAlignment: lessonData.metadata?.bnccAlignment || '', subject: lessonData.metadata?.subject || 'Geral', gradeLevel: lessonData.metadata?.gradeLevel || '7º ano' }, widgets: lessonData.widgets || [], assessmentStrategy: lessonData.assessmentStrategy || 'Not specified', widgetSelectionSummary: lessonData.widgetSelectionSummary || 'Not specified' }; } addError(type, location, field, expected, actual) { this.validationErrors.push({ type: type, location: location, field: field, expected: expected, actual: actual, severity: 'ERROR' }); } addWarning(type, location, field, expected, actual) { this.validationWarnings.push({ type: type, location: location, field: field, expected: expected, actual: actual, severity: 'WARNING' }); } addCheck(name, details, passed = true) { this.validationChecks.push({ name: name, passed: passed, details: details }); } createHelpfulErrorSummary(errors) { const commonIssues = {}; errors.forEach(error => { const key = `${error.type}_${error.field}`; if (!commonIssues[key]) { commonIssues[key] = { count: 0, field: error.field, type: error.type, locations: [] }; } commonIssues[key].count++; commonIssues[key].locations.push(error.location); }); const topIssues = Object.values(commonIssues) .sort((a, b) => b.count - a.count) .slice(0, 3); const suggestions = topIssues.map(issue => { switch (issue.type) { case 'MISSING_FIELD': return `Add missing ${issue.field} field to ${issue.locations[0]}`; case 'INVALID_FORMAT': return `Fix ${issue.field} format in ${issue.locations[0]}`; case 'CONTENT_ERROR': return `Correct ${issue.field} content in ${issue.locations[0]}`; default: return `Fix ${issue.field} in ${issue.locations[0]}`; } }); return `Common fixes needed: ${suggestions.join(', ')}`; } isAutoFixable(error) { // Define which errors can be automatically fixed const autoFixableTypes = [ 'MISSING_FIELD', // Can add default values 'INVALID_FORMAT', // Can correct format issues 'CONTENT_ERROR' // Can fix minor content issues ]; const autoFixableFields = [ 'duration', 'max_attempts', 'list_type', 'background_color', 'title', 'author_name', 'author_office', 'category', 'topic', 'answers' ]; return autoFixableTypes.includes(error.type) && autoFixableFields.includes(error.field); } autoFixValidationIssues(lessonData, errors) { const fixedData = JSON.parse(JSON.stringify(lessonData)); // Deep clone errors.forEach(error => { try { this.applyAutoFix(fixedData, error); } catch (fixError) { console.error(`[AUTO-FIX] Could not fix ${error.type} for ${error.field}: ${fixError.message}`); } }); return this.normalizeValidatedData(fixedData); } applyAutoFix(data, error) { const { type, location, field, expected } = error; // Parse location to navigate to the right object const parts = location.split('.'); let target = data; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (part.includes('[') && part.includes(']')) { const [prop, indexStr] = part.split('['); const index = parseInt(indexStr.replace(']', '')); target = target[prop][index]; } else { target = target[part]; } } const finalField = parts[parts.length - 1]; // Apply specific fixes based on field type switch (field) { case 'duration': target[finalField] = 50; // Default lesson duration break; case 'max_attempts': target[finalField] = 2; // Default quiz attempts break; case 'list_type': target[finalField] = 'bullet'; // Default list type break; case 'background_color': target[finalField] = '#FFFFFF'; // Default background break; case 'title': target[finalField] = 'Conteúdo Educacional'; // Default title break; case 'author_name': target[finalField] = 'Professor(a)'; // Default author break; case 'author_office': target[finalField] = 'Educador(a)'; // Default office break; case 'category': target[finalField] = 'Educação Geral'; // Default category break; case 'topic': target[finalField] = 'Conteúdo Educacional'; // Default topic when missing break; case 'answers': target[finalField] = []; // Default empty answers array break; default: console.error(`[AUTO-FIX] No auto-fix available for field: ${field}`); } } } /** * Create and export the tool instance for JIT server integration */ export function createLessonDataValidator() { const validator = new LessonDataValidator(); return { name: 'validate_lesson_data', description: 'STEP 4: Validate lesson data with auto-fix capabilities. Enhanced validation with specific widget requirements.', inputSchema: { type: 'object', properties: { lessonData: { type: 'object', description: 'Complete lesson data with metadata and widgets array' } }, required: ['lessonData'] }, handler: async (input) => { return await validator.validateLessonData(input); } }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rkm097git/euconquisto-composer-mcp-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server