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

246 lines (210 loc) 9.19 kB
/** * Module de formatage des features * @module markdown/feature-formatter */ const path = require('path'); const fs = require('fs-extra'); const chalk = require('chalk'); const { createSlug, handleMarkdownError, markdownInstructions } = require('./utils'); const { processUserStories } = require('./story-formatter'); /** * Génère le contenu d'une feature en markdown * @param {Object} feature - Données de la feature * @param {string} feature.title - Titre de la feature * @param {string} [feature.description] - Description de la feature * @param {string} [feature.business_value] - Valeur business de la feature * @param {Array} [feature.stories] - User stories associées à la feature * @param {string} epicTitle - Titre de l'epic parent * @returns {string} - Contenu markdown de la feature */ function generateFeatureContent(feature, epicTitle) { const featureTitle = feature.title; const featureDescription = feature.description || ''; const businessValue = feature.business_value || ''; let content = `# Feature: ${featureTitle}\n\n`; content += `${markdownInstructions.featureFileInstructions}\n\n`; content += `## Description\n\n${featureDescription}\n\n`; if (businessValue) { content += `## Business Value\n\n${businessValue}\n\n`; } content += `## Parent Epic\n\n${epicTitle}\n\n`; content += `## User Stories\n\n`; // Si aucune user story, ajouter un avertissement explicite if (!feature?.stories?.length) content += '> ⚠️ **Aucune user story générée pour cette feature.**\n\n'; // Les user stories seront ajoutées par référence, pas directement ici content += '_Les user stories associées se trouvent dans le dossier "user-stories"._\n\n'; return content; } /** * Traite une feature individuelle et crée le fichier markdown correspondant * @param {Object} feature - Données de la feature * @param {string} feature.title - Titre de la feature * @param {string} [feature.description] - Description de la feature * @param {string} [feature.business_value] - Valeur business de la feature * @param {Array} [feature.stories] - User stories associées à la feature * @param {string} epicDir - Chemin du répertoire de l'epic parent * @param {string} epicTitle - Titre de l'epic parent * @param {Map} userStoryMap - Map pour suivre les user stories * @param {Object} epicJson - Données JSON de l'epic pour référencement * @param {Array} epicJson.features - Liste des features de l'epic * @returns {Promise<Object>} - Objet JSON représentant la feature */ async function processFeature(feature, epicDir, epicTitle, userStoryMap, epicJson) { if (!feature?.title) { throw handleMarkdownError('Invalid feature data: title is required'); } const featureTitle = feature.title; const featureSlug = createSlug(featureTitle); try { // Créer le répertoire de la feature const featureDir = path.join(epicDir, 'features', featureSlug); await fs.ensureDir(featureDir); // Générer le contenu markdown const featureContent = generateFeatureContent(feature, epicTitle); // Écrire le fichier const featureFilePath = path.join(featureDir, 'feature.md'); await fs.writeFile(featureFilePath, featureContent); console.error(chalk.green(`✓ Feature document created: ${featureFilePath} (stderr)`)); // Créer l'entrée JSON de la feature const featureJson = createFeatureJson(feature, featureFilePath, featureSlug); // Ajouter cette feature à l'epic parent epicJson.features.push(featureJson); // Traiter les user stories de cette feature await processFeatureStories(feature, featureDir, userStoryMap, featureJson); return featureJson; } catch (error) { throw handleMarkdownError(`Error processing feature '${featureTitle}'`, error); } } /** * Traite les features d'un epic et crée les fichiers markdown correspondants * @param {Array} features - Liste des features à traiter * @param {string} epicDir - Chemin du répertoire de l'epic parent * @param {string} epicTitle - Titre de l'epic parent * @param {Map} userStoryMap - Map pour suivre les user stories * @param {Object} epicJson - Données JSON de l'epic pour référencement * @param {Array} epicJson.features - Liste des features de l'epic * @returns {Promise<void>} */ async function processFeatures(features, epicDir, epicTitle, userStoryMap, epicJson) { if (!features?.length) { console.error(chalk.yellow(`⚠️ No features found for epic "${epicTitle}", skipping features processing (stderr)`)); return; } try { // Créer le répertoire des features const featuresDir = path.join(epicDir, 'features'); await fs.ensureDir(featuresDir); // Initialiser le tableau des features dans l'epic JSON epicJson.features = []; // Traiter chaque feature for (const feature of features) { await processFeature(feature, epicDir, epicTitle, userStoryMap, epicJson); } } catch (error) { throw handleMarkdownError(`Error processing features for epic "${epicTitle}"`, error); } } /** * Factory function pour créer un formateur de features * @param {Object} options - Options de configuration * @returns {Object} - API du formateur de features */ function createFeatureFormatter(_options = {}) { /** * Formate et écrit le contenu d'une feature dans un fichier markdown * @param {Object} featureData - Données de la feature * @param {string} featureDirectoryPath - Chemin du répertoire où écrire la feature * @param {Object} parentEpic - Epic parent de la feature * @param {string} parentEpic.title - Titre de l'epic parent * @returns {Promise<string>} - Chemin du fichier créé */ async function format(featureData, featureDirectoryPath, parentEpic) { if (!featureData?.title) { throw handleMarkdownError('Invalid feature data: title is required'); } if (!featureDirectoryPath) { throw handleMarkdownError('Feature directory path is required'); } if (!parentEpic?.title) { throw handleMarkdownError('Parent epic with title is required for feature formatting'); } try { const content = generateFeatureContent(featureData, parentEpic.title); const filePath = path.join(featureDirectoryPath, 'feature.md'); await fs.writeFile(filePath, content); return filePath; } catch (error) { throw handleMarkdownError(`Failed to format feature: ${featureData.title}`, error); } } /** * Génère un slug à partir d'un titre * @param {string} title - Titre à convertir en slug * @returns {string} - Slug généré */ function generateSlug(title) { if (!title || typeof title !== 'string') { throw new Error('Title is required and must be a string'); } return createSlug(title); } return { format, generateSlug }; } /** * Crée un objet JSON représentant une feature * @param {Object} feature - Données de la feature * @param {string} featureFilePath - Chemin du fichier markdown de la feature * @param {string} featureSlug - Slug de la feature * @returns {Object} - Objet JSON représentant la feature */ function createFeatureJson(feature, featureFilePath, featureSlug) { return { title: feature.title, description: feature.description || '', business_value: feature.business_value || '', slug: featureSlug, path: `./${path.relative(process.cwd(), featureFilePath).replace(/\\/g, '/')}`, stories: [] }; } /** * Traite les user stories d'une feature * @param {Object} feature - Données de la feature * @param {Array} [feature.stories] - Liste des user stories de la feature * @param {string} featureDir - Chemin du répertoire de la feature * @param {Map} userStoryMap - Map pour suivre les user stories * @param {Object} featureJson - Objet JSON représentant la feature * @returns {Promise<void>} */ async function processFeatureStories(feature, featureDir, userStoryMap, featureJson) { // Toujours créer le dossier user-stories et un README explicatif si besoin await processUserStories(feature.stories || [], featureDir, userStoryMap, feature); // Ajouter les références aux user stories dans le JSON de la feature if (!feature?.stories?.length) return; for (const story of feature.stories) { const storyKey = story.id || story.title; if (userStoryMap.has(storyKey)) { const storyInfo = userStoryMap.get(storyKey); featureJson.stories.push({ id: story.id || '', title: story.title, path: storyInfo.relativePath }); } } } module.exports = { // Factory function principale createFeatureFormatter, // Fonctions utilisées par d'autres modules ou tests processFeatures, processFeature, generateFeatureContent, // Fonctions helper exposées pour les tests createFeatureJson, processFeatureStories };