UNPKG

claude-git-hooks

Version:

Git hooks with Claude CLI for code analysis and automatic commit messages

302 lines (260 loc) 9.77 kB
/** * File: resolution-prompt.js * Purpose: Generates AI-friendly resolution prompts when code analysis fails * * Key responsibilities: * - Parse blocking issues from analysis results * - Load resolution template * - Replace placeholders with issue data * - Include affected file contents * - Write resolution prompt file * * Dependencies: * - fs/promises: For file operations * - path: For file paths * - git-operations: For repository info * - logger: Debug and error logging */ import fs from 'fs/promises'; import path from 'path'; import { getRepoName, getCurrentBranch, getRepoRoot } from './git-operations.js'; import logger from './logger.js'; /** * Custom error for resolution prompt failures */ class ResolutionPromptError extends Error { constructor(message, { templatePath, cause, context } = {}) { super(message); this.name = 'ResolutionPromptError'; this.templatePath = templatePath; this.cause = cause; this.context = context; } } /** * Formats blocking issues for resolution prompt * Why: Structures issues in readable markdown format for AI processing * * @param {Array<Object>} blockingIssues - Array of blocking issues * @returns {string} Formatted markdown * * Issue object structure: * { * description: string, // Human-readable description * file: string, // Relative file path * line: number, // Line number * method: string, // Method or class name * severity: string // 'blocker' | 'critical' * } */ const formatBlockingIssues = (blockingIssues) => { logger.debug( 'resolution-prompt - formatBlockingIssues', 'Formatting blocking issues', { issueCount: blockingIssues.length } ); if (!Array.isArray(blockingIssues) || blockingIssues.length === 0) { return 'No blocking issues found.'; } return blockingIssues .map((issue, index) => { const issueNum = index + 1; const severity = (issue.severity || 'unknown').toUpperCase(); return `### Issue #${issueNum} [${severity}] **Description:** ${issue.description || 'No description'} **Location:** ${issue.file || 'unknown'}:${issue.line || '?'} **Method/Class:** ${issue.method || 'unknown'} `; }) .join('\n'); }; /** * Gets unique file paths from blocking issues * Why: Need to include full content of affected files * * @param {Array<Object>} blockingIssues - Array of blocking issues * @returns {Array<string>} Unique file paths */ const getAffectedFiles = (blockingIssues) => { if (!Array.isArray(blockingIssues)) { return []; } // Extract unique file paths const filePaths = blockingIssues .map(issue => issue.file) .filter(file => file && typeof file === 'string'); const uniqueFiles = [...new Set(filePaths)]; logger.debug( 'resolution-prompt - getAffectedFiles', 'Extracted affected files', { totalIssues: blockingIssues.length, uniqueFiles: uniqueFiles.length } ); return uniqueFiles; }; /** * Reads and formats affected file contents * Why: AI needs full file context to suggest fixes * Why absolute path: Ensures files are found regardless of cwd (cross-platform) * * @param {Array<string>} filePaths - Array of file paths (relative to repo root) * @returns {Promise<string>} Formatted file contents in markdown */ const formatFileContents = async (filePaths) => { logger.debug( 'resolution-prompt - formatFileContents', 'Reading file contents', { fileCount: filePaths.length } ); // Why: Use repo root for absolute paths (works on Windows/PowerShell/Git Bash) const repoRoot = getRepoRoot(); const fileContents = await Promise.allSettled( filePaths.map(async (filePath) => { try { const absolutePath = path.join(repoRoot, filePath); const content = await fs.readFile(absolutePath, 'utf8'); return `### File: ${filePath} \`\`\` ${content} \`\`\` `; } catch (error) { logger.error( 'resolution-prompt - formatFileContents', `Failed to read file: ${filePath}`, error ); return `### File: ${filePath} **Error:** Could not read file `; } }) ); const formatted = fileContents .filter(result => result.status === 'fulfilled') .map(result => result.value) .join('\n'); logger.debug( 'resolution-prompt - formatFileContents', 'File contents formatted', { successfulReads: fileContents.filter(r => r.status === 'fulfilled').length } ); return formatted; }; /** * Generates resolution prompt file from analysis results * Why: Creates AI-friendly prompt that can be copied to Claude for automatic fixes * Why absolute path: Ensures files are found/created regardless of cwd (cross-platform) * * @param {Object} analysisResult - Analysis result with blocking issues * @param {Array<Object>} analysisResult.blockingIssues - Array of blocking issues * @param {Object} options - Generation options * @param {string} options.outputPath - Output file path (default: 'claude_resolution_prompt.md') * @param {string} options.templatePath - Template path (default: '.claude/prompts/CLAUDE_RESOLUTION_PROMPT.md') * @param {number} options.fileCount - Number of files analyzed * @returns {Promise<string>} Path to generated resolution prompt * @throws {ResolutionPromptError} If generation fails */ const generateResolutionPrompt = async ( analysisResult, { outputPath = null, templatePath = '.claude/prompts/CLAUDE_RESOLUTION_PROMPT.md', fileCount = 0 } = {} ) => { // Why: Use repo root for absolute paths (works on Windows/PowerShell/Git Bash) const repoRoot = getRepoRoot(); // Load config to get default output path const { getConfig } = await import('../config.js'); const config = await getConfig(); const finalOutputPath = outputPath || config.output.resolutionFile; const absoluteOutputPath = path.join(repoRoot, finalOutputPath); const absoluteTemplatePath = path.join(repoRoot, templatePath); logger.debug( 'resolution-prompt - generateResolutionPrompt', 'Generating resolution prompt', { repoRoot, outputPath: absoluteOutputPath, templatePath: absoluteTemplatePath, blockingIssuesCount: analysisResult.blockingIssues?.length || 0 } ); try { // Load template const template = await fs.readFile(absoluteTemplatePath, 'utf8'); // Get repository context const repoName = getRepoName(); const branchName = getCurrentBranch(); const commitSha = 'pending'; // Format blocking issues const issuesFormatted = formatBlockingIssues(analysisResult.blockingIssues || []); // Get and format affected files const affectedFiles = getAffectedFiles(analysisResult.blockingIssues || []); const fileContentsFormatted = await formatFileContents(affectedFiles); // Replace placeholders // Why: Use simple string replacement instead of complex regex for clarity let prompt = template .replace(/{{REPO_NAME}}/g, repoName) .replace(/{{BRANCH_NAME}}/g, branchName) .replace(/{{COMMIT_SHA}}/g, commitSha) .replace(/{{FILE_COUNT}}/g, String(fileCount)) .replace(/{{BLOCKING_ISSUES}}/g, issuesFormatted) .replace(/{{FILE_CONTENTS}}/g, fileContentsFormatted); // Ensure output directory exists const outputDir = path.dirname(absoluteOutputPath); await fs.mkdir(outputDir, { recursive: true }); // Write resolution prompt file await fs.writeFile(absoluteOutputPath, prompt, 'utf8'); logger.debug( 'resolution-prompt - generateResolutionPrompt', 'Resolution prompt generated successfully', { outputPath: absoluteOutputPath, promptLength: prompt.length } ); return absoluteOutputPath; } catch (error) { logger.error( 'resolution-prompt - generateResolutionPrompt', 'Failed to generate resolution prompt', error ); if (error.code === 'ENOENT' && error.path === absoluteTemplatePath) { throw new ResolutionPromptError('Resolution template not found', { templatePath: absoluteTemplatePath, cause: error }); } throw new ResolutionPromptError('Failed to generate resolution prompt', { templatePath: absoluteTemplatePath, cause: error }); } }; /** * Checks if resolution prompt should be generated * Why: Only generate when there are actual blocking issues * * @param {Object} analysisResult - Analysis result * @returns {boolean} True if should generate prompt */ const shouldGeneratePrompt = (analysisResult) => { const hasBlockingIssues = Array.isArray(analysisResult.blockingIssues) && analysisResult.blockingIssues.length > 0; logger.debug( 'resolution-prompt - shouldGeneratePrompt', 'Checking if resolution prompt needed', { hasBlockingIssues, blockingIssuesCount: analysisResult.blockingIssues?.length || 0 } ); return hasBlockingIssues; }; export { ResolutionPromptError, formatBlockingIssues, getAffectedFiles, formatFileContents, generateResolutionPrompt, shouldGeneratePrompt };