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.

310 lines (309 loc) • 10.4 kB
"use strict"; /** * @fileoverview Question Domain Entity - Pure Business Logic * @version 1.0.0 * @since 2025-07-29 * @lastUpdated 2025-07-29 * @module Question Domain Entity * @description Pure domain entity representing a Quiz Question in the business domain, * free from framework dependencies and infrastructure concerns. * Contains only business rules and domain logic. * @contributors Claude Code Agent * @dependencies None (framework-agnostic) * @requirements REQ-ARCH-001 (Clean Architecture Domain Layer) * @testCoverage Unit tests for business rules and validation logic */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Question = void 0; const QuestionId_1 = require("../value-objects/QuestionId"); const QuestionAnsweredEvent_1 = require("../events/QuestionAnsweredEvent"); /** * Question Domain Entity * * @description Represents a quiz question in the business domain with all business * rules and invariants. This is a pure domain entity without any * framework dependencies. * * @example * ```typescript * const question = Question.createMultipleChoice({ * questionText: "What is TypeScript?", * options: ["A language", "A framework", "A tool"], * correctAnswer: "A language", * order: 1 * }); * ``` * * @since 2025-07-29 * @author Claude Code Agent * @requirements REQ-ARCH-001 (Clean Architecture Domain Layer) */ class Question { constructor(props) { this.props = props; this._domainEvents = []; this.validate(); } /** * Create a multiple choice question */ static createMultipleChoice(data) { const now = new Date(); const questionId = QuestionId_1.QuestionId.generate(); return new Question({ id: questionId, questionText: data.questionText, options: data.options, correctAnswer: data.correctAnswer, explanation: data.explanation, order: data.order, type: 'multiple_choice', points: data.points || 1, metadata: data.metadata, createdAt: now, updatedAt: now, }); } /** * Create a true/false question */ static createTrueFalse(data) { const now = new Date(); const questionId = QuestionId_1.QuestionId.generate(); return new Question({ id: questionId, questionText: data.questionText, options: ['True', 'False'], correctAnswer: data.correctAnswer ? 'True' : 'False', explanation: data.explanation, order: data.order, type: 'true_false', points: data.points || 1, metadata: data.metadata, createdAt: now, updatedAt: now, }); } /** * Reconstitute Question from persistence layer */ static fromPersistence(data) { return new Question(data); } // Getters (read-only access to properties) get id() { return this.props.id; } get questionText() { return this.props.questionText; } get options() { return [...this.props.options]; } get correctAnswer() { return this.props.correctAnswer; } get explanation() { return this.props.explanation; } get order() { return this.props.order; } get type() { return this.props.type; } get points() { return this.props.points; } get metadata() { return this.props.metadata; } get createdAt() { return this.props.createdAt; } get updatedAt() { return this.props.updatedAt; } /** * Business Rules & Domain Logic */ get isMultipleChoice() { return this.props.type === 'multiple_choice'; } get isTrueFalse() { return this.props.type === 'true_false'; } get hasExplanation() { var _a; return !!((_a = this.props.explanation) === null || _a === void 0 ? void 0 : _a.trim()); } get optionCount() { return this.props.options.length; } get difficulty() { // Business rule: Determine difficulty based on number of options if (this.isTrueFalse) return 'easy'; if (this.optionCount <= 3) return 'medium'; return 'hard'; } /** * Business Operations */ /** * Check if provided answer is correct */ isCorrectAnswer(userAnswer) { if (!(userAnswer === null || userAnswer === void 0 ? void 0 : userAnswer.trim())) { return false; } // Business rule: Case-insensitive comparison with trimmed whitespace const normalizedUserAnswer = userAnswer.trim().toLowerCase(); const normalizedCorrectAnswer = this.props.correctAnswer.trim().toLowerCase(); return normalizedUserAnswer === normalizedCorrectAnswer; } /** * Validate that the correct answer is among the options */ isValidConfiguration() { // For multiple choice and true/false, correct answer must be in options if (this.isMultipleChoice || this.isTrueFalse) { return this.props.options.some(option => option.trim().toLowerCase() === this.props.correctAnswer.trim().toLowerCase()); } // For other types, any non-empty answer is valid return !!this.props.correctAnswer.trim(); } /** * Calculate points based on difficulty and custom rules */ calculatePoints() { // Business rule: Base points with difficulty multiplier const difficultyMultiplier = { easy: 1, medium: 1.5, hard: 2 }; return Math.round(this.props.points * difficultyMultiplier[this.difficulty]); } /** * Record that this question was answered */ recordAnswer(userAnswer, isCorrect, userId) { this.addDomainEvent(new QuestionAnsweredEvent_1.QuestionAnsweredEvent(this.id.value, userAnswer, this.props.correctAnswer, isCorrect, this.calculatePoints(), userId || 'anonymous')); } /** * Update question with new properties */ withUpdates(updates) { const updatedProps = { ...this.props, ...updates, updatedAt: new Date(), }; return new Question(updatedProps); } /** * Change question order */ withNewOrder(newOrder) { if (newOrder < 1) { throw new Error('Question order must be a positive integer'); } return new Question({ ...this.props, order: newOrder, updatedAt: new Date(), }); } /** * Domain Events Management */ addDomainEvent(event) { this._domainEvents.push(event); } getDomainEvents() { return [...this._domainEvents]; } clearDomainEvents() { this._domainEvents = []; } /** * Business Validation Rules */ validate() { var _a, _b; if (!((_a = this.props.questionText) === null || _a === void 0 ? void 0 : _a.trim())) { throw new Error('Question text is required and cannot be empty'); } if (this.props.questionText.length > 1000) { throw new Error('Question text cannot exceed 1000 characters'); } if (!Array.isArray(this.props.options) || this.props.options.length < 1) { throw new Error('Question must have at least one option'); } if (this.props.options.length > 10) { throw new Error('Question cannot have more than 10 options'); } // Validate options are not empty if (this.props.options.some(option => !(option === null || option === void 0 ? void 0 : option.trim()))) { throw new Error('Question options cannot be empty'); } // Check for duplicate options const uniqueOptions = new Set(this.props.options.map(opt => opt.trim().toLowerCase())); if (uniqueOptions.size !== this.props.options.length) { throw new Error('Question cannot have duplicate options'); } if (!((_b = this.props.correctAnswer) === null || _b === void 0 ? void 0 : _b.trim())) { throw new Error('Question must have a correct answer'); } if (this.props.correctAnswer.length > 500) { throw new Error('Correct answer cannot exceed 500 characters'); } if (this.props.order < 1) { throw new Error('Question order must be a positive integer'); } if (this.props.points < 0) { throw new Error('Question points cannot be negative'); } if (this.props.points > 100) { throw new Error('Question points cannot exceed 100'); } // Type-specific validation if (this.isTrueFalse && this.props.options.length !== 2) { throw new Error('True/false questions must have exactly 2 options'); } if (this.isMultipleChoice && this.props.options.length < 2) { throw new Error('Multiple choice questions must have at least 2 options'); } // Validate explanation length if provided if (this.props.explanation && this.props.explanation.length > 2000) { throw new Error('Question explanation cannot exceed 2000 characters'); } // Business rule validation: correct answer must be valid if (!this.isValidConfiguration()) { throw new Error('Correct answer must be one of the provided options for multiple choice questions'); } } /** * Convert to plain object for serialization */ toSnapshot() { return { id: this.props.id, questionText: this.props.questionText, options: [...this.props.options], correctAnswer: this.props.correctAnswer, explanation: this.props.explanation, order: this.props.order, type: this.props.type, points: this.props.points, metadata: this.props.metadata, createdAt: this.props.createdAt, updatedAt: this.props.updatedAt, }; } /** * Equality comparison based on ID */ equals(other) { return this.id.equals(other.id); } } exports.Question = Question;