UNPKG

handoff-ai

Version:

AI collaboration framework for persistent project knowledge and smooth handoffs

1,223 lines (1,046 loc) 48.6 kB
const fs = require('fs-extra'); const path = require('path'); const { glob } = require('glob'); // Main inject-docs functionality async function injectDocs(options) { // Check if documentation exists const docsExist = await checkHandoffDocs(); if (!docsExist) { throw new Error('No Handoff documentation found to inject'); } // Analyze project structure and detect languages const projectInfo = await analyzeProject(options.files); if (projectInfo.files.length === 0) { throw new Error('No source files found to process'); } // Load and parse Handoff documentation const handoffDocs = await loadHandoffDocs(); // Generate inline documentation for each file const results = await generateInlineDocs(projectInfo, handoffDocs, options); return results; } async function checkHandoffDocs() { const requiredFiles = [ '.project/assumptions.md' ]; for (const file of requiredFiles) { if (await fs.pathExists(file)) { return true; } } return false; } async function analyzeProject(filePattern) { const defaultPatterns = [ '**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx', '**/*.py', '**/*.java', '**/*.cs', '**/*.go', '**/*.rs', '**/*.php', '**/*.rb', '**/*.cpp', '**/*.c', '**/*.h', '**/*.hpp' ]; const patterns = filePattern ? [filePattern] : defaultPatterns; const files = []; for (const pattern of patterns) { const matches = await glob(pattern, { ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**', '.project/**'] }); files.push(...matches); } const uniqueFiles = [...new Set(files)]; const projectInfo = { files: uniqueFiles.map(file => ({ path: file, language: detectLanguage(file) })) }; return projectInfo; } function detectLanguage(filePath) { const ext = path.extname(filePath).toLowerCase(); const languageMap = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.java': 'java', '.cs': 'csharp', '.go': 'go', '.rs': 'rust', '.php': 'php', '.rb': 'ruby', '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp', '.c': 'c', '.h': 'c', '.hpp': 'cpp' }; return languageMap[ext] || 'unknown'; } async function loadHandoffDocs() { const docs = {}; // Whitelist of actual handoff documentation files (not guides) const handoffDocFiles = [ '.project/assumptions.md', // AI decisions and patterns '.project/architecture.md', // Architecture documentation '.project/api-docs.md', // API documentation '.project/design-principles.md', // Design principles '.project/patterns.md', // Code patterns '.project/business-logic.md', // Business logic documentation '.project/constraints.md' // Project constraints ]; // Load only actual handoff docs (not guides or EPICs) for (const docFile of handoffDocFiles) { if (await fs.pathExists(docFile)) { const fileName = path.basename(docFile, '.md'); docs[fileName] = await fs.readFile(docFile, 'utf8'); } } // Special handling for assumptions.md - extract meaningful content if (docs.assumptions) { docs.assumptions = extractMeaningfulAssumptions(docs.assumptions); } return docs; } function extractMeaningfulAssumptions(assumptionsContent) { // Extract only the actual assumptions, not the template/guide content const lines = assumptionsContent.split('\n'); const meaningfulContent = []; let inAssumptionSection = false; for (const line of lines) { // Skip template sections and guides if (line.includes('How to Use This File') || line.includes('Assumption Template') || line.includes('Review Status') || line.includes('*No assumptions recorded yet*')) { continue; } // Look for actual content sections if (line.includes('### Architecture Decisions') || line.includes('### Design Principles') || line.includes('### API Behaviors') || line.includes('### Implementation Patterns') || line.includes('### Current Assumptions')) { inAssumptionSection = true; continue; } // Collect meaningful content if (inAssumptionSection && line.trim() && !line.startsWith('#') && !line.includes('```')) { meaningfulContent.push(line.trim()); } } return meaningfulContent.join('\n'); } async function generateInlineDocs(projectInfo, handoffDocs, options) { const results = []; const failures = []; const unsavedFiles = []; // Check if we have meaningful handoff docs const hasHandoffDocs = Object.keys(handoffDocs).length > 0 && Object.values(handoffDocs).some(doc => doc && doc.trim().length > 50); for (const fileInfo of projectInfo.files) { if (fileInfo.language === 'unknown') continue; const fileContent = await fs.readFile(fileInfo.path, 'utf8'); // Check if file is empty or appears to be unsaved if (fileContent.trim().length === 0) { unsavedFiles.push(fileInfo.path); continue; } const codeElements = extractCodeElements(fileContent, fileInfo.language); // If no code elements found but file has content, it might be unsaved changes if (codeElements.length === 0 && fileContent.length > 50) { // File has content but no recognizable code patterns - might be unsaved unsavedFiles.push(fileInfo.path); continue; } const fileResult = { file: fileInfo.path, language: fileInfo.language, originalContent: fileContent, documentation: [] }; // Process each function/class in the file for (const element of codeElements) { let documentation; if (hasHandoffDocs) { // Try to use handoff docs documentation = generateHandoffBasedDocumentation(element, handoffDocs, fileInfo.language); } if (!documentation) { // Fallback to generic documentation documentation = generateGenericDocumentation(element, fileContent, fileInfo.language); } if (documentation) { fileResult.documentation.push({ element: element, documentation: documentation }); } } if (fileResult.documentation.length > 0) { fileResult.newContent = injectDocumentationIntoFile( fileContent, fileResult.documentation, fileInfo.language ); results.push(fileResult); } } // Handle unsaved files if (unsavedFiles.length > 0) { const chalk = require('chalk'); console.log(chalk.yellow('\n⚠️ Some files appear to be unsaved or empty:')); unsavedFiles.forEach(file => { console.log(chalk.gray(` • ${file}`)); }); console.log(chalk.blue('\n💡 Please save your files in the editor before running inject-docs.')); console.log(chalk.gray('Files with unsaved changes cannot be processed for documentation injection.')); if (results.length === 0) { throw new Error('No files could be processed. Please save your files and try again.'); } } return results; } function extractCodeElements(content, language) { const elements = []; switch (language) { case 'javascript': case 'typescript': // Extract functions and classes const functionRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)/g; const classRegex = /(?:export\s+)?class\s+(\w+)/g; const arrowFunctionRegex = /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g; let match; while ((match = functionRegex.exec(content)) !== null) { elements.push({ type: 'function', name: match[1], line: getLineNumber(content, match.index) }); } while ((match = classRegex.exec(content)) !== null) { elements.push({ type: 'class', name: match[1], line: getLineNumber(content, match.index) }); } while ((match = arrowFunctionRegex.exec(content)) !== null) { elements.push({ type: 'function', name: match[1], line: getLineNumber(content, match.index) }); } break; case 'python': const pyFunctionRegex = /def\s+(\w+)\s*\([^)]*\):/g; const pyClassRegex = /class\s+(\w+)(?:\([^)]*\))?:/g; while ((match = pyFunctionRegex.exec(content)) !== null) { elements.push({ type: 'function', name: match[1], line: getLineNumber(content, match.index) }); } while ((match = pyClassRegex.exec(content)) !== null) { elements.push({ type: 'class', name: match[1], line: getLineNumber(content, match.index) }); } break; } return elements; } function getLineNumber(content, index) { return content.substring(0, index).split('\n').length; } function generateHandoffBasedDocumentation(element, handoffDocs, language) { // Look for relevant context in handoff docs const allDocsText = Object.values(handoffDocs).join('\n'); const elementName = element.name.toLowerCase(); // Find relevant sections const relevantSections = findRelevantSections(elementName, allDocsText); if (relevantSections.length === 0) { return null; // No relevant context found } // Generate documentation based on relevant sections const context = relevantSections[0]; const description = generateDescriptionFromContext(element, context); if (!description) { return null; } return formatDocumentationForLanguage(element, description, language); } function findRelevantSections(elementName, docsText) { const lines = docsText.split('\n'); const relevantSections = []; // Function-specific patterns to look for const functionPatterns = { validateEmail: ['email validation', 'validate email', 'email format'], hashPassword: ['password hash', 'bcrypt', 'salt rounds', 'password security'], calculateDiscount: ['discount calculation', 'percentage-based discount', 'discount percent'], formatPrice: ['price format', 'currency format', 'intl.numberformat', 'usd display'], generateRandomId: ['random string', 'unique identifier', 'id generation', 'base36'], authenticateToken: ['jwt token', 'token verification', 'authentication middleware'], generateToken: ['jwt token', 'token creation', 'token generation'], loginUser: ['user login', 'authentication', 'email password'], registerUser: ['user registration', 'new account', 'signup'], getAllProducts: ['product listing', 'get products', 'product filter'], getProductById: ['product by id', 'specific product', 'find product'], createProduct: ['create product', 'new product', 'add product'], updateProduct: ['update product', 'modify product', 'product update'], startServer: ['server startup', 'express server', 'server listen'] }; // Get patterns for this specific function const specificPatterns = functionPatterns[elementName] || []; // First try direct function name match for (let i = 0; i < lines.length; i++) { const line = lines[i].toLowerCase(); if (line.includes(elementName.toLowerCase())) { // Get context around this line (2 lines before and after) const contextStart = Math.max(0, i - 2); const contextEnd = Math.min(lines.length, i + 3); const context = lines.slice(contextStart, contextEnd).join(' ').trim(); relevantSections.push({ context: context, score: 10, source: 'direct-match' }); } } // Then try function-specific patterns if (specificPatterns.length > 0) { for (const pattern of specificPatterns) { for (let i = 0; i < lines.length; i++) { const line = lines[i].toLowerCase(); if (line.includes(pattern)) { // Get context around this line const contextStart = Math.max(0, i - 1); const contextEnd = Math.min(lines.length, i + 2); const context = lines.slice(contextStart, contextEnd).join(' ').trim(); relevantSections.push({ context: context, score: 8, source: 'pattern-match', pattern: pattern }); } } } } // If no matches yet, try generic function type patterns if (relevantSections.length === 0) { const genericPatterns = getGenericFunctionPatterns(elementName); for (const pattern of genericPatterns) { for (let i = 0; i < lines.length; i++) { const line = lines[i].toLowerCase(); if (line.includes(pattern.keyword)) { // Get context around this line const contextStart = Math.max(0, i - 1); const contextEnd = Math.min(lines.length, i + 2); const context = lines.slice(contextStart, contextEnd).join(' ').trim(); relevantSections.push({ context: context, score: pattern.score, source: 'generic-match', pattern: pattern.keyword }); } } } } // Sort by relevance score and return only the context return relevantSections .sort((a, b) => b.score - a.score) .slice(0, 2) // Take top 2 most relevant .map(section => section.context); } function getGenericFunctionPatterns(elementName) { const patterns = []; const lowerName = elementName.toLowerCase(); // Add patterns based on function name prefixes if (lowerName.startsWith('get') || lowerName.startsWith('fetch') || lowerName.startsWith('retrieve')) { patterns.push({ keyword: 'retrieve', score: 5 }); patterns.push({ keyword: 'get', score: 4 }); patterns.push({ keyword: 'fetch', score: 4 }); } if (lowerName.startsWith('create') || lowerName.startsWith('add') || lowerName.startsWith('insert')) { patterns.push({ keyword: 'create', score: 5 }); patterns.push({ keyword: 'add', score: 4 }); patterns.push({ keyword: 'new', score: 3 }); } if (lowerName.startsWith('update') || lowerName.startsWith('modify') || lowerName.startsWith('change')) { patterns.push({ keyword: 'update', score: 5 }); patterns.push({ keyword: 'modify', score: 4 }); patterns.push({ keyword: 'change', score: 3 }); } if (lowerName.startsWith('delete') || lowerName.startsWith('remove')) { patterns.push({ keyword: 'delete', score: 5 }); patterns.push({ keyword: 'remove', score: 4 }); } if (lowerName.startsWith('validate') || lowerName.startsWith('check')) { patterns.push({ keyword: 'validation', score: 5 }); patterns.push({ keyword: 'validate', score: 4 }); } if (lowerName.startsWith('format') || lowerName.startsWith('display')) { patterns.push({ keyword: 'format', score: 5 }); patterns.push({ keyword: 'display', score: 4 }); } if (lowerName.startsWith('generate') || lowerName.startsWith('create')) { patterns.push({ keyword: 'generate', score: 5 }); patterns.push({ keyword: 'create', score: 4 }); } if (lowerName.startsWith('hash') || lowerName.startsWith('encrypt')) { patterns.push({ keyword: 'hash', score: 5 }); patterns.push({ keyword: 'encrypt', score: 4 }); patterns.push({ keyword: 'security', score: 3 }); } if (lowerName.startsWith('auth') || lowerName.startsWith('login')) { patterns.push({ keyword: 'authentication', score: 5 }); patterns.push({ keyword: 'auth', score: 4 }); patterns.push({ keyword: 'login', score: 3 }); } if (lowerName.startsWith('calculate') || lowerName.startsWith('compute')) { patterns.push({ keyword: 'calculate', score: 5 }); patterns.push({ keyword: 'compute', score: 4 }); } return patterns; } function getFunctionSpecificPatterns(functionName) { const lowerName = functionName.toLowerCase(); const patterns = []; // Define very specific patterns for each function type if (lowerName.includes('validate')) { if (lowerName.includes('email')) { patterns.push({ keyword: 'email validation', score: 9 }); patterns.push({ keyword: 'email format', score: 8 }); patterns.push({ keyword: 'email regex', score: 7 }); } patterns.push({ keyword: 'input validation', score: 6 }); patterns.push({ keyword: 'validation pattern', score: 5 }); } if (lowerName.includes('hash')) { if (lowerName.includes('password')) { patterns.push({ keyword: 'password hashing', score: 9 }); patterns.push({ keyword: 'bcrypt', score: 8 }); patterns.push({ keyword: 'password security', score: 7 }); patterns.push({ keyword: 'salt rounds', score: 6 }); } patterns.push({ keyword: 'hashing', score: 5 }); } if (lowerName.includes('format')) { if (lowerName.includes('price')) { patterns.push({ keyword: 'price format', score: 9 }); patterns.push({ keyword: 'currency format', score: 8 }); patterns.push({ keyword: 'money display', score: 7 }); } patterns.push({ keyword: 'formatting', score: 5 }); } if (lowerName.includes('calculate')) { if (lowerName.includes('discount')) { patterns.push({ keyword: 'discount calculation', score: 9 }); patterns.push({ keyword: 'price discount', score: 8 }); } patterns.push({ keyword: 'calculation', score: 5 }); } if (lowerName.includes('generate')) { if (lowerName.includes('id') || lowerName.includes('random')) { patterns.push({ keyword: 'id generation', score: 9 }); patterns.push({ keyword: 'random id', score: 8 }); patterns.push({ keyword: 'unique identifier', score: 7 }); } patterns.push({ keyword: 'generation', score: 5 }); } if (lowerName.includes('auth')) { patterns.push({ keyword: 'authentication', score: 8 }); patterns.push({ keyword: 'auth token', score: 7 }); patterns.push({ keyword: 'login', score: 6 }); } return patterns; } function isContextRelevantToFunction(functionName, context, keyword) { const lowerContext = context.toLowerCase(); const lowerName = functionName.toLowerCase(); // Check if the context actually relates to the function's purpose // This prevents pulling random text that just happens to contain a keyword // For email validation if (lowerName.includes('validate') && lowerName.includes('email')) { return lowerContext.includes('email') && (lowerContext.includes('valid') || lowerContext.includes('format') || lowerContext.includes('regex')); } // For password hashing if (lowerName.includes('hash') && lowerName.includes('password')) { return lowerContext.includes('password') && (lowerContext.includes('hash') || lowerContext.includes('bcrypt') || lowerContext.includes('security')); } // For price formatting if (lowerName.includes('format') && lowerName.includes('price')) { return lowerContext.includes('price') || lowerContext.includes('currency') || lowerContext.includes('money'); } // For discount calculation if (lowerName.includes('calculate') && lowerName.includes('discount')) { return lowerContext.includes('discount') && lowerContext.includes('price'); } // For ID generation if (lowerName.includes('generate') && (lowerName.includes('id') || lowerName.includes('random'))) { return lowerContext.includes('id') || lowerContext.includes('random') || lowerContext.includes('unique'); } // Generic relevance check - context should mention the function or related concepts return lowerContext.includes(lowerName) || lowerContext.includes(keyword); } function generateDescriptionFromContext(element, context) { // Clean and extract meaningful description from context let description = context .replace(/[#*\-\[\]]/g, '') // Remove markdown .replace(/\s+/g, ' ') // Normalize whitespace .replace(/^\d+\.\s*/, '') // Remove numbered list markers .replace(/^[•\-]\s*/, '') // Remove bullet points .trim(); // Function-specific extraction patterns const functionName = element.name.toLowerCase(); // Look for specific patterns in the context that match our function const patterns = { validateemail: /email validation.*?regex pattern/i, hashpassword: /password.*?bcrypt.*?salt rounds/i, calculatediscount: /discount calculation.*?percentage.*?validation/i, formatprice: /price format.*?currency.*?intl\.numberformat/i, generaterandomid: /random.*?string.*?base36/i, authenticatetoken: /jwt token.*?verification/i, generatetoken: /jwt token.*?creation/i, loginuser: /user login.*?authentication.*?email.*?password/i, registeruser: /user registration.*?new account.*?validation/i, getallproducts: /product listing.*?optional.*?filtering/i, getproductbyid: /product.*?by id.*?error handling/i, createproduct: /create.*?product.*?validation/i, updateproduct: /update.*?product.*?validation/i, startserver: /server.*?startup.*?express.*?port/i }; const pattern = patterns[functionName]; if (pattern) { const match = description.match(pattern); if (match) { return match[0]; } } // Look for lines that contain function-specific keywords const lines = description.split(/[.!?:]+/); for (const line of lines) { const trimmed = line.trim(); if (trimmed.length > 15 && isSentenceRelevantToFunction(element.name, trimmed)) { return trimmed; } } // Skip generic or irrelevant descriptions const genericPhrases = [ 'separation of concerns', 'error handling', 'product management', 'requires authentication', 'consistent json', 'routes, middleware', 'all database' ]; const lowerDescription = description.toLowerCase(); const isGeneric = genericPhrases.some(phrase => lowerDescription.includes(phrase)); if (isGeneric) { return null; // This context is too generic/irrelevant } // If context is too long, extract the most relevant part if (description.length > 100) { const sentences = description.split(/[.!?]+/); // Look for sentences that mention the function name for (const sentence of sentences) { const trimmed = sentence.trim(); if (trimmed.toLowerCase().includes(element.name.toLowerCase()) && trimmed.length > 20 && trimmed.length < 80) { return trimmed; } } // Look for sentences that are function-specific for (const sentence of sentences) { const trimmed = sentence.trim(); if (isSentenceRelevantToFunction(element.name, trimmed) && trimmed.length > 20 && trimmed.length < 80) { return trimmed; } } // If no good sentence found, return null to fall back to generic return null; } // Ensure it's a proper description and relevant to the function if (description.length < 10 || !isSentenceRelevantToFunction(element.name, description)) { return null; } return description; } function isSentenceRelevantToFunction(functionName, sentence) { const lowerName = functionName.toLowerCase(); const lowerSentence = sentence.toLowerCase(); // Skip sentences that are clearly not about the function const irrelevantPhrases = [ 'architecture decisions', 'design principles', 'api behaviors', 'error responses', 'requires authentication', 'separation of concerns', 'async operations', 'all database', 'routes, middleware' ]; if (irrelevantPhrases.some(phrase => lowerSentence.includes(phrase))) { return false; } // Check if the sentence is actually about the function's purpose if (lowerName.includes('validate') && lowerName.includes('email')) { return lowerSentence.includes('email') && (lowerSentence.includes('valid') || lowerSentence.includes('format') || lowerSentence.includes('check') || lowerSentence.includes('regex')) && !lowerSentence.includes('requires email, password'); } if (lowerName.includes('hash') && lowerName.includes('password')) { return lowerSentence.includes('password') && (lowerSentence.includes('hash') || lowerSentence.includes('bcrypt') || lowerSentence.includes('security') || lowerSentence.includes('salt')) && !lowerSentence.includes('always hash passwords, validate inputs'); } if (lowerName.includes('format') && lowerName.includes('price')) { return (lowerSentence.includes('price') || lowerSentence.includes('currency') || lowerSentence.includes('money') || lowerSentence.includes('format')) && !lowerSentence.includes('standard format { error'); } if (lowerName.includes('calculate') && lowerName.includes('discount')) { return lowerSentence.includes('discount') && (lowerSentence.includes('calculate') || lowerSentence.includes('percentage')) && !lowerSentence.includes('requires authentication'); } if (lowerName.includes('generate') && (lowerName.includes('id') || lowerName.includes('random'))) { return (lowerSentence.includes('id') || lowerSentence.includes('random') || lowerSentence.includes('generate') || lowerSentence.includes('unique')) && !lowerSentence.includes('user id and email'); } // Generic check - sentence should be about the function's domain return lowerSentence.includes(lowerName) || lowerSentence.includes(lowerName.replace(/([A-Z])/g, ' $1').toLowerCase().trim()); } function generateGenericDocumentation(element, fileContent, language) { // Generate documentation based on function analysis const functionCode = extractFunctionCode(element, fileContent); const purpose = inferFunctionPurpose(element.name, functionCode); let description = generateDescriptionFromPurpose(element.name, purpose); // Ensure we always have a description for generic documentation if (!description) { description = `${element.name} - ${element.type} implementation`; } return formatDocumentationForLanguage(element, description, language); } function extractFunctionCode(element, fileContent) { const lines = fileContent.split('\n'); const startLine = element.line - 1; // Extract function code (simplified - just get a few lines) const codeLines = lines.slice(startLine, Math.min(startLine + 5, lines.length)); return codeLines.join('\n'); } function inferFunctionPurpose(functionName, functionCode) { const purpose = { keywords: [], confidence: 0 }; // Analyze function name for clues const nameKeywords = extractKeywordsFromName(functionName); purpose.keywords.push(...nameKeywords); // Analyze function code for clues const codeKeywords = extractKeywordsFromCode(functionCode); purpose.keywords.push(...codeKeywords); // Calculate confidence based on how much we can infer if (nameKeywords.length > 0) purpose.confidence += 0.5; if (codeKeywords.length > 0) purpose.confidence += 0.3; if (functionCode.includes('return')) purpose.confidence += 0.2; return purpose; } function extractKeywordsFromName(functionName) { const keywords = []; // Common function name patterns const patterns = { validate: ['validation', 'validate', 'check'], hash: ['hash', 'encrypt', 'security'], calculate: ['calculate', 'compute', 'math'], format: ['format', 'display', 'string'], generate: ['generate', 'create', 'random'], authenticate: ['auth', 'login', 'security'], register: ['register', 'signup', 'user'], get: ['retrieve', 'fetch', 'get'], create: ['create', 'add', 'new'], update: ['update', 'modify', 'change'], delete: ['delete', 'remove', 'destroy'] }; const lowerName = functionName.toLowerCase(); for (const [pattern, relatedKeywords] of Object.entries(patterns)) { if (lowerName.includes(pattern)) { keywords.push(...relatedKeywords); } } return keywords; } function extractKeywordsFromCode(code) { const keywords = []; const lowerCode = code.toLowerCase(); // Look for specific patterns in code if (lowerCode.includes('bcrypt') || lowerCode.includes('hash')) { keywords.push('password', 'hash', 'security'); } if (lowerCode.includes('email') || lowerCode.includes('@')) { keywords.push('email', 'validation'); } if (lowerCode.includes('jwt') || lowerCode.includes('token')) { keywords.push('authentication', 'token', 'security'); } if (lowerCode.includes('price') || lowerCode.includes('currency')) { keywords.push('price', 'money', 'format'); } if (lowerCode.includes('math.random') || lowerCode.includes('random')) { keywords.push('random', 'generate', 'id'); } return keywords; } function generateDescriptionFromPurpose(functionName, purpose) { // Fallback for specific functions with high-quality descriptions const specificDescriptions = { validateEmail: 'Validates email addresses using regex pattern', hashPassword: 'Securely hashes passwords using bcrypt with salt rounds', calculateDiscount: 'Calculates price discount based on percentage with validation', formatPrice: 'Formats numeric price as USD currency string', generateRandomId: 'Generates random unique identifier using base36 encoding', authenticateToken: 'Verifies JWT authentication token', generateToken: 'Creates JWT token for user authentication', loginUser: 'Authenticates user credentials and returns token', registerUser: 'Creates new user account with validation and password hashing', getAllProducts: 'Retrieves products with optional category and price filtering', getProductById: 'Retrieves specific product by ID with error handling', createProduct: 'Creates new product with required field validation', updateProduct: 'Updates existing product data with validation', startServer: 'Starts Express server on specified port' }; // Check for specific function descriptions first if (specificDescriptions[functionName]) { return specificDescriptions[functionName]; } // Generate description based on inferred purpose if (purpose.keywords.length === 0) { return `${functionName} - Function implementation`; } // Create description based on keywords and function analysis const primaryKeyword = purpose.keywords[0]; const lowerName = functionName.toLowerCase(); // Enhanced descriptions based on function name patterns if (lowerName.includes('validate')) { if (lowerName.includes('email')) return 'Validates email addresses using regex pattern'; return 'Validates input data according to specified rules'; } if (lowerName.includes('hash')) { if (lowerName.includes('password')) return 'Securely hashes passwords using bcrypt'; return 'Hashes data for secure storage'; } if (lowerName.includes('calculate')) { if (lowerName.includes('discount')) return 'Calculates price discount based on percentage'; return 'Calculates and returns computed value'; } if (lowerName.includes('format')) { if (lowerName.includes('price')) return 'Formats numeric price as currency string'; return 'Formats data for display presentation'; } if (lowerName.includes('generate')) { if (lowerName.includes('id') || lowerName.includes('random')) return 'Generates random unique identifier'; if (lowerName.includes('token')) return 'Generates authentication token'; return 'Generates new value or resource'; } if (lowerName.includes('authenticate') || lowerName.includes('auth')) { if (lowerName.includes('token')) return 'Verifies authentication token'; return 'Authenticates user credentials'; } if (lowerName.includes('login')) { return 'Authenticates user credentials and returns token'; } if (lowerName.includes('register')) { return 'Creates new user account with validation'; } if (lowerName.startsWith('get') || lowerName.startsWith('fetch')) { if (lowerName.includes('all')) return 'Retrieves all items with optional filtering'; if (lowerName.includes('byid') || lowerName.includes('by_id')) return 'Retrieves specific item by ID'; return 'Retrieves data from storage'; } if (lowerName.startsWith('create') || lowerName.startsWith('add')) { return 'Creates new resource with validation'; } if (lowerName.startsWith('update') || lowerName.startsWith('modify')) { return 'Updates existing resource data'; } if (lowerName.startsWith('delete') || lowerName.startsWith('remove')) { return 'Deletes specified resource'; } if (lowerName.startsWith('start')) { if (lowerName.includes('server')) return 'Starts server on specified port'; return 'Starts specified service or process'; } // Fallback to generic descriptions const descriptions = { validation: `Validates input data`, validate: `Validates input data`, hash: `Hashes data for secure storage`, encrypt: `Encrypts data for security`, calculate: `Calculates and returns computed value`, format: `Formats data for display`, generate: `Generates new value`, authenticate: `Authenticates user credentials`, register: `Registers new user account`, retrieve: `Retrieves data from storage`, create: `Creates new resource`, update: `Updates existing resource`, delete: `Deletes specified resource` }; const description = descriptions[primaryKeyword] || `Handles ${primaryKeyword} operations`; return description; } function formatDocumentationForLanguage(element, description, language) { switch (language) { case 'javascript': case 'typescript': let jsDoc = `/**\n * ${description}`; if (element.type === 'function') { const returnDoc = generateReturnDocumentation(element.name); jsDoc += `\n * @returns ${returnDoc}`; } jsDoc += `\n */`; return jsDoc; case 'python': let pyDoc = `"""${description}`; if (element.type === 'function') { const returnDoc = generateReturnDocumentation(element.name, 'python'); pyDoc += `\n \n Returns:\n ${returnDoc}`; } pyDoc += `\n """`; return pyDoc; case 'java': let javaDoc = `/**\n * ${description}`; if (element.type === 'method' || element.type === 'function') { const returnDoc = generateReturnDocumentation(element.name, 'java'); javaDoc += `\n * @return ${returnDoc}`; } javaDoc += `\n */`; return javaDoc; default: return `/* ${description} */`; } } function generateReturnDocumentation(functionName, language = 'javascript') { const lowerName = functionName.toLowerCase(); // Specific return documentation for each function const specificReturns = { validateEmail: { javascript: '{boolean} True if email format is valid, false otherwise', python: 'bool: True if email format is valid, False otherwise', java: 'boolean True if email format is valid, false otherwise' }, hashPassword: { javascript: '{Promise<string>} Promise resolving to bcrypt hashed password', python: 'str: Bcrypt hashed password string', java: 'String Bcrypt hashed password string' }, calculateDiscount: { javascript: '{number} Final price after discount is applied', python: 'float: Final price after discount is applied', java: 'double Final price after discount is applied' }, formatPrice: { javascript: '{string} Formatted price string in USD currency format', python: 'str: Formatted price string in USD currency format', java: 'String Formatted price string in USD currency format' }, generateRandomId: { javascript: '{string} Random alphanumeric identifier string', python: 'str: Random alphanumeric identifier string', java: 'String Random alphanumeric identifier string' }, authenticateToken: { javascript: '{void} Calls next() if valid, sends 401/403 response if invalid', python: 'None: Calls next function or sends error response', java: 'void Calls next function or sends error response' }, generateToken: { javascript: '{string} Signed JWT token string', python: 'str: Signed JWT token string', java: 'String Signed JWT token string' }, loginUser: { javascript: '{void} Sends JSON response with token or error', python: 'None: Sends JSON response with token or error', java: 'void Sends JSON response with token or error' }, registerUser: { javascript: '{void} Sends JSON response with user data and token or error', python: 'None: Sends JSON response with user data and token or error', java: 'void Sends JSON response with user data and token or error' }, getAllProducts: { javascript: '{void} Sends JSON response with filtered products array', python: 'None: Sends JSON response with filtered products array', java: 'void Sends JSON response with filtered products array' }, getProductById: { javascript: '{void} Sends JSON response with product object or 404 error', python: 'None: Sends JSON response with product object or 404 error', java: 'void Sends JSON response with product object or 404 error' }, createProduct: { javascript: '{void} Sends JSON response with created product or validation error', python: 'None: Sends JSON response with created product or validation error', java: 'void Sends JSON response with created product or validation error' }, updateProduct: { javascript: '{void} Sends JSON response with updated product or error', python: 'None: Sends JSON response with updated product or error', java: 'void Sends JSON response with updated product or error' }, startServer: { javascript: '{void} Starts Express server and logs port information', python: 'None: Starts server and logs port information', java: 'void Starts server and logs port information' } }; // Check for specific function documentation if (specificReturns[functionName]) { return specificReturns[functionName][language] || specificReturns[functionName].javascript; } // Pattern-based return documentation for common function types if (lowerName.startsWith('validate') || lowerName.startsWith('check') || lowerName.startsWith('is')) { return language === 'javascript' ? '{boolean} True if validation passes, false otherwise' : language === 'python' ? 'bool: True if validation passes, False otherwise' : 'boolean True if validation passes, false otherwise'; } if (lowerName.startsWith('get') || lowerName.startsWith('fetch') || lowerName.startsWith('find')) { if (lowerName.includes('all') || lowerName.includes('list')) { return language === 'javascript' ? '{Array} Array of retrieved items' : language === 'python' ? 'list: List of retrieved items' : 'Array Array of retrieved items'; } return language === 'javascript' ? '{Object|null} Retrieved item or null if not found' : language === 'python' ? 'object|None: Retrieved item or None if not found' : 'Object Retrieved item or null if not found'; } if (lowerName.startsWith('create') || lowerName.startsWith('add') || lowerName.startsWith('insert')) { return language === 'javascript' ? '{Object} Created item object' : language === 'python' ? 'object: Created item object' : 'Object Created item object'; } if (lowerName.startsWith('update') || lowerName.startsWith('modify') || lowerName.startsWith('edit')) { return language === 'javascript' ? '{Object} Updated item object' : language === 'python' ? 'object: Updated item object' : 'Object Updated item object'; } if (lowerName.startsWith('delete') || lowerName.startsWith('remove')) { return language === 'javascript' ? '{boolean} True if deletion successful' : language === 'python' ? 'bool: True if deletion successful' : 'boolean True if deletion successful'; } if (lowerName.startsWith('calculate') || lowerName.startsWith('compute')) { return language === 'javascript' ? '{number} Calculated result' : language === 'python' ? 'float: Calculated result' : 'double Calculated result'; } if (lowerName.startsWith('format') || lowerName.startsWith('stringify')) { return language === 'javascript' ? '{string} Formatted string' : language === 'python' ? 'str: Formatted string' : 'String Formatted string'; } if (lowerName.startsWith('generate') || lowerName.startsWith('build')) { return language === 'javascript' ? '{string} Generated value' : language === 'python' ? 'str: Generated value' : 'String Generated value'; } if (lowerName.includes('hash') || lowerName.includes('encrypt')) { return language === 'javascript' ? '{string} Hashed/encrypted string' : language === 'python' ? 'str: Hashed/encrypted string' : 'String Hashed/encrypted string'; } if (lowerName.includes('middleware') || lowerName.includes('auth') || lowerName.includes('handler')) { return language === 'javascript' ? '{void} Middleware function with side effects' : language === 'python' ? 'None: Function with side effects' : 'void Function with side effects'; } if (lowerName.includes('start') || lowerName.includes('init') || lowerName.includes('setup')) { return language === 'javascript' ? '{void} Initializes and starts service' : language === 'python' ? 'None: Initializes and starts service' : 'void Initializes and starts service'; } // Default fallback return language === 'javascript' ? '{*} Function return value' : language === 'python' ? 'Return value of the function' : 'Function return value'; } function injectDocumentationIntoFile(content, documentation, language) { let newContent = content; const lines = content.split('\n'); // Sort documentation by line number (descending) to avoid index shifting const sortedDocs = documentation.sort((a, b) => b.element.line - a.element.line); for (const doc of sortedDocs) { const lineIndex = doc.element.line - 1; if (lineIndex >= 0 && lineIndex < lines.length) { // Check if documentation already exists if (!hasExistingDocumentation(lines, lineIndex, language)) { // Insert documentation before the element const indent = getIndentation(lines[lineIndex]); const docLines = doc.documentation.split('\n').map(line => indent + line); lines.splice(lineIndex, 0, ...docLines); } } } return lines.join('\n'); } function hasExistingDocumentation(lines, lineIndex, language) { // Check if there's already documentation above this line if (lineIndex === 0) return false; const prevLine = lines[lineIndex - 1].trim(); // Also check a few lines above for block comments let hasDocComment = false; switch (language) { case 'javascript': case 'typescript': case 'java': // Check if previous line ends a JSDoc comment if (prevLine.endsWith('*/')) { // Look backwards to see if there's a /** starting the comment for (let i = lineIndex - 2; i >= Math.max(0, lineIndex - 10); i--) { const line = lines[i].trim(); if (line.startsWith('/**')) { hasDocComment = true; break; } if (line && !line.startsWith('*') && !line.startsWith('*/')) { break; // Found non-comment line, stop looking } } } return hasDocComment || prevLine.startsWith('/**'); case 'python': return prevLine.endsWith('"""') || prevLine.startsWith('"""'); default: return false; } } function getIndentation(line) { const match = line.match(/^(\s*)/); return match ? match[1] : ''; } async function applyDocumentationChanges(results) { for (const result of results) { await fs.writeFile(result.file, result.newContent, 'utf8'); } } function displayDryRunResults(results) { console.log('\n📋 Proposed Changes:\n'); results.forEach((result, index) => { console.log(`${index + 1}. ${result.file} (${result.language})`); console.log(` ${result.documentation.length} documentation blocks to add\n`); result.documentation.forEach((doc, docIndex) => { console.log(` ${docIndex + 1}. ${doc.element.type}: ${doc.element.name} (line ${doc.element.line})`); // Show the actual documentation content if (doc.documentation) { const firstLine = doc.documentation.split('\n')[1] || doc.documentation.split('\n')[0]; // Skip /** line console.log(` ${firstLine.trim()}\n`); } else { console.log(` [No documentation generated]\n`); } }); }); console.log('\n💡 Run without --dry-run to apply these changes'); } module.exports = { injectDocs, applyDocumentationChanges, displayDryRunResults };