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

275 lines (248 loc) 9.89 kB
/** * Module de formatage des user stories * @module markdown/story-formatter */ const path = require('path'); const fs = require('fs-extra'); const chalk = require('chalk'); // NOTE: markdownInstructions est importé mais non utilisé const { createSlug, handleMarkdownError /*, markdownInstructions */ } = require('./utils'); /** * Classe Builder pour construire un document markdown de user story */ class UserStoryBuilder { /** * Initialise le builder avec les données de base de la user story * @param {Object} userStory - Données de la user story */ constructor(userStory) { this.id = userStory.id || ''; this.title = userStory.title || ''; this.description = userStory.description || ''; this.acceptanceCriteria = userStory.acceptance_criteria || []; this.tasks = userStory.tasks || []; this.priority = userStory.priority || ''; this.dependencies = userStory.dependencies || []; this.lines = []; } /** * Ajoute l'en-tête de la user story * @returns {UserStoryBuilder} - L'instance courante pour chaînage */ withHeader() { this.lines.push(`# User Story ${this.id}: ${this.title}`); this.lines.push(''); return this; } /** * Ajoute la section description * @returns {UserStoryBuilder} - L'instance courante pour chaînage */ withDescription() { this.lines.push('## Description'); this.lines.push(`- [ ] ${this.description}`); this.lines.push(''); return this; } /** * Ajoute la section des critères d'acceptation * @returns {UserStoryBuilder} - L'instance courante pour chaînage */ withAcceptanceCriteria() { this.lines.push('### Acceptance Criteria'); for (const criteria of this.acceptanceCriteria) { this.lines.push(`- [ ] ${criteria}`); } this.lines.push(''); return this; } /** * Ajoute la section des tâches techniques * @returns {UserStoryBuilder} - L'instance courante pour chaînage */ withTasks() { this.lines.push('### Technical Tasks'); for (const task of this.tasks) { this.lines.push(`- [ ] ${task}`); } this.lines.push(''); return this; } /** * Ajoute la section des métadonnées (priorité, dépendances) * @returns {UserStoryBuilder} - L'instance courante pour chaînage */ withMetadata() { // Ajout conditionnel des métadonnées si présentes if (this.priority) { this.lines.push(`**Priority:** ${this.priority}`); } if (this.dependencies?.length > 0) { this.lines.push(`**Dependencies:** ${this.dependencies.join(', ')}`); } // Ligne vide après les métadonnées si au moins une section a été ajoutée if (this.priority || this.dependencies?.length > 0) { this.lines.push(''); } return this; } /** * Ajoute les instructions pour l'IA * @returns {UserStoryBuilder} - L'instance courante pour chaînage */ withAIInstructions() { this.lines.push('## 🤖 User Story Instructions for AI'); this.lines.push(''); this.lines.push('Lorsque vous travaillez avec cette User Story:'); this.lines.push('- Mettez à jour le statut des tâches en remplaçant [ ] par [x] lorsqu\'elles sont terminées'); this.lines.push('- Mettez à jour le statut des critères d\'acceptation en remplaçant [ ] par [x] lorsqu\'ils sont validés'); this.lines.push('- Vérifiez les liens vers la feature parent et les dépendances avant de commencer'); this.lines.push('- Ne modifiez PAS la structure existante du document'); this.lines.push(''); this.lines.push('Exemple de mise à jour:'); this.lines.push('- [ ] Tâche à faire → - [x] Tâche terminée'); this.lines.push(''); this.lines.push('---'); return this; } /** * Construit le document markdown final * @returns {string} - Document markdown formaté */ build() { return this.lines.join('\n'); } } /** * Format a user story as Markdown with checkboxes and enhanced AI instructions * Façade pour le UserStoryBuilder * @param {Object} userStory - User story object * @returns {string} - Formatted Markdown */ function formatUserStory(userStory) { return new UserStoryBuilder(userStory) .withHeader() .withDescription() .withAcceptanceCriteria() .withTasks() .withMetadata() .withAIInstructions() .build(); } /** * Traite les user stories et crée les fichiers markdown correspondants * @param {Array} stories - Liste des user stories à traiter * @param {string} featureDir - Chemin du répertoire de la feature parente * @param {Map} userStoryMap - Map pour suivre les user stories générées * @param {Object} feature - Objet feature parent * @param {string} feature.title - Titre de la feature parente * @returns {Promise<void>} * @throws {Error} Si le répertoire de la feature n'existe pas */ async function processUserStories(stories, featureDir, userStoryMap, feature) { if (!featureDir) { throw handleMarkdownError('Le répertoire de la feature est requis pour traiter les user stories'); } // Créer le répertoire des user stories systématiquement const userStoriesDir = path.join(featureDir, 'user-stories'); await fs.ensureDir(userStoriesDir); if (!stories?.length) { // Créer un README explicatif si aucune user story const readmePath = path.join(userStoriesDir, 'README.md'); const msg = `# 📭 Aucune user story générée pour cette feature\n\nCe dossier a été créé automatiquement par Agile Planner.\n\n- Si vous attendiez des user stories, vérifiez la configuration ou la description de la feature.\n- Vous pouvez ajouter manuellement des user stories ici si besoin.\n`; await fs.writeFile(readmePath, msg); console.error(chalk.yellow('⚠️ Aucune user story trouvée : README explicatif généré dans user-stories (stderr).')); return; } try { // Traiter chaque user story for (const story of stories) { await _formatAndWriteSingleStoryFile(story, userStoriesDir, userStoryMap, feature); } } catch (error) { throw handleMarkdownError(`Error processing user stories`, error); } } /** * Traite une user story individuelle * @param {Object} story - Données de la user story * @param {string} story.title - Titre de la user story * @param {string} [story.id] - Identifiant optionnel de la user story * @param {string} userStoriesDir - Répertoire des user stories * @param {Map} userStoryMap - Map pour suivre les user stories * @param {Object} [feature] - Feature parente * @param {string} [feature.title] - Titre de la feature parente * @returns {Promise<Object>} - Informations sur la user story créée * @throws {Error} Si la user story n'a pas de titre ou si le répertoire n'existe pas */ async function _formatAndWriteSingleStoryFile(story, userStoriesDir, userStoryMap, feature) { // Vérifications des paramètres requis, avec message d'erreur explicite pour chaque cas if (!story?.title) throw handleMarkdownError('Le titre de la user story est requis'); if (!userStoriesDir) throw handleMarkdownError('Le répertoire des user stories est requis'); const storyTitle = story.title; const storyId = story.id || ''; const storySlug = createSlug(storyTitle); // Créer le répertoire de la story const storyPath = path.join(userStoriesDir, storySlug + '.md'); // Formatage du contenu markdown const storyContent = formatUserStory(story); // Écrire le fichier await fs.writeFile(storyPath, storyContent); console.error(chalk.green(`✓ User story document created: ${storyPath} (stderr)`)); // Tracker cette user story dans la map const relativePath = `./${path.relative(process.cwd(), storyPath).replace(/\\/g, '/')}`; userStoryMap.set(storyTitle, { path: storyPath, relativePath, feature: feature ? feature.title : null, id: storyId }); // Si la story a aussi un ID, l'ajouter comme clé alternative if (storyId) { userStoryMap.set(storyId, { path: storyPath, relativePath, feature: feature?.title || null, id: storyId }); } return { path: storyPath, relativePath, title: storyTitle, id: storyId, slug: storySlug }; } /** * Factory function pour créer un formateur de user stories * @param {Object} options - Options de configuration * @returns {Object} - API du formateur de user stories avec des méthodes pour traiter et formater les user stories * @property {Function} formatUserStory - Fonction pour formater une user story en markdown * @property {Function} processUserStories - Fonction pour traiter un ensemble de user stories * @property {Function} processUserStory - Fonction pour traiter une user story individuelle * @property {Function} createBuilder - Fonction pour créer une instance de UserStoryBuilder */ function createStoryFormatter(_options = {}) { /** * Crée une instance de UserStoryBuilder pour construire une user story personnalisée * @param {Object} userStory - Données de base de la user story * @returns {UserStoryBuilder} - Une instance du builder pour construire la user story */ function createBuilder(userStory) { if (!userStory) throw new Error('Les données de la user story sont requises pour créer un builder'); return new UserStoryBuilder(userStory); } return { formatUserStory, processUserStories, processUserStory: _formatAndWriteSingleStoryFile, createBuilder }; } module.exports = { createStoryFormatter, formatUserStory, processUserStories, UserStoryBuilder };