woaru
Version:
Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language
467 lines • 20.1 kB
JavaScript
import fs from 'fs-extra';
import * as path from 'path';
import chalk from 'chalk';
import { AIReviewAgent } from './AIReviewAgent.js';
import { APP_CONFIG } from '../config/constants.js';
export class DocumentationAgent {
aiReviewAgent;
promptTemplates;
constructor(aiConfig, promptTemplates) {
this.aiReviewAgent = new AIReviewAgent(aiConfig, promptTemplates);
this.promptTemplates = promptTemplates;
}
/**
* Generate documentation for a list of files using AI analysis
*
* @param fileList - Array of file paths to process for documentation
* @param projectPath - Root path of the project for context
* @param documentationType - Type of documentation to generate ('nopro' for human-friendly, 'pro' for technical, 'forai' for machine-readable)
* @returns Promise resolving to array of documentation results with file modifications
*/
async generateDocumentation(fileList, projectPath, documentationType) {
const results = [];
console.log(chalk.cyan(`🔍 Analyzing ${fileList.length} files for documentation...`));
for (const filePath of fileList) {
try {
const fileResults = await this.processFile(filePath, projectPath, documentationType);
results.push(...fileResults);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.warn(chalk.yellow(`⚠️ Could not process ${filePath}: ${errorMessage}`));
// Continue processing other files instead of failing completely
}
}
return results;
}
/**
* Process a single file for documentation generation
*
* @param filePath - Path to the file to process
* @param projectPath - Root project path for context
* @param documentationType - Type of documentation to generate
* @returns Promise resolving to array of documentation results for this file
*/
async processFile(filePath, projectPath, documentationType) {
const content = await fs.readFile(filePath, 'utf-8');
const language = this.detectLanguage(filePath);
// Skip non-code files
if (!this.isCodeFile(language)) {
return [];
}
// For ForAI documentation, we generate file-level context headers
if (documentationType === 'forai') {
return await this.processFileForAIContext(filePath, projectPath, content, language);
}
// Extract functions/classes that need documentation
const codeElements = this.extractCodeElements(content, language);
const results = [];
for (const element of codeElements) {
// Skip if already has documentation of the same type
if (element.hasExistingDoc &&
element.existingDocType === documentationType) {
console.log(chalk.gray(` ⏭️ Skipping ${element.name} (already has ${documentationType} documentation)`));
continue;
}
try {
// Generate documentation using AI
const generatedDoc = await this.generateDocumentationForElement(element, filePath, projectPath, language, documentationType);
if (generatedDoc) {
results.push({
filePath,
originalContent: content,
generatedDoc,
insertionPoint: element.startLine,
functionName: element.name,
documentationType,
});
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.warn(chalk.yellow(`⚠️ Could not generate documentation for ${element.name}: ${errorMessage}`));
// Log additional context for debugging
console.debug(`Debug info: File=${filePath}, Element=${element.name}, Type=${documentationType}`);
}
}
return results;
}
/**
* Process a file for AI context header generation (file-level documentation)
*
* @param filePath - Path to the file to process
* @param projectPath - Root project path for context
* @param content - File content to analyze
* @param language - Programming language of the file
* @returns Promise resolving to array with single documentation result for file header
*/
async processFileForAIContext(filePath, projectPath, content, language) {
try {
// Check if file already has woaru_context header
const lines = content.split('\n');
const hasExistingAIDoc = this.checkExistingAIDocumentation(lines);
if (hasExistingAIDoc) {
console.log(chalk.gray(` ⏭️ Skipping ${path.basename(filePath)} (already has ${APP_CONFIG.DOCUMENTATION.CONTEXT_HEADER_KEY} header)`));
return [];
}
// Prepare context for AI analysis
const context = {
filePath,
language,
totalLines: lines.length,
projectContext: {
name: path.basename(projectPath),
type: 'application',
dependencies: [],
},
};
// Use AI to generate file-level context documentation
const result = await this.aiReviewAgent.performMultiLLMReview(content, context);
// Extract the best documentation from results
const aiContextHeader = this.extractBestDocumentation(result, 'forai');
if (aiContextHeader) {
console.log(chalk.gray(` ✓ Generated AI context header for ${path.basename(filePath)}`));
// Format as comment block for the specific language
const formattedHeader = this.formatAIContextHeader(aiContextHeader, language);
return [
{
filePath,
originalContent: content,
generatedDoc: formattedHeader,
insertionPoint: 1, // Insert at beginning of file
functionName: 'FILE_HEADER',
documentationType: 'forai',
},
];
}
else {
console.warn(chalk.yellow(`⚠️ No AI context generated for ${path.basename(filePath)}`));
return [];
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.warn(chalk.yellow(`⚠️ Could not generate AI context for ${path.basename(filePath)}: ${errorMessage}`));
return [];
}
}
/**
* Check if file already has woaru_context header at the beginning
*
* @param lines - Array of file lines to check
* @returns True if woaru_context header exists, false otherwise
*/
checkExistingAIDocumentation(lines) {
// Check first N lines for woaru_context header
// Why: Headers are typically at the very beginning of files
const checkLines = APP_CONFIG.DOCUMENTATION.CONTEXT_HEADER_CHECK_LINES;
for (let i = 0; i < Math.min(checkLines, lines.length); i++) {
if (lines[i].includes(`${APP_CONFIG.DOCUMENTATION.CONTEXT_HEADER_KEY}:`)) {
return true;
}
}
return false;
}
/**
* Format AI context header as appropriate comment block for the language
*
* @param aiContext - Raw AI-generated YAML context content
* @param language - Programming language for comment syntax selection
* @returns Formatted comment block with YAML content
*/
formatAIContextHeader(aiContext, language) {
// Clean up the AI response to ensure it's valid YAML
let cleanContext = aiContext.trim();
// If AI didn't include the woaru_context: wrapper, add it
// Why: Some LLMs may return just the content without the root key
const contextKey = APP_CONFIG.DOCUMENTATION.CONTEXT_HEADER_KEY;
if (!cleanContext.includes(`${contextKey}:`)) {
cleanContext = `${contextKey}:\n${cleanContext
.split('\n')
.map(line => ` ${line}`)
.join('\n')}`;
}
// Add timestamp
const timestamp = new Date().toISOString();
// Only replace if generated_at exists, otherwise add it
if (cleanContext.includes('generated_at:')) {
cleanContext = cleanContext.replace(/generated_at: .*/, `generated_at: "${timestamp}"`);
}
else {
// Add generated_at if missing
const schemaVersion = APP_CONFIG.DOCUMENTATION.SCHEMA_VERSION;
cleanContext = cleanContext.replace(new RegExp(`schema_version: "${schemaVersion}"`), `schema_version: "${schemaVersion}"\n generated_at: "${timestamp}"`);
}
// Format as comment block based on language
switch (language) {
case 'javascript':
case 'typescript':
case 'java':
case 'c':
case 'cpp':
case 'csharp':
return `/*\n${cleanContext}\n*/\n\n`;
case 'python':
case 'ruby':
return `"""\n${cleanContext}\n"""\n\n`;
case 'go':
case 'rust':
return `/*\n${cleanContext}\n*/\n\n`;
default:
return `/*\n${cleanContext}\n*/\n\n`;
}
}
/**
* Generate documentation for a specific code element using AI
*
* @param element - Code element (function/class) to document
* @param filePath - Path to the file containing the element
* @param projectPath - Root project path for context
* @param language - Programming language of the file
* @param documentationType - Type of documentation to generate
* @returns Promise resolving to generated documentation string or null if failed
*/
async generateDocumentationForElement(element, filePath, projectPath, language, documentationType) {
try {
// Prepare context for AI analysis
const context = {
filePath,
language,
totalLines: element.content.split('\n').length,
projectContext: {
name: path.basename(projectPath),
type: 'application',
dependencies: [],
},
};
// Use AI to generate documentation
const result = await this.aiReviewAgent.performMultiLLMReview(element.content, context);
// Extract the best documentation from results
const bestDoc = this.extractBestDocumentation(result, documentationType);
if (bestDoc) {
console.log(chalk.gray(` ✓ Generated ${documentationType} documentation for ${element.name}`));
return bestDoc;
}
else {
console.warn(chalk.yellow(`⚠️ No usable documentation generated for ${element.name}`));
return null;
}
}
catch (error) {
console.error(chalk.red(`❌ AI generation failed for ${element.name}: ${error instanceof Error ? error.message : 'Unknown error'}`));
return null;
}
}
/**
* Extract the best documentation from AI results
*
* @param aiResult - Results from AI analysis containing provider responses
* @param documentationType - Type of documentation requested
* @returns Best documentation string from available results or null if none found
*/
extractBestDocumentation(aiResult, _documentationType) {
// Find the first successful result
const providers = Object.keys(aiResult.results);
for (const provider of providers) {
const providerResults = aiResult.results[provider];
if (providerResults && providerResults.length > 0) {
const firstResult = providerResults[0];
// AIReviewFinding has 'suggestion' property containing the generated documentation
if (firstResult.suggestion) {
return firstResult.suggestion.trim();
}
}
}
return null;
}
/**
* Apply documentation changes to files
*/
async applyDocumentation(results) {
const fileGroups = this.groupResultsByFile(results);
for (const [filePath, fileResults] of fileGroups) {
try {
await this.applyDocumentationToFile(filePath, fileResults);
console.log(chalk.green(` ✓ Applied documentation to ${path.basename(filePath)}`));
}
catch (error) {
console.error(chalk.red(` ❌ Failed to apply documentation to ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`));
}
}
}
/**
* Apply documentation to a single file
*/
async applyDocumentationToFile(filePath, results) {
const originalContent = await fs.readFile(filePath, 'utf-8');
const lines = originalContent.split('\n');
// Sort results by insertion point (reverse order to maintain line numbers)
// Why reverse order: When inserting multiple docs, later insertions don't affect earlier line numbers
const sortedResults = results.sort((a, b) => b.insertionPoint - a.insertionPoint);
// Apply each documentation insertion
for (const result of sortedResults) {
const insertionLine = result.insertionPoint - 1; // Convert to 0-based index
// Add the documentation comment
const docLines = result.generatedDoc.split('\n');
lines.splice(insertionLine, 0, ...docLines);
}
// Write the modified content back to the file
await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
}
/**
* Group results by file path
*/
groupResultsByFile(results) {
const groups = new Map();
for (const result of results) {
if (!groups.has(result.filePath)) {
groups.set(result.filePath, []);
}
const group = groups.get(result.filePath);
if (group) {
group.push(result);
}
}
return groups;
}
/**
* Extract code elements (functions, classes, methods) from source code
*
* Uses regex patterns to identify code constructs that should be documented.
* Why regex instead of AST parsing: Balance between complexity and reliability across languages.
*
* @param content - Source code content to analyze
* @param language - Programming language for pattern selection
* @returns Array of detected code elements with metadata
*/
extractCodeElements(content, language) {
const elements = [];
const lines = content.split('\n');
// Simple regex patterns for function/class detection
const patterns = this.getPatterns(language);
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
for (const pattern of patterns) {
const match = line.match(pattern.regex);
if (match) {
const functionName = match[pattern.nameGroup] || 'anonymous';
// Check if already has documentation
const hasExistingDoc = this.checkExistingDocumentation(lines, i);
elements.push({
name: functionName,
startLine: i + 1,
endLine: i + 1, // Simplified - just the declaration line
content: line,
hasExistingDoc: hasExistingDoc.hasDoc,
existingDocType: hasExistingDoc.type,
});
}
}
}
return elements;
}
/**
* Get regex patterns for different languages
*/
getPatterns(language) {
const patterns = [];
switch (language) {
case 'javascript':
case 'typescript':
patterns.push({ regex: /^(export\s+)?(async\s+)?function\s+(\w+)/, nameGroup: 3 }, {
regex: /^(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(/,
nameGroup: 3,
}, { regex: /^(export\s+)?(class\s+)(\w+)/, nameGroup: 3 }, { regex: /^\s*(async\s+)?(\w+)\s*\([^)]*\)\s*\{/, nameGroup: 2 });
break;
case 'python':
patterns.push({ regex: /^(async\s+)?def\s+(\w+)/, nameGroup: 2 }, { regex: /^class\s+(\w+)/, nameGroup: 1 });
break;
case 'java':
patterns.push({
regex: /^(public|private|protected)?\s*(static\s+)?(void|[\w<>]+)\s+(\w+)\s*\(/,
nameGroup: 4,
}, {
regex: /^(public|private|protected)?\s*(class|interface)\s+(\w+)/,
nameGroup: 3,
});
break;
default:
// Generic patterns
patterns.push({ regex: /function\s+(\w+)/, nameGroup: 1 }, { regex: /class\s+(\w+)/, nameGroup: 1 });
}
return patterns;
}
/**
* Check if a function already has documentation
*/
checkExistingDocumentation(lines, functionLineIndex) {
// Look backwards from the function line to find documentation
for (let i = functionLineIndex - 1; i >= 0; i--) {
const line = lines[i].trim();
// Stop if we hit another function or non-comment line
if (line &&
!line.startsWith('//') &&
!line.startsWith('/*') &&
!line.startsWith('*')) {
break;
}
// Check for human-friendly documentation
if (line.includes('Explain-for-humans:')) {
return { hasDoc: true, type: 'nopro' };
}
// Check for technical documentation (JSDoc/TSDoc)
if (line.includes('@param') ||
line.includes('@returns') ||
line.includes('/**')) {
return { hasDoc: true, type: 'pro' };
}
// Check for ForAI-optimized documentation (woaru_context header)
if (line.includes(`${APP_CONFIG.DOCUMENTATION.CONTEXT_HEADER_KEY}:`)) {
return { hasDoc: true, type: 'forai' };
}
}
return { hasDoc: false };
}
/**
* Detect programming language from file extension
*/
detectLanguage(filePath) {
const ext = path.extname(filePath).toLowerCase();
const languageMap = {
'.js': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.py': 'python',
'.java': 'java',
'.cpp': 'cpp',
'.c': 'c',
'.cs': 'csharp',
'.go': 'go',
'.rs': 'rust',
'.php': 'php',
'.rb': 'ruby',
};
return languageMap[ext] || 'unknown';
}
/**
* Check if file is a code file that should be documented
*/
isCodeFile(language) {
const supportedLanguages = [
'javascript',
'typescript',
'python',
'java',
'cpp',
'c',
'csharp',
'go',
'rust',
'php',
'ruby',
];
return supportedLanguages.includes(language);
}
}
//# sourceMappingURL=DocumentationAgent.js.map