UNPKG

agile-planner-mcp-server

Version:

Serveur MCP pour la génération d'artefacts agiles (backlogs, features, user stories) avec IA - compatible Windsurf, Claude et Cursor

232 lines (199 loc) 7.24 kB
// Module de validation des backlogs - Stratégie spécifique const chalk = require('chalk'); const { SchemaValidatorStrategy } = require('./schema-validator-strategy'); const { EpicValidator } = require('./epic-validator'); const { UserStoryValidator } = require('./user-story-validator'); // Classe spécialisée pour la validation des backlogs (pattern Strategy) class BacklogValidator extends SchemaValidatorStrategy { constructor() { super(); this.epicValidator = new EpicValidator(); this.userStoryValidator = new UserStoryValidator(); this.schema = this.createBacklogSchema(); } createBacklogSchema() { return { required: ['projectName', 'epics'], properties: { projectName: { type: 'string' }, description: { type: 'string' }, epics: { type: 'array', items: this.epicValidator.schema }, mvp: { type: 'array' }, iterations: { type: 'array' } }, additionalProperties: false }; } // Normalise un backlog pour le format moderne 'epics' (pluriel) normalizeBacklog(backlog) { if (!backlog || typeof backlog !== 'object') { return null; } // Rejeter tout backlog qui utilise l'ancien format 'epic' (singulier) if (backlog.epic && !backlog.epics) { // Ne pas tenter de convertir le format singulier // Rejeter complètement l'ancien format 'epic' console.error(chalk.yellow('⚠️ Format obsolète détecté (epic singulier) - Rejeté')); return { ...backlog, epics: null // Forcer l'échec de validation }; } // Retourner le backlog tel quel (doit déjà avoir epics) return backlog; } /** * Vérifie les champs obligatoires du backlog * @param {Object} backlog - Backlog à valider * @returns {Array} - Erreurs détectées lors de la validation * @private */ _validateRequiredFields(backlog) { const errors = []; if (!backlog || typeof backlog !== 'object') { errors.push('Backlog invalide'); } if (!backlog.projectName) { errors.push('projectName requis'); } if (!Array.isArray(backlog.epics) || backlog.epics.length === 0) { errors.push('epics requis et doit être un tableau non vide'); } return errors; } /** * Vérifie les sections optionnelles du backlog (MVP et itérations) * @param {Object} backlog - Backlog à valider * @returns {Array} - Erreurs détectées lors de la validation * @private */ _validateOptionalSections(backlog) { const errors = []; // Validation du format de MVP et itérations (si présents) if (backlog.mvp && !Array.isArray(backlog.mvp)) { errors.push('mvp doit être un tableau'); } if (backlog.iterations && !Array.isArray(backlog.iterations)) { errors.push('iterations doit être un tableau'); } return errors; } /** * Valide un backlog complet * @param {Object} backlog - Backlog à valider * @returns {Object} - Résultat de validation {valid: boolean, errors?: Array} */ validate(backlog) { // Extraire les données si elles sont dans un wrapper const extractedBacklog = this.extractData(backlog); if (!extractedBacklog) { return { valid: false, errors: ['Données du backlog invalides ou manquantes'] }; } // Normaliser le backlog pour n'accepter que le format 'epics' (pluriel) const normalizedBacklog = this.normalizeBacklog(extractedBacklog); console.error(`Validation du backlog ${normalizedBacklog?.projectName || 'sans nom'}`); // Vérification de la structure de base avec le schéma const baseResult = this.validateAgainstSchema(normalizedBacklog, this.schema); if (!baseResult.valid) { return baseResult; } // Collecte de toutes les erreurs détectées const errors = [ ...this._validateRequiredFields(normalizedBacklog), ...this._validateOptionalSections(normalizedBacklog) ]; // Validation des epics if (normalizedBacklog.epics && Array.isArray(normalizedBacklog.epics)) { const epicErrors = normalizedBacklog.epics.map(this.validateEpic).filter(Boolean); if (epicErrors.length > 0) { errors.push(...epicErrors); } } // Construction du résultat final return errors.length > 0 ? { valid: false, errors } : { valid: true }; } validateEpic(epic) { if (!epic.id || !epic.title) { return 'Chaque epic doit avoir un id et un title'; } if (epic.features && !Array.isArray(epic.features)) { return 'features doit être un tableau'; } return null; } validateMvp(backlog, errors) { if (backlog.mvp) { if (!Array.isArray(backlog.mvp)) { errors.push('La section MVP doit être un tableau'); } else { // Vérification de chaque user story du MVP for (const story of backlog.mvp) { if (!story.id || !story.title) { errors.push('Une user story du MVP doit avoir un ID et un titre'); break; } } } } } _validateIteration(iteration) { if (!iteration.name) { return 'name est requis à /iterations'; } if (!iteration.stories || !Array.isArray(iteration.stories)) { return 'stories doit être un tableau à /iterations'; } // Vérification rapide des stories for (const story of iteration.stories) { if (!story.id || !story.title) { return `Une user story de l'itération ${iteration.name} doit avoir un ID et un titre`; } } return null; } validateIterations(backlog, errors) { if (!backlog.iterations) { return; // Aucune itération à valider } if (!Array.isArray(backlog.iterations)) { errors.push('La section iterations doit être un tableau'); return; } // Vérification de chaque itération for (const iteration of backlog.iterations) { const errorMsg = this._validateIteration(iteration); if (errorMsg) { errors.push(errorMsg); } } } _logValidationErrors(errors) { console.error(chalk.red('⚠️ Backlog invalide:')); errors.forEach(error => { console.error(chalk.yellow(` - ${error}`)); }); } validateBacklog(backlog) { console.error(chalk.blue('🔍 Validation du backlog...')); try { const result = this.validate(backlog); // Afficher le résultat de validation if (result.valid) { console.error(chalk.green('✓ Backlog valide')); } else { this._logValidationErrors(result.errors); } return result; } catch (error) { console.error(chalk.red('❌ Erreur lors de la validation du backlog:'), error); return { valid: false, error: `Erreur inattendue: ${error.message || 'Erreur inconnue'}` }; } } } module.exports = { BacklogValidator };