UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

510 lines (509 loc) • 20.5 kB
"use strict"; /** * @fileoverview Quiz Validation Domain Service * @version 1.0.0 * @since 2025-07-29 * @lastUpdated 2025-07-29 * @module QuizValidationService Domain Service * @description Pure domain service for quiz and question validation business rules, * free from infrastructure concerns and framework dependencies. * @contributors Claude Code Agent * @dependencies Quiz, Question domain entities * @requirements REQ-ARCH-001 (Clean Architecture Domain Layer) * @testCoverage Unit tests for validation rules and business logic */ Object.defineProperty(exports, "__esModule", { value: true }); exports.QuizValidationService = void 0; /** * Quiz Validation Domain Service * * @description Encapsulates all business logic for validating quizzes and questions * according to educational standards, accessibility guidelines, and * business rules. Provides comprehensive validation reporting. * * @example * ```typescript * const validator = new QuizValidationService(); * const result = validator.validateQuiz(quiz, { * strictMode: true, * educationalStandards: true, * accessibilityCheck: true * }); * ``` * * @since 2025-07-29 * @author Claude Code Agent * @requirements REQ-ARCH-001 (Clean Architecture Domain Layer) */ class QuizValidationService { /** * Validate a complete quiz with all questions */ validateQuiz(quiz, options = {}) { const errors = []; const warnings = []; // Basic quiz validation this.validateQuizBasics(quiz, errors, warnings); // Question validation this.validateQuestions(quiz.questions, errors, warnings, options); // Educational standards validation if (options.educationalStandards) { this.validateEducationalStandards(quiz, errors, warnings); } // Accessibility validation if (options.accessibilityCheck) { this.validateAccessibility(quiz, errors, warnings); } // Content quality validation if (options.contentQualityCheck) { this.validateContentQuality(quiz, errors, warnings); } // Strict mode additional checks if (options.strictMode) { this.validateStrictMode(quiz, errors, warnings); } const score = this.calculateQualityScore(quiz, errors, warnings); return { isValid: errors.filter(e => e.severity === 'critical').length === 0, errors, warnings, score, }; } /** * Validate basic quiz properties */ validateQuizBasics(quiz, errors, warnings) { // Title validation if (!quiz.title.trim()) { errors.push({ code: 'QUIZ_TITLE_EMPTY', message: 'Quiz title cannot be empty', field: 'title', severity: 'critical', }); } if (quiz.title.length < 3) { warnings.push({ code: 'QUIZ_TITLE_SHORT', message: 'Quiz title is very short', field: 'title', recommendation: 'Consider a more descriptive title (3+ characters)', }); } if (quiz.title.length > 100) { warnings.push({ code: 'QUIZ_TITLE_LONG', message: 'Quiz title is very long', field: 'title', recommendation: 'Consider shortening the title for better display', }); } // Question count validation if (quiz.questionCount === 0) { errors.push({ code: 'QUIZ_NO_QUESTIONS', message: 'Quiz must have at least one question', field: 'questions', severity: 'critical', }); } if (quiz.questionCount === 1) { warnings.push({ code: 'QUIZ_SINGLE_QUESTION', message: 'Quiz has only one question', field: 'questions', recommendation: 'Consider adding more questions for better assessment', }); } if (quiz.questionCount > 50) { warnings.push({ code: 'QUIZ_TOO_MANY_QUESTIONS', message: 'Quiz has many questions which may cause user fatigue', field: 'questions', recommendation: 'Consider breaking into multiple shorter quizzes', }); } // Time limit validation if (quiz.isTimeLimited && quiz.timeLimit < 1) { errors.push({ code: 'QUIZ_INVALID_TIME_LIMIT', message: 'Time limit must be at least 1 minute', field: 'timeLimit', severity: 'major', }); } if (quiz.isTimeLimited && quiz.timeLimit < quiz.questionCount) { warnings.push({ code: 'QUIZ_TIME_LIMIT_SHORT', message: 'Time limit may be too short (less than 1 minute per question)', field: 'timeLimit', recommendation: 'Consider allowing more time per question', }); } // Category validation if (!quiz.category.trim()) { warnings.push({ code: 'QUIZ_NO_CATEGORY', message: 'Quiz has no category assigned', field: 'category', recommendation: 'Assign a category for better organization', }); } } /** * Validate all questions in the quiz */ validateQuestions(questions, errors, warnings, options) { const questionTexts = new Set(); const questionOrders = new Set(); questions.forEach((question, index) => { // Individual question validation this.validateSingleQuestion(question, errors, warnings, index + 1); // Check for duplicate question text const normalizedText = question.questionText.toLowerCase().trim(); if (questionTexts.has(normalizedText)) { errors.push({ code: 'QUESTION_DUPLICATE', message: `Question ${index + 1} has duplicate text`, field: `questions[${index}].questionText`, severity: 'major', }); } questionTexts.add(normalizedText); // Check for duplicate order numbers if (questionOrders.has(question.order)) { warnings.push({ code: 'QUESTION_DUPLICATE_ORDER', message: `Question ${index + 1} has duplicate order number`, field: `questions[${index}].order`, recommendation: 'Ensure each question has a unique order number', }); } questionOrders.add(question.order); }); this.validateQuestionSequence(questions, errors, warnings); } /** * Validate a single question */ validateSingleQuestion(question, errors, warnings, questionNumber) { const prefix = `Question ${questionNumber}`; // Question text validation if (question.questionText.length < 5) { warnings.push({ code: 'QUESTION_TEXT_SHORT', message: `${prefix}: Question text is very short`, recommendation: 'Consider making the question more specific', }); } if (question.questionText.length > 500) { warnings.push({ code: 'QUESTION_TEXT_LONG', message: `${prefix}: Question text is very long`, recommendation: 'Consider breaking into multiple questions or simplifying', }); } // Options validation if (question.optionCount < 2) { errors.push({ code: 'QUESTION_INSUFFICIENT_OPTIONS', message: `${prefix}: Must have at least 2 options`, severity: 'critical', }); } if (question.isMultipleChoice && question.optionCount < 3) { warnings.push({ code: 'QUESTION_FEW_OPTIONS', message: `${prefix}: Multiple choice questions work better with 3+ options`, recommendation: 'Add more plausible distractors', }); } if (question.optionCount > 6) { warnings.push({ code: 'QUESTION_TOO_MANY_OPTIONS', message: `${prefix}: Too many options may confuse users`, recommendation: 'Consider reducing to 4-5 options', }); } // Validate option lengths const optionLengths = question.options.map(opt => opt.length); const avgLength = optionLengths.reduce((sum, len) => sum + len, 0) / optionLengths.length; const maxLength = Math.max(...optionLengths); const minLength = Math.min(...optionLengths); if (maxLength - minLength > avgLength) { warnings.push({ code: 'QUESTION_UNBALANCED_OPTIONS', message: `${prefix}: Option lengths vary significantly`, recommendation: 'Try to make options similar in length', }); } // Correct answer validation if (!question.isValidConfiguration()) { errors.push({ code: 'QUESTION_INVALID_ANSWER', message: `${prefix}: Correct answer must be one of the provided options`, severity: 'critical', }); } // Points validation if (question.points === 0) { warnings.push({ code: 'QUESTION_ZERO_POINTS', message: `${prefix}: Question awards zero points`, recommendation: 'Consider assigning points for scoring', }); } if (question.points > 10) { warnings.push({ code: 'QUESTION_HIGH_POINTS', message: `${prefix}: Question has unusually high point value`, recommendation: 'Consider if this weighting is intentional', }); } } /** * Validate question sequence and ordering */ validateQuestionSequence(questions, errors, warnings) { const orders = questions.map(q => q.order).sort((a, b) => a - b); // Check for gaps in sequence for (let i = 0; i < orders.length - 1; i++) { if (orders[i + 1] - orders[i] > 1) { warnings.push({ code: 'QUESTION_SEQUENCE_GAP', message: 'Gap in question order sequence', recommendation: 'Consider using consecutive order numbers', }); break; } } // Check if orders start from 1 if (orders[0] !== 1) { warnings.push({ code: 'QUESTION_SEQUENCE_START', message: 'Question ordering does not start from 1', recommendation: 'Consider starting question order from 1', }); } } /** * Validate against educational standards */ validateEducationalStandards(quiz, errors, warnings) { // Bloom's taxonomy level distribution const difficultyDistribution = this.analyzeDifficultyDistribution(quiz.questions); if (difficultyDistribution.easy > 0.8) { warnings.push({ code: 'QUIZ_TOO_EASY', message: 'Quiz may be too easy (80%+ easy questions)', recommendation: 'Include more challenging questions for better assessment', }); } if (difficultyDistribution.hard > 0.6) { warnings.push({ code: 'QUIZ_TOO_HARD', message: 'Quiz may be too difficult (60%+ hard questions)', recommendation: 'Include easier questions to support learning progression', }); } // Check for explanations const questionsWithExplanations = quiz.questions.filter(q => q.hasExplanation).length; const explanationRate = questionsWithExplanations / quiz.questionCount; if (explanationRate < 0.5) { warnings.push({ code: 'QUIZ_FEW_EXPLANATIONS', message: 'Less than 50% of questions have explanations', recommendation: 'Add explanations to help with learning', }); } // Minimum time per question recommendation if (quiz.isTimeLimited) { const timePerQuestion = quiz.timeLimit / quiz.questionCount; if (timePerQuestion < 0.5) { warnings.push({ code: 'QUIZ_INSUFFICIENT_TIME', message: 'Less than 30 seconds per question on average', recommendation: 'Allow more time for thoughtful responses', }); } } } /** * Validate accessibility guidelines */ validateAccessibility(quiz, errors, warnings) { // Check for potentially problematic content quiz.questions.forEach((question, index) => { const text = `${question.questionText} ${question.options.join(' ')}`.toLowerCase(); // Color-only instructions if (this.containsColorOnlyInstructions(text)) { warnings.push({ code: 'ACCESSIBILITY_COLOR_ONLY', message: `Question ${index + 1}: May rely on color alone for meaning`, recommendation: 'Add text labels or patterns alongside color', }); } // Very long questions without breaks if (question.questionText.length > 300 && !question.questionText.includes('.')) { warnings.push({ code: 'ACCESSIBILITY_LONG_TEXT', message: `Question ${index + 1}: Very long text without sentence breaks`, recommendation: 'Break long questions into shorter sentences', }); } // Check for clear language if (this.hasComplexLanguage(question.questionText)) { warnings.push({ code: 'ACCESSIBILITY_COMPLEX_LANGUAGE', message: `Question ${index + 1}: May use complex language`, recommendation: 'Consider simplifying language for broader accessibility', }); } }); } /** * Validate content quality */ validateContentQuality(quiz, errors, warnings) { quiz.questions.forEach((question, index) => { // Check for obvious/trivial questions if (this.isTrivialQuestion(question)) { warnings.push({ code: 'CONTENT_TRIVIAL_QUESTION', message: `Question ${index + 1}: May be too obvious or trivial`, recommendation: 'Consider making the question more thought-provoking', }); } // Check for ambiguous questions if (this.isAmbiguousQuestion(question)) { warnings.push({ code: 'CONTENT_AMBIGUOUS_QUESTION', message: `Question ${index + 1}: May be ambiguous`, recommendation: 'Clarify the question to avoid confusion', }); } // Check for grammar/spelling (basic check) if (this.hasGrammarIssues(question.questionText)) { warnings.push({ code: 'CONTENT_GRAMMAR_ISSUES', message: `Question ${index + 1}: May have grammar or spelling issues`, recommendation: 'Review and proofread the question text', }); } }); } /** * Additional validation for strict mode */ validateStrictMode(quiz, errors, warnings) { // All questions must have explanations const questionsWithoutExplanations = quiz.questions.filter(q => !q.hasExplanation); if (questionsWithoutExplanations.length > 0) { errors.push({ code: 'STRICT_MISSING_EXPLANATIONS', message: 'Strict mode requires all questions to have explanations', severity: 'major', }); } // Minimum number of questions if (quiz.questionCount < 5) { errors.push({ code: 'STRICT_INSUFFICIENT_QUESTIONS', message: 'Strict mode requires at least 5 questions', severity: 'major', }); } // All questions must have unique points const pointValues = quiz.questions.map(q => q.points); const uniquePoints = new Set(pointValues); if (uniquePoints.size === 1 && quiz.questionCount > 3) { warnings.push({ code: 'STRICT_UNIFORM_POINTS', message: 'All questions have the same point value', recommendation: 'Consider varying points based on difficulty', }); } } /** * Calculate overall quality score (0-100) */ calculateQualityScore(quiz, errors, warnings) { let score = 100; // Deduct points for errors errors.forEach(error => { switch (error.severity) { case 'critical': score -= 25; break; case 'major': score -= 15; break; case 'minor': score -= 5; break; } }); // Deduct points for warnings score -= warnings.length * 2; // Bonus points for good practices const explanationRate = quiz.questions.filter(q => q.hasExplanation).length / quiz.questionCount; score += Math.round(explanationRate * 10); if (quiz.questionCount >= 5 && quiz.questionCount <= 20) { score += 5; // Good question count } return Math.max(0, Math.min(100, score)); } /** * Helper methods for content analysis */ analyzeDifficultyDistribution(questions) { const total = questions.length; const counts = { easy: 0, medium: 0, hard: 0 }; questions.forEach(q => { counts[q.difficulty]++; }); return { easy: counts.easy / total, medium: counts.medium / total, hard: counts.hard / total, }; } containsColorOnlyInstructions(text) { const colorWords = ['red', 'green', 'blue', 'yellow', 'orange', 'purple', 'pink']; const colorPattern = new RegExp(`\\b(${colorWords.join('|')})\\b`, 'i'); return colorPattern.test(text) && /choose|select|click/.test(text); } hasComplexLanguage(text) { // Simple heuristic: sentences with more than 20 words or complex words const sentences = text.split(/[.!?]+/); return sentences.some(sentence => { const words = sentence.trim().split(/\s+/); return words.length > 20 || words.some(word => word.length > 12); }); } isTrivialQuestion(question) { const text = question.questionText.toLowerCase(); const trivialPatterns = [ /what is \d+ \+ \d+/, /true or false/i, /what color is/i, /how many letters/i, ]; return trivialPatterns.some(pattern => pattern.test(text)); } isAmbiguousQuestion(question) { const text = question.questionText.toLowerCase(); const ambiguousWords = ['sometimes', 'usually', 'often', 'might', 'could', 'may']; return (ambiguousWords.some(word => text.includes(word)) && !text.includes('always') && !text.includes('never')); } hasGrammarIssues(text) { // Very basic grammar check - in production, you'd use a proper grammar checker return (/\s{2,}/.test(text) || // Multiple spaces /^[a-z]/.test(text) || // Doesn't start with capital !/[.!?]$/.test(text.trim())); // Doesn't end with punctuation } } exports.QuizValidationService = QuizValidationService;