| "use strict"; |
| |
| |
| |
| |
| |
| |
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { |
| if (k2 === undefined) k2 = k; |
| var desc = Object.getOwnPropertyDescriptor(m, k); |
| if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { |
| desc = { enumerable: true, get: function() { return m[k]; } }; |
| } |
| Object.defineProperty(o, k2, desc); |
| }) : (function(o, m, k, k2) { |
| if (k2 === undefined) k2 = k; |
| o[k2] = m[k]; |
| })); |
| var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { |
| Object.defineProperty(o, "default", { enumerable: true, value: v }); |
| }) : function(o, v) { |
| o["default"] = v; |
| }); |
| var __importStar = (this && this.__importStar) || (function () { |
| var ownKeys = function(o) { |
| ownKeys = Object.getOwnPropertyNames || function (o) { |
| var ar = []; |
| for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; |
| return ar; |
| }; |
| return ownKeys(o); |
| }; |
| return function (mod) { |
| if (mod && mod.__esModule) return mod; |
| var result = {}; |
| if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); |
| __setModuleDefault(result, mod); |
| return result; |
| }; |
| })(); |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.generatePlan = exports.formatPlanAsComment = exports.expandContext = exports.retrieveFiles = exports.predictRelevantFiles = exports.clearRepoCache = exports.mapRepositoryStructure = exports.IGNORED_PATTERNS = exports.SENSITIVE_FILE_PATTERNS = exports.generateAgenticPlanningPrompt = exports.generateCompactFallback = exports.extractRelevantSnippet = exports.validateFileExistsInTree = exports.heuristicFileSelection = exports.ISSUE_MODE_PROMPTS = exports.detectIssueMode = exports.generateFileSummary = exports.smartTruncate = exports.TOKEN_BUDGET = exports.TIER_LIMITS = void 0; |
| const octokit_1 = require("./octokit"); |
| const generated_filter_1 = require("./generated-filter"); |
| |
| |
| |
| exports.TIER_LIMITS = { |
| free: { |
| maxPhases: 1, |
| maxFiles: 5, |
| maxChars: 30000, |
| maxRetrievalRounds: 1, |
| enableContextExpansion: false, |
| enableAdvancedAnalysis: false |
| }, |
| starter: { |
| maxPhases: 2, |
| maxFiles: 10, |
| maxChars: 75000, |
| maxRetrievalRounds: 2, |
| enableContextExpansion: true, |
| enableAdvancedAnalysis: false |
| }, |
| pro: { |
| maxPhases: 5, |
| maxFiles: 15, |
| maxChars: 100000, |
| maxRetrievalRounds: 3, |
| enableContextExpansion: true, |
| enableAdvancedAnalysis: true |
| } |
| }; |
| |
| |
| |
| exports.TOKEN_BUDGET = { |
| MAX_CONTEXT_TOKENS: 3500, |
| MAX_RETRIEVED_FILES: 5, |
| MAX_FILE_CHARS: 2000, |
| MAX_SNIPPET_LINES: 80, |
| MAX_RETRIES: 1, |
| MAX_SUMMARY_CHARS: 300, |
| MAX_SYMBOLS_PER_FILE: 10, |
| MAX_EXPANDED_TOKENS: 5000, |
| MAX_EXPANSION_DEPTH: 1, |
| CACHE_TTL_MS: 60 * 60 * 1000, |
| MAX_OUTPUT_TOKENS: 1800 |
| }; |
| |
| |
| |
| |
| |
| |
| |
| const smartTruncate = (content, maxLength) => { |
| if (content.length <= maxLength) { |
| return content; |
| } |
| const truncated = content.substring(0, maxLength); |
| |
| const boundaries = [ |
| /\nexport\s+(?:const|function|class|interface)\s+\w+/g, |
| /\nfunction\s+\w+\s*\(/g, |
| /\nconst\s+\w+\s*=\s*(?:async\s+)?\(?/g, |
| /\nclass\s+\w+/g, |
| /\ninterface\s+\w+/g, |
| /\n\/\*\*[\s\S]*?\*\//g, |
| /\n\/\/.*$/gm, |
| /\n\s*\n/g |
| ]; |
| let bestMatchIndex = -1; |
| let bestMatch = ''; |
| |
| for (const boundary of boundaries) { |
| let match; |
| const regex = new RegExp(boundary.source, boundary.flags); |
| while ((match = regex.exec(truncated)) !== null) { |
| if (match.index > bestMatchIndex && match.index < maxLength - 100) { |
| bestMatchIndex = match.index; |
| bestMatch = match[0]; |
| } |
| } |
| } |
| |
| if (bestMatchIndex > 0) { |
| return (content.substring(0, bestMatchIndex) + '\n... [truncated at boundary]'); |
| } |
| |
| const lastNewline = truncated.lastIndexOf('\n'); |
| if (lastNewline > maxLength - 200) { |
| return content.substring(0, lastNewline) + '\n... [truncated]'; |
| } |
| |
| return truncated + '\n... [truncated]'; |
| }; |
| exports.smartTruncate = smartTruncate; |
| |
| |
| |
| |
| const generateFileSummary = async (content, path) => { |
| const lines = content.split('\n'); |
| const symbols = []; |
| const dependencies = []; |
| const uiElements = []; |
| const dataFlow = []; |
| const reactHooks = []; |
| const apiCalls = []; |
| const contextUsage = []; |
| try { |
| |
| if (path.endsWith('.ts') || |
| path.endsWith('.tsx') || |
| path.endsWith('.js') || |
| path.endsWith('.jsx')) { |
| const tsMorph = await Promise.resolve().then(() => __importStar(require('ts-morph'))); |
| const { Project, SyntaxKind } = tsMorph; |
| const project = new Project({ |
| useInMemoryFileSystem: true |
| }); |
| |
| const sourceFile = project.createSourceFile(path, content); |
| |
| const exportDeclarations = sourceFile.getExportDeclarations(); |
| for (const exportDecl of exportDeclarations) { |
| const moduleSpecifier = exportDecl.getModuleSpecifierValue(); |
| if (moduleSpecifier) |
| dependencies.push(moduleSpecifier); |
| } |
| |
| const functions = sourceFile.getFunctions(); |
| const classes = sourceFile.getClasses(); |
| const interfaces = sourceFile.getInterfaces(); |
| functions.forEach((fn) => { |
| const name = fn.getName(); |
| if (name) |
| symbols.push(name); |
| |
| if (fn.isAsync()) { |
| dataFlow.push(`async function: ${name}`); |
| } |
| |
| fn.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((callExpr) => { |
| const expr = callExpr.getExpression().getText(); |
| if (expr === 'fetch' || |
| expr.includes('axios') || |
| expr.includes('request')) { |
| apiCalls.push(`${name} calls ${expr}`); |
| } |
| }); |
| }); |
| classes.forEach((cls) => { |
| const name = cls.getName(); |
| if (name) |
| symbols.push(name); |
| |
| const isReactComponent = cls.getExtends()?.getText().includes('Component') || |
| sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).length > 0; |
| if (isReactComponent) { |
| dataFlow.push(`React component: ${name}`); |
| } |
| }); |
| interfaces.forEach((iface) => { |
| const name = iface.getName(); |
| if (name) |
| symbols.push(name); |
| }); |
| |
| sourceFile |
| .getDescendantsOfKind(SyntaxKind.CallExpression) |
| .forEach((callExpr) => { |
| const expr = callExpr.getExpression().getText(); |
| if (expr.startsWith('use') && |
| expr.length > 3 && |
| expr[3] === expr[3].toUpperCase()) { |
| reactHooks.push(expr); |
| } |
| }); |
| |
| if (path.endsWith('.tsx') || path.endsWith('.jsx')) { |
| const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement); |
| const jsxOpeningElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement); |
| const allJsxElements = [...jsxElements, ...jsxOpeningElements]; |
| const uniqueTags = new Set(); |
| allJsxElements.forEach((jsx) => { |
| const tagName = jsx.getTagNameNode().getText(); |
| |
| if (tagName[0] === tagName[0].toUpperCase() || |
| [ |
| 'Button', |
| 'Input', |
| 'Form', |
| 'Modal', |
| 'Card', |
| 'Nav', |
| 'Header', |
| 'Footer', |
| 'Sidebar' |
| ].includes(tagName)) { |
| uniqueTags.add(tagName); |
| } |
| }); |
| uiElements.push(...Array.from(uniqueTags)); |
| } |
| |
| sourceFile |
| .getDescendantsOfKind(SyntaxKind.CallExpression) |
| .forEach((callExpr) => { |
| const expr = callExpr.getExpression().getText(); |
| if (expr.includes('Context') || |
| expr.includes('createContext') || |
| expr.includes('useContext')) { |
| contextUsage.push(expr); |
| } |
| }); |
| |
| sourceFile.getVariableStatements().forEach((varStmt) => { |
| varStmt.getDeclarations().forEach((decl) => { |
| const name = decl.getName(); |
| const type = decl.getTypeNode()?.getText(); |
| if (type?.includes('useState') || type?.includes('useReducer')) { |
| dataFlow.push(`state: ${name}`); |
| } |
| }); |
| }); |
| |
| try { |
| project.removeSourceFile(sourceFile); |
| } |
| catch { } |
| } |
| } |
| catch (error) { |
| |
| console.log('[SUMMARY] ts-morph failed, using fallback for:', path, error); |
| |
| for (const line of lines) { |
| const exportMatch = line.match(/export\s+(?:const|function|class|interface)\s+(\w+)/); |
| if (exportMatch) |
| symbols.push(exportMatch[1]); |
| const componentMatch = line.match(/function\s+(\w+)\s*\(|const\s+(\w+)\s*=/); |
| if (componentMatch) { |
| const name = componentMatch[1] || componentMatch[2]; |
| if (name && name[0] === name[0].toUpperCase()) |
| symbols.push(name); |
| } |
| |
| const hookMatch = line.match(/(use\w+)\(/); |
| if (hookMatch) |
| reactHooks.push(hookMatch[1]); |
| |
| if (line.includes('fetch(') || |
| line.includes('axios.') || |
| line.includes('request(')) { |
| apiCalls.push('API call detected'); |
| } |
| } |
| } |
| |
| let semanticRole = 'module'; |
| const filename = path.split('/').pop() || path; |
| const lowerPath = path.toLowerCase(); |
| if (lowerPath.includes('/components/') || lowerPath.includes('/component/')) { |
| semanticRole = 'ui-component'; |
| } |
| else if (lowerPath.includes('/pages/') || lowerPath.includes('/page/')) { |
| semanticRole = 'page'; |
| } |
| else if (lowerPath.includes('/hooks/') || filename.includes('use')) { |
| semanticRole = 'react-hook'; |
| } |
| else if (lowerPath.includes('/api/') || lowerPath.includes('/routes/')) { |
| semanticRole = 'api-handler'; |
| } |
| else if (lowerPath.includes('/utils/') || lowerPath.includes('/helpers/')) { |
| semanticRole = 'utility'; |
| } |
| else if (lowerPath.includes('/services/') || lowerPath.includes('/lib/')) { |
| semanticRole = 'service'; |
| } |
| else if (filename.includes('api') || filename.includes('route')) { |
| semanticRole = 'api-handler'; |
| } |
| else if (reactHooks.length > 0) { |
| semanticRole = 'react-hook'; |
| } |
| else if (uiElements.length > 0) { |
| semanticRole = 'ui-component'; |
| } |
| |
| const summaryParts = []; |
| if (semanticRole !== 'module') { |
| summaryParts.push(`${semanticRole.replace('-', ' ')}`); |
| } |
| if (symbols.length > 0) { |
| summaryParts.push(`exports: ${symbols.slice(0, 5).join(', ')}`); |
| } |
| if (reactHooks.length > 0) { |
| summaryParts.push(`hooks: ${reactHooks.slice(0, 3).join(', ')}`); |
| } |
| if (uiElements.length > 0) { |
| summaryParts.push(`UI: ${uiElements.slice(0, 3).join(', ')}`); |
| } |
| if (apiCalls.length > 0) { |
| summaryParts.push(`API calls`); |
| } |
| if (contextUsage.length > 0) { |
| summaryParts.push(`context usage`); |
| } |
| if (dataFlow.length > 0) { |
| summaryParts.push(`manages state/data`); |
| } |
| const summary = summaryParts.length > 0 |
| ? summaryParts.join(' | ') |
| : `File (${lines.length} lines)`; |
| return { |
| path, |
| summary: summary.substring(0, exports.TOKEN_BUDGET.MAX_SUMMARY_CHARS), |
| symbols: symbols.slice(0, exports.TOKEN_BUDGET.MAX_SYMBOLS_PER_FILE), |
| estimatedLines: lines.length, |
| semanticRole, |
| dependencies: dependencies.slice(0, 10), |
| uiElements: uiElements.slice(0, 10), |
| dataFlow: dataFlow.slice(0, 5) |
| }; |
| }; |
| exports.generateFileSummary = generateFileSummary; |
| const detectIssueMode = (issueTitle, issueBody) => { |
| const text = (issueTitle + ' ' + issueBody).toLowerCase(); |
| if (text.includes('button') || |
| text.includes('component') || |
| text.includes('layout') || |
| text.includes('responsive') || |
| text.includes('mobile') || |
| text.includes('nav')) { |
| return 'ui'; |
| } |
| if (text.includes('api') || |
| text.includes('endpoint') || |
| text.includes('auth') || |
| text.includes('database') || |
| text.includes('server') || |
| text.includes('backend')) { |
| return 'backend'; |
| } |
| if (text.includes('slow') || |
| text.includes('performance') || |
| text.includes('optimize') || |
| text.includes('bundle') || |
| text.includes('render') || |
| text.includes('cache')) { |
| return 'performance'; |
| } |
| if (text.includes('security') || |
| text.includes('vulnerability') || |
| text.includes('inject') || |
| text.includes('xss') || |
| text.includes('csrf') || |
| text.includes('auth')) { |
| return 'security'; |
| } |
| return 'general'; |
| }; |
| exports.detectIssueMode = detectIssueMode; |
| exports.ISSUE_MODE_PROMPTS = { |
| ui: 'Senior frontend engineer. Focus on components, layouts, interactions.', |
| backend: 'Senior backend engineer. Focus on APIs, auth, database.', |
| performance: 'Performance specialist. Focus on bundle size, caching, queries.', |
| security: 'Security engineer. Focus on injection, auth, sanitization.', |
| general: 'Senior full-stack engineer. Provide implementation plan.' |
| }; |
| const heuristicFileSelection = (issueTitle, issueBody, repoFiles, issueMode) => { |
| const text = (issueTitle + ' ' + issueBody).toLowerCase(); |
| const keywords = text |
| .split(/\s+/) |
| .filter(w => w.length > 3) |
| .slice(0, 5); |
| const scoredFiles = repoFiles.map(filePath => { |
| const lowerPath = filePath.toLowerCase(); |
| let score = 0; |
| const reasons = []; |
| |
| for (const keyword of keywords) { |
| if (lowerPath.includes(keyword)) { |
| score += 10; |
| reasons.push(`keyword match: ${keyword}`); |
| } |
| } |
| |
| if (issueMode === 'ui') { |
| if (lowerPath.includes('/components/') || |
| lowerPath.includes('/component/')) { |
| score += 15; |
| reasons.push('ui component folder'); |
| } |
| if (lowerPath.includes('/pages/') || lowerPath.includes('/page/')) { |
| score += 12; |
| reasons.push('ui page folder'); |
| } |
| if (lowerPath.includes('/layouts/') || lowerPath.includes('/layout/')) { |
| score += 10; |
| reasons.push('layout folder'); |
| } |
| if (lowerPath.includes('button') || |
| lowerPath.includes('nav') || |
| lowerPath.includes('header')) { |
| score += 8; |
| reasons.push('ui-related keyword'); |
| } |
| } |
| else if (issueMode === 'backend') { |
| if (lowerPath.includes('api')) { |
| score += 15; |
| reasons.push('api folder'); |
| } |
| if (lowerPath.includes('auth')) { |
| score += 12; |
| reasons.push('auth folder'); |
| } |
| if (lowerPath.includes('db') || lowerPath.includes('model')) { |
| score += 10; |
| reasons.push('database folder'); |
| } |
| if (lowerPath.includes('controller') || lowerPath.includes('service')) { |
| score += 8; |
| reasons.push('backend folder'); |
| } |
| } |
| else if (issueMode === 'performance') { |
| if (lowerPath.includes('config') || |
| lowerPath.includes('webpack') || |
| lowerPath.includes('vite')) { |
| score += 15; |
| reasons.push('config folder'); |
| } |
| if (lowerPath.includes('cache')) { |
| score += 12; |
| reasons.push('cache folder'); |
| } |
| } |
| else if (issueMode === 'security') { |
| if (lowerPath.includes('auth') || lowerPath.includes('middleware')) { |
| score += 15; |
| reasons.push('security-critical folder'); |
| } |
| if (lowerPath.includes('validation')) { |
| score += 12; |
| reasons.push('validation folder'); |
| } |
| } |
| |
| const filename = filePath.split('/').pop() || ''; |
| if (filename === 'index.ts' || |
| filename === 'index.js' || |
| filename === 'index.tsx') { |
| score += 5; |
| reasons.push('index file'); |
| } |
| if (!filePath.includes('/')) { |
| score += 3; |
| reasons.push('root file'); |
| } |
| |
| if (issueMode === 'ui' && |
| (filePath.endsWith('.tsx') || filePath.endsWith('.jsx'))) { |
| score += 5; |
| reasons.push('react component extension'); |
| } |
| if (issueMode === 'backend' && |
| (filePath.endsWith('.ts') || filePath.endsWith('.js'))) { |
| score += 3; |
| reasons.push('backend extension'); |
| } |
| return { path: filePath, score, reasons }; |
| }); |
| |
| scoredFiles.sort((a, b) => b.score - a.score); |
| const topFiles = scoredFiles |
| .filter(f => f.score > 0) |
| .slice(0, exports.TOKEN_BUDGET.MAX_RETRIEVED_FILES) |
| .map(f => f.path); |
| console.log('[HEURISTIC SELECTION] Top files:', topFiles.map((path, i) => ({ |
| rank: i + 1, |
| path, |
| score: scoredFiles.find(f => f.path === path)?.score, |
| reasons: scoredFiles.find(f => f.path === path)?.reasons |
| }))); |
| return topFiles; |
| }; |
| exports.heuristicFileSelection = heuristicFileSelection; |
| |
| |
| |
| const validateFileExistsInTree = (filePath, repoFiles) => { |
| const normalizedPath = filePath.replace(/^\//, '').replace(/\\/g, '/'); |
| return repoFiles.some(f => f.replace(/^\//, '').replace(/\\/g, '/') === normalizedPath); |
| }; |
| exports.validateFileExistsInTree = validateFileExistsInTree; |
| |
| |
| |
| const extractRelevantSnippet = (content, searchTerm, maxLines = exports.TOKEN_BUDGET.MAX_SNIPPET_LINES) => { |
| const lines = content.split('\n'); |
| const relevantLines = []; |
| const lowerSearchTerm = searchTerm.toLowerCase(); |
| for (let i = 0; i < lines.length; i++) { |
| const line = lines[i]; |
| if (line.toLowerCase().includes(lowerSearchTerm)) { |
| |
| const start = Math.max(0, i - 3); |
| const end = Math.min(lines.length, i + maxLines / 2); |
| for (let j = start; j < end; j++) { |
| if (!relevantLines.includes(lines[j])) { |
| relevantLines.push(lines[j]); |
| } |
| } |
| if (relevantLines.length >= maxLines) |
| break; |
| } |
| } |
| return relevantLines.slice(0, maxLines).join('\n'); |
| }; |
| exports.extractRelevantSnippet = extractRelevantSnippet; |
| |
| |
| |
| const generateCompactFallback = (issueTitle, relevantFiles, issueMode) => { |
| const mode = issueMode; |
| let suggestedAreas = []; |
| if (mode === 'ui') { |
| suggestedAreas = [ |
| 'components', |
| 'layouts', |
| 'navigation', |
| 'shared UI elements' |
| ]; |
| } |
| else if (mode === 'backend') { |
| suggestedAreas = ['API routes', 'controllers', 'models', 'middleware']; |
| } |
| else if (mode === 'performance') { |
| suggestedAreas = [ |
| 'rendering logic', |
| 'database queries', |
| 'bundle configuration', |
| 'caching' |
| ]; |
| } |
| else if (mode === 'security') { |
| suggestedAreas = [ |
| 'input validation', |
| 'authentication flows', |
| 'API endpoints', |
| 'data sanitization' |
| ]; |
| } |
| else { |
| suggestedAreas = relevantFiles.slice(0, 3); |
| } |
| return `## Compact Implementation Plan
|
|
|
| **Likely Affected Areas:**
|
| ${suggestedAreas.map(area => `- ${area}`).join('\n')}
|
|
|
| **Suggested Tasks:**
|
| - Analyze the issue requirements
|
| - Identify necessary code changes
|
| - Update affected files
|
| - Test the implementation
|
| - Verify no regressions
|
|
|
| **Suggested Search Terms:**
|
| ${relevantFiles |
| .slice(0, 3) |
| .map(f => f.split('/').pop()) |
| .join(', ')}
|
|
|
| *Generated by Prix AI - Compact Fallback Mode*`; |
| }; |
| exports.generateCompactFallback = generateCompactFallback; |
| |
| |
| |
| const generateAgenticPlanningPrompt = (issueTitle, issueBody, fileSummaries, issueMode) => { |
| const modePrompt = exports.ISSUE_MODE_PROMPTS[issueMode]; |
| const summaryContext = fileSummaries |
| .map(f => `- ${f.path}: ${f.summary}`) |
| .join('\n'); |
| return `${modePrompt}
|
|
|
| Issue: ${issueTitle}
|
| Description: ${issueBody}
|
|
|
| Files:
|
| ${summaryContext}
|
|
|
| IMPORTANT: Return your response as markdown only.
|
| DO NOT use markdown tables. Use headings and bullet lists instead.
|
| DO NOT return JSON. Return plain markdown format.
|
|
|
| Include these sections in order:
|
| 1. Understanding (brief explanation of the issue)
|
| 2. Affected files (list)
|
| 3. Implementation phases (as headings with bullet lists, not tables)
|
| - For EACH phase, include a code block with the specific AI prompt to execute that phase
|
| - Format: "### Phase X: [Title]" then tasks/risks, then "\`\`\`" followed by the AI prompt for that phase
|
| 4. Risks (bullet list)
|
| 5. Testing checklist (bullet list)
|
| 6. Combined AI Prompt (final code block with a comprehensive prompt that combines all phases)
|
|
|
| CRITICAL - AI Prompt Format:
|
| - Each phase MUST have its own code block with a specific, detailed AI prompt to execute just that phase
|
| - The phase prompt should be actionable and specific (e.g., "You are a code scanner. Go through the codebase and find all static buttons...")
|
| - At the end, include a "### Combined AI Prompt" section with a single comprehensive prompt that combines all phases
|
| - The combined prompt should allow the user to solve the entire issue in one go by pasting it to an AI
|
|
|
| Example phase prompt format:
|
| \`\`\`
|
| You are a senior frontend developer. Your task is to [specific task]. Go through the codebase and [specific actions]. List your findings to the user.
|
| \`\`\`
|
|
|
| Example combined prompt format:
|
| \`\`\`
|
| You are a senior full-stack developer. Your task is to [overall goal].
|
| Phase 1: [phase 1 task]
|
| Phase 2: [phase 2 task]
|
| [Additional context and instructions]
|
| \`\`\`
|
|
|
| Be concise. Focus on actionable tasks.`; |
| }; |
| exports.generateAgenticPlanningPrompt = generateAgenticPlanningPrompt; |
| |
| |
| |
| exports.SENSITIVE_FILE_PATTERNS = [ |
| /\.env$/, |
| /\.env\./, |
| /credentials/, |
| /secret/, |
| /private.*key/, |
| /\.pem$/, |
| /\.key$/, |
| /password/, |
| /token/, |
| /\.aws/, |
| /\.ssh/ |
| ]; |
| exports.IGNORED_PATTERNS = [ |
| /node_modules/, |
| /dist/, |
| /build/, |
| /\.next/, |
| /coverage/, |
| /\.min\.js$/, |
| /\.bundle\.js$/, |
| /\.generated\./, |
| /vendor/, |
| /\.map$/, |
| /\.snap$/, |
| /__pycache__/, |
| /\.pyc$/, |
| /\.egg-info/, |
| /target/, |
| /out/, |
| /\.cache/, |
| /\.vscode/, |
| /\.idea/, |
| /\.DS_Store$/, |
| /Thumbs\.db$/ |
| ]; |
| const repoStructureCache = new Map(); |
| |
| |
| |
| const getCacheKey = (owner, repo, ref) => { |
| return `${owner}/${repo}/${ref}`; |
| }; |
| |
| |
| |
| const isCacheValid = (entry) => { |
| const now = Date.now(); |
| return now - entry.timestamp < exports.TOKEN_BUDGET.CACHE_TTL_MS; |
| }; |
| |
| |
| |
| |
| const mapRepositoryStructure = async (owner, repo, ref = 'HEAD') => { |
| const cacheKey = getCacheKey(owner, repo, ref); |
| |
| const cachedEntry = repoStructureCache.get(cacheKey); |
| if (cachedEntry && isCacheValid(cachedEntry)) { |
| console.log('[REPO CACHE] Using cached structure for', cacheKey); |
| return cachedEntry.structure; |
| } |
| console.log('[REPO CACHE] Cache miss or expired for', cacheKey, '- fetching from GitHub'); |
| const structure = { |
| folders: [], |
| files: [], |
| extensions: [], |
| packageJson: null, |
| frameworkHints: [], |
| rootFiles: [] |
| }; |
| try { |
| |
| const { data: tree } = await octokit_1.octokit.git.getTree({ |
| owner, |
| repo, |
| tree_sha: ref, |
| recursive: 'true' |
| }); |
| if (!tree.tree) { |
| return structure; |
| } |
| const extensions = new Set(); |
| const folders = new Set(); |
| for (const item of tree.tree) { |
| |
| if (shouldIgnorePath(item.path)) { |
| continue; |
| } |
| if (item.type === 'blob') { |
| structure.files.push(item.path); |
| |
| if (!item.path.includes('/')) { |
| structure.rootFiles.push(item.path); |
| } |
| |
| const ext = item.path.split('.').pop(); |
| if (ext && ext !== item.path) { |
| extensions.add(`.${ext}`); |
| } |
| |
| if (item.path === 'package.json') { |
| try { |
| const { data: file } = await octokit_1.octokit.repos.getContent({ |
| owner, |
| repo, |
| path: 'package.json', |
| ref |
| }); |
| if (!Array.isArray(file) && file.type === 'file' && file.content) { |
| structure.packageJson = JSON.parse(Buffer.from(file.content, 'base64').toString()); |
| structure.frameworkHints = detectFrameworkHints(structure.packageJson); |
| } |
| } |
| catch (e) { |
| |
| } |
| } |
| } |
| else if (item.type === 'tree') { |
| folders.add(item.path); |
| } |
| } |
| structure.extensions = Array.from(extensions); |
| structure.folders = Array.from(folders).slice(0, 100); |
| |
| repoStructureCache.set(cacheKey, { |
| structure, |
| timestamp: Date.now() |
| }); |
| console.log('[REPO CACHE] Cached structure for', cacheKey, 'with', structure.files.length, 'files'); |
| } |
| catch (error) { |
| console.error('Error mapping repository structure:', error.message); |
| } |
| return structure; |
| }; |
| exports.mapRepositoryStructure = mapRepositoryStructure; |
| |
| |
| |
| const clearRepoCache = (owner, repo) => { |
| if (owner && repo) { |
| |
| for (const key of repoStructureCache.keys()) { |
| if (key.startsWith(`${owner}/${repo}/`)) { |
| repoStructureCache.delete(key); |
| console.log('[REPO CACHE] Cleared cache for', key); |
| } |
| } |
| } |
| else { |
| |
| repoStructureCache.clear(); |
| console.log('[REPO CACHE] Cleared all cache'); |
| } |
| }; |
| exports.clearRepoCache = clearRepoCache; |
| |
| |
| |
| const detectFrameworkHints = (packageJson) => { |
| const hints = []; |
| const deps = { |
| ...packageJson.dependencies, |
| ...packageJson.devDependencies |
| }; |
| if (deps.react) |
| hints.push('React'); |
| if (deps.next) |
| hints.push('Next.js'); |
| if (deps.vue) |
| hints.push('Vue'); |
| if (deps.angular || deps['@angular/core']) |
| hints.push('Angular'); |
| if (deps.express) |
| hints.push('Express'); |
| if (deps['@nestjs/core'] || deps.nestjs) |
| hints.push('NestJS'); |
| if (deps.fastify) |
| hints.push('Fastify'); |
| if (deps.django || deps['djangorestframework']) |
| hints.push('Django'); |
| if (deps.flask) |
| hints.push('Flask'); |
| if (deps.rails) |
| hints.push('Rails'); |
| if (deps['@prisma/client'] || deps.prisma) |
| hints.push('Prisma'); |
| if (deps.typeorm) |
| hints.push('TypeORM'); |
| if (deps.mongoose) |
| hints.push('Mongoose'); |
| if (deps.sequelize) |
| hints.push('Sequelize'); |
| if (deps.drizzle) |
| hints.push('Drizzle'); |
| if (deps.postgres || deps.pg) |
| hints.push('PostgreSQL'); |
| if (deps.redis) |
| hints.push('Redis'); |
| if (deps.mongodb || deps.mongoose) |
| hints.push('MongoDB'); |
| return hints; |
| }; |
| |
| |
| |
| const shouldIgnorePath = (path) => { |
| |
| if ((0, generated_filter_1.isGeneratedFile)(path)) { |
| return true; |
| } |
| |
| for (const pattern of exports.SENSITIVE_FILE_PATTERNS) { |
| if (pattern.test(path)) { |
| return true; |
| } |
| } |
| |
| for (const pattern of exports.IGNORED_PATTERNS) { |
| if (pattern.test(path)) { |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| |
| |
| |
| |
| |
| const predictRelevantFiles = async (issueTitle, issueBody, structure, bot, limits) => { |
| try { |
| |
| const structureSummary = buildStructureSummary(structure); |
| const prompt = `You are an expert code reviewer. Given a GitHub issue and repository structure, identify the most relevant files for implementing a solution.
|
|
|
| ISSUE:
|
| Title: ${issueTitle}
|
| Body: ${issueBody}
|
|
|
| REPOSITORY STRUCTURE:
|
| ${structureSummary}
|
|
|
| FRAMEWORKS: ${structure.frameworkHints.join(', ') || 'Unknown'}
|
|
|
| TASK:
|
| Identify the most relevant files for this issue. Return a JSON array with:
|
| - path: file path
|
| - confidence: "high" | "medium" | "low"
|
| - reasoning: brief explanation
|
|
|
| Prioritize:
|
| 1. Files directly related to the issue
|
| 2. Core implementation files
|
| 3. Configuration/routing files
|
| 4. Test files (if relevant)
|
|
|
| Limit to ${limits.maxFiles} files maximum.
|
|
|
| Return ONLY valid JSON array, no markdown formatting.`; |
| const [response] = await bot.chat(prompt, { |
| max_tokens: exports.TOKEN_BUDGET.MAX_OUTPUT_TOKENS |
| }); |
| |
| console.log('TOKEN_USAGE', { |
| action: 'ISSUE_PLANNING', |
| model: bot.getCurrentModel(), |
| planningStage: 'file_prediction', |
| promptLength: prompt.length, |
| responseLength: response.length, |
| timestamp: new Date().toISOString() |
| }); |
| |
| const predictions = parseLLMPredictions(response); |
| |
| return predictions.slice(0, limits.maxFiles); |
| } |
| catch (error) { |
| console.error('Error predicting relevant files:', error.message); |
| return []; |
| } |
| }; |
| exports.predictRelevantFiles = predictRelevantFiles; |
| |
| |
| |
| const buildStructureSummary = (structure) => { |
| const lines = []; |
| lines.push(`Total files: ${structure.files.length}`); |
| lines.push(`Extensions: ${structure.extensions.slice(0, 10).join(', ')}`); |
| lines.push(`Main folders: ${structure.folders.slice(0, 15).join(', ')}`); |
| if (structure.rootFiles.length > 0) { |
| lines.push(`Root files: ${structure.rootFiles.join(', ')}`); |
| } |
| return lines.join('\n'); |
| }; |
| |
| |
| |
| const parseLLMPredictions = (response) => { |
| try { |
| |
| const jsonMatch = response.match(/\[[\s\S]*\]/); |
| if (!jsonMatch) { |
| return []; |
| } |
| const parsed = JSON.parse(jsonMatch[0]); |
| return Array.isArray(parsed) ? parsed : []; |
| } |
| catch (error) { |
| console.error('Error parsing LLM predictions:', error); |
| return []; |
| } |
| }; |
| |
| |
| |
| |
| |
| |
| const retrieveFiles = async (predictions, owner, repo, ref, limits) => { |
| const retrievedFiles = []; |
| let totalChars = 0; |
| |
| const sorted = predictions.sort((a, b) => { |
| const confidenceOrder = { high: 0, medium: 1, low: 2 }; |
| return confidenceOrder[a.confidence] - confidenceOrder[b.confidence]; |
| }); |
| for (const prediction of sorted) { |
| |
| if ((0, generated_filter_1.isGeneratedFile)(prediction.path)) { |
| (0, generated_filter_1.logIgnoredFile)(prediction.path, 0); |
| continue; |
| } |
| |
| if (retrievedFiles.length >= limits.maxFiles) { |
| console.log(`Reached max files limit: ${limits.maxFiles}`); |
| break; |
| } |
| if (totalChars >= limits.maxChars) { |
| console.log(`Reached max chars limit: ${limits.maxChars}`); |
| break; |
| } |
| try { |
| const { data: file } = await octokit_1.octokit.repos.getContent({ |
| owner, |
| repo, |
| path: prediction.path, |
| ref |
| }); |
| if (Array.isArray(file) || file.type !== 'file' || !file.content) { |
| continue; |
| } |
| const content = Buffer.from(file.content, 'base64').toString(); |
| const size = content.length; |
| |
| if ((0, generated_filter_1.isDiffTooLarge)(content)) { |
| console.log(`Diff too large for ${prediction.path}, skipping (${size} chars)`); |
| (0, generated_filter_1.logIgnoredFile)(prediction.path, size); |
| continue; |
| } |
| |
| if (totalChars + size > limits.maxChars) { |
| |
| const remainingChars = limits.maxChars - totalChars; |
| if (remainingChars > 1000) { |
| retrievedFiles.push({ |
| path: prediction.path, |
| content: content.substring(0, remainingChars), |
| size: remainingChars |
| }); |
| totalChars = limits.maxChars; |
| } |
| break; |
| } |
| retrievedFiles.push({ |
| path: prediction.path, |
| content, |
| size |
| }); |
| totalChars += size; |
| } |
| catch (error) { |
| console.error(`Error retrieving file ${prediction.path}:`, error.message); |
| } |
| } |
| |
| (0, generated_filter_1.logTokenSavings)(); |
| return retrievedFiles; |
| }; |
| exports.retrieveFiles = retrieveFiles; |
| |
| |
| |
| |
| |
| |
| const expandContext = async (files, owner, repo, ref, limits, structure) => { |
| if (!limits.enableContextExpansion) { |
| return files; |
| } |
| const expandedFiles = [...files]; |
| const importedPaths = new Set(); |
| let totalExpandedChars = 0; |
| |
| for (const file of files) { |
| const imports = extractImports(file.content, file.path); |
| for (const imp of imports) { |
| importedPaths.add(imp); |
| } |
| } |
| |
| for (const importPath of Array.from(importedPaths).slice(0, 2)) { |
| if (expandedFiles.length >= limits.maxFiles) |
| break; |
| |
| if (totalExpandedChars >= exports.TOKEN_BUDGET.MAX_EXPANDED_TOKENS) { |
| console.log('[CONTEXT EXPANSION] Reached MAX_EXPANDED_TOKENS limit'); |
| break; |
| } |
| const resolvedPath = resolveImportPath(importPath, structure); |
| if (!resolvedPath) |
| continue; |
| try { |
| const { data: file } = await octokit_1.octokit.repos.getContent({ |
| owner, |
| repo, |
| path: resolvedPath, |
| ref |
| }); |
| if (!Array.isArray(file) && file.type === 'file' && file.content) { |
| const content = Buffer.from(file.content, 'base64').toString(); |
| const fileSize = content.length; |
| |
| if (totalExpandedChars + fileSize > exports.TOKEN_BUDGET.MAX_EXPANDED_TOKENS) { |
| console.log('[CONTEXT EXPANSION] File would exceed token budget, skipping:', resolvedPath); |
| continue; |
| } |
| expandedFiles.push({ |
| path: resolvedPath, |
| content, |
| size: fileSize |
| }); |
| totalExpandedChars += fileSize; |
| console.log('[CONTEXT EXPANSION] Added file:', resolvedPath, 'size:', fileSize, 'total:', totalExpandedChars); |
| } |
| } |
| catch (error) { |
| |
| } |
| } |
| console.log('[CONTEXT EXPANSION] Summary', { |
| originalFiles: files.length, |
| expandedFiles: expandedFiles.length, |
| totalExpandedChars, |
| maxExpandedTokens: exports.TOKEN_BUDGET.MAX_EXPANDED_TOKENS |
| }); |
| return expandedFiles; |
| }; |
| exports.expandContext = expandContext; |
| |
| |
| |
| const extractImports = (content, filePath) => { |
| const imports = []; |
| const lines = content.split('\n'); |
| for (const line of lines) { |
| |
| const importMatch = line.match(/import.*from ['"]([^'"]+)['"]/); |
| if (importMatch) { |
| imports.push(importMatch[1]); |
| } |
| |
| const pythonMatch = line.match(/^(?:from|import)\s+(\S+)/); |
| if (pythonMatch) { |
| imports.push(pythonMatch[1]); |
| } |
| |
| const rubyMatch = line.match(/require\s+['"]([^'"]+)['"]/); |
| if (rubyMatch) { |
| imports.push(rubyMatch[1]); |
| } |
| } |
| return imports; |
| }; |
| |
| |
| |
| const resolveImportPath = (importPath, structure) => { |
| |
| if (importPath.startsWith('./') || importPath.startsWith('../')) { |
| return importPath; |
| } |
| |
| const candidates = structure.files.filter(f => f.endsWith(importPath) || |
| f.endsWith(`${importPath}.ts`) || |
| f.endsWith(`${importPath}.js`) || |
| f.endsWith(`${importPath}.tsx`) || |
| f.endsWith(`${importPath}.jsx`)); |
| return candidates[0] || null; |
| }; |
| |
| |
| |
| |
| |
| |
| const generateCombinedPromptFromPhases = (phases, understanding) => { |
| const lines = []; |
| lines.push('You are a senior full-stack developer. Your task is to implement the following issue.'); |
| lines.push(''); |
| lines.push('Issue Context:'); |
| lines.push(understanding); |
| lines.push(''); |
| lines.push('Please execute the implementation in the following phases:'); |
| lines.push(''); |
| phases.forEach((phase, index) => { |
| lines.push(`Phase ${index + 1}: ${phase.title}`); |
| lines.push(`Purpose: ${phase.purpose}`); |
| if (phase.tasks.length > 0) { |
| lines.push('Tasks:'); |
| phase.tasks.forEach(task => lines.push(`- ${task}`)); |
| } |
| lines.push(''); |
| }); |
| lines.push('Instructions:'); |
| lines.push('- Go through each phase systematically'); |
| lines.push('- Make the necessary code changes'); |
| lines.push('- Test your changes'); |
| lines.push('- Report your progress and any issues encountered'); |
| lines.push(''); |
| lines.push('Begin implementation now.'); |
| return lines.join('\n'); |
| }; |
| |
| |
| |
| |
| const formatPlanAsComment = (plan) => { |
| |
| if (plan.rawMarkdown) { |
| const header = '# 🧠 Implementation Plan\n\n'; |
| const footer = '\n\n---\n\n> If you are facing any issues, report to us at [support@prixai.xyz](mailto:support@prixai.xyz) or [https://www.prixai.xyz/feedback](https://www.prixai.xyz/feedback)\n\n*Generated by Prix AI — Autonomous Code Planning*\n\n🚀 [prixai.xyz](https://prixai.xyz)'; |
| |
| let processedMarkdown = plan.rawMarkdown; |
| |
| if (!processedMarkdown.includes('### Combined AI Prompt') && |
| plan.phases.length > 0) { |
| const combinedPrompt = generateCombinedPromptFromPhases(plan.phases, plan.understanding); |
| processedMarkdown += |
| '\n\n### Combined AI Prompt\n\n```\n' + combinedPrompt + '\n```'; |
| } |
| const formatted = header + processedMarkdown + footer; |
| console.log('FORMATTER_OUTPUT', { |
| mode: 'raw_markdown', |
| length: formatted.length, |
| rawLength: plan.rawMarkdown.length |
| }); |
| return formatted; |
| } |
| |
| const lines = []; |
| lines.push('# 🧠 Implementation Plan'); |
| lines.push(''); |
| |
| const complexityEmoji = plan.complexity === 'Low' |
| ? '🟢' |
| : plan.complexity === 'Medium' |
| ? '🟡' |
| : '🔴'; |
| lines.push(`**Complexity:** ${complexityEmoji} ${plan.complexity}`); |
| lines.push(''); |
| |
| lines.push('## 📊 Estimated Scope'); |
| lines.push(''); |
| lines.push('| Metric | Value |'); |
| lines.push('|--------|-------|'); |
| lines.push(`| Files Affected | ${plan.estimatedScope.filesAffected} |`); |
| lines.push(`| Effort | ${plan.estimatedScope.effort} |`); |
| lines.push(`| Risk Level | ${plan.estimatedScope.riskLevel} |`); |
| lines.push(''); |
| |
| lines.push('## 🎯 Understanding'); |
| lines.push(''); |
| lines.push(plan.understanding); |
| lines.push(''); |
| |
| if (plan.relevantAreas.length > 0) { |
| lines.push('## 📁 Relevant Areas'); |
| lines.push(''); |
| for (const area of plan.relevantAreas) { |
| lines.push(`- \`${area}\``); |
| } |
| lines.push(''); |
| } |
| |
| for (const phase of plan.phases) { |
| lines.push(`## 📋 Phase — ${phase.title}`); |
| lines.push(''); |
| lines.push(`**Purpose:** ${phase.purpose}`); |
| lines.push(''); |
| if (phase.tasks.length > 0) { |
| lines.push('**Tasks:**'); |
| for (const task of phase.tasks) { |
| lines.push(`- ${task}`); |
| } |
| lines.push(''); |
| } |
| if (phase.risks.length > 0) { |
| lines.push('**Risks:**'); |
| for (const risk of phase.risks) { |
| lines.push(`- ⚠️ ${risk}`); |
| } |
| lines.push(''); |
| } |
| } |
| |
| if (plan.testingChecklist.length > 0) { |
| lines.push('## ✅ Testing Checklist'); |
| lines.push(''); |
| for (const item of plan.testingChecklist) { |
| lines.push(`- [ ] ${item}`); |
| } |
| lines.push(''); |
| } |
| |
| if (plan.aiPrompts.length > 0) { |
| lines.push('## 🤖 Suggested AI Prompts'); |
| lines.push(''); |
| for (let i = 0; i < plan.aiPrompts.length; i++) { |
| lines.push(`### Prompt ${i + 1}`); |
| lines.push(''); |
| lines.push('```'); |
| lines.push(plan.aiPrompts[i]); |
| lines.push('```'); |
| lines.push(''); |
| } |
| } |
| |
| lines.push('---'); |
| lines.push(''); |
| lines.push('> If you are facing any issues, report to us at [support@prixai.xyz](mailto:support@prixai.xyz) or [https://www.prixai.xyz/feedback](https://www.prixai.xyz/feedback)'); |
| lines.push(''); |
| lines.push('*Generated by Prix AI — Autonomous Code Planning*'); |
| lines.push(''); |
| lines.push('🚀 [prixai.xyz](https://prixai.xyz)'); |
| const formatted = lines.join('\n'); |
| console.log('FORMATTER_OUTPUT', { |
| mode: 'legacy', |
| length: formatted.length, |
| phases: plan.phases.length |
| }); |
| return formatted; |
| }; |
| exports.formatPlanAsComment = formatPlanAsComment; |
| const generatePlan = async (options, bot) => { |
| |
| (0, generated_filter_1.resetFilterMetrics)(); |
| const startTime = Date.now(); |
| const limits = exports.TIER_LIMITS[options.tier]; |
| const metrics = { |
| filesRetrieved: 0, |
| tokenEstimate: 0, |
| retrievalRounds: 0, |
| executionDuration: 0, |
| tier: options.tier |
| }; |
| let retryCount = 0; |
| const maxRetries = exports.TOKEN_BUDGET.MAX_RETRIES; |
| let structure = null; |
| try { |
| |
| const issueMode = (0, exports.detectIssueMode)(options.issueTitle, options.issueBody); |
| console.log('[ISSUE MODE]', { mode: issueMode, title: options.issueTitle }); |
| |
| structure = await (0, exports.mapRepositoryStructure)(options.owner, options.repo, options.ref); |
| |
| let filteredPredictions; |
| |
| const isSimpleIssue = options.issueTitle.length < 120 && |
| structure.files.length < 50 && |
| !options.issueBody.toLowerCase().includes('architecture') && |
| !options.issueBody.toLowerCase().includes('complex'); |
| if (isSimpleIssue) { |
| console.log('[HEURISTIC RETRIEVAL] Using heuristic file selection for simple issue'); |
| const heuristicPaths = (0, exports.heuristicFileSelection)(options.issueTitle, options.issueBody, structure.files, issueMode); |
| filteredPredictions = heuristicPaths.map(path => ({ |
| path, |
| confidence: 'medium', |
| reasoning: 'Heuristic selection based on issue keywords' |
| })); |
| } |
| else { |
| console.log('[LLM RETRIEVAL] Using AI file selection for complex issue'); |
| const predictions = await (0, exports.predictRelevantFiles)(options.issueTitle, options.issueBody, structure, bot, limits); |
| filteredPredictions = predictions.slice(0, exports.TOKEN_BUDGET.MAX_RETRIEVED_FILES); |
| } |
| |
| const validPredictions = filteredPredictions.filter(prediction => structure && (0, exports.validateFileExistsInTree)(prediction.path, structure.files)); |
| |
| const predictionsToRetrieve = validPredictions.slice(0, exports.TOKEN_BUDGET.MAX_RETRIEVED_FILES); |
| console.log('[PARALLEL RETRIEVAL] Fetching', predictionsToRetrieve.length, 'files in parallel'); |
| |
| const retrievalPromises = predictionsToRetrieve.map(prediction => retrieveSingleFile(prediction.path, options.owner, options.repo, options.ref).catch(err => { |
| console.warn('[FILE RETRIEVAL ERROR]', { |
| path: prediction.path, |
| error: err.message |
| }); |
| return null; |
| })); |
| const results = await Promise.allSettled(retrievalPromises); |
| |
| const files = []; |
| for (const result of results) { |
| if (result.status === 'fulfilled' && result.value) { |
| const file = result.value; |
| |
| if (file.content.length > exports.TOKEN_BUDGET.MAX_FILE_CHARS) { |
| file.content = (0, exports.smartTruncate)(file.content, exports.TOKEN_BUDGET.MAX_FILE_CHARS); |
| } |
| files.push(file); |
| metrics.filesRetrieved++; |
| } |
| } |
| console.log('[PARALLEL RETRIEVAL] Successfully retrieved', files.length, 'files'); |
| metrics.retrievalRounds = 1; |
| |
| const fileSummaries = []; |
| for (const file of files) { |
| const summary = await (0, exports.generateFileSummary)(file.content, file.path); |
| fileSummaries.push(summary); |
| } |
| |
| const planningPrompt = (0, exports.generateAgenticPlanningPrompt)(options.issueTitle, options.issueBody, fileSummaries, issueMode); |
| |
| metrics.tokenEstimate = Math.ceil(planningPrompt.length / 3.5); |
| |
| let plan; |
| try { |
| plan = await generateImplementationPlanWithPrompt(planningPrompt, structure, bot, limits); |
| } |
| catch (planErr) { |
| console.error('[PLAN_GENERATION_RETRY]', { |
| error: planErr.message, |
| stack: planErr.stack, |
| attempt: retryCount + 1 |
| }); |
| if (retryCount < maxRetries) { |
| retryCount++; |
| console.warn('[RETRY] Retrying plan generation', { attempt: retryCount }); |
| |
| const relevantFilePaths = fileSummaries.map(f => f.path); |
| const fallbackContent = (0, exports.generateCompactFallback)(options.issueTitle, relevantFilePaths, issueMode); |
| plan = parseFallbackPlan(fallbackContent); |
| } |
| else { |
| |
| console.error('[PLAN GENERATION FAILED] Using compact fallback'); |
| const relevantFilePaths = fileSummaries.map(f => f.path); |
| const fallbackContent = (0, exports.generateCompactFallback)(options.issueTitle, relevantFilePaths, issueMode); |
| plan = parseFallbackPlan(fallbackContent); |
| } |
| } |
| metrics.executionDuration = Date.now() - startTime; |
| return { plan, metrics }; |
| } |
| catch (error) { |
| metrics.executionDuration = Date.now() - startTime; |
| console.error('[PLAN GENERATION ERROR]', { |
| error: error.message, |
| stack: error.stack |
| }); |
| |
| const relevantFilePaths = structure?.files?.slice(0, 5) || [ |
| 'src/', |
| 'components/', |
| 'lib/' |
| ]; |
| const issueMode = (0, exports.detectIssueMode)(options.issueTitle, options.issueBody); |
| const fallbackContent = (0, exports.generateCompactFallback)(options.issueTitle, relevantFilePaths, issueMode); |
| const plan = parseFallbackPlan(fallbackContent); |
| return { plan, metrics }; |
| } |
| }; |
| exports.generatePlan = generatePlan; |
| |
| |
| |
| const generateImplementationPlanWithPrompt = async (prompt, structure, bot, limits) => { |
| const [response] = await bot.chat(prompt, { |
| max_tokens: exports.TOKEN_BUDGET.MAX_OUTPUT_TOKENS |
| }); |
| |
| console.log('TOKEN_USAGE', { |
| action: 'ISSUE_PLANNING', |
| model: bot.getCurrentModel(), |
| planningStage: 'plan_generation', |
| promptLength: prompt.length, |
| responseLength: response.length, |
| timestamp: new Date().toISOString() |
| }); |
| |
| if (!response) { |
| console.log('RAW_AI_RESPONSE: Null response received'); |
| return { |
| understanding: 'Failed to generate plan due to null response', |
| phases: [], |
| estimatedScope: { filesAffected: 0, effort: 'Unknown', riskLevel: 'Low' }, |
| complexity: 'Medium', |
| relevantAreas: [], |
| testingChecklist: [], |
| aiPrompts: [] |
| }; |
| } |
| console.log('RAW_AI_RESPONSE', { |
| length: response.length, |
| preview: response.substring(0, 500) |
| }); |
| console.log('RAW_RESPONSE_FULL', response); |
| |
| const plan = parsePlanResponse(response); |
| console.log('PARSED_PLAN', { |
| phases: plan.phases.length, |
| understanding: plan.understanding.substring(0, 200) |
| }); |
| return plan; |
| }; |
| |
| |
| |
| const parsePlanResponse = (response) => { |
| const cleanedMarkdown = response |
| .trim() |
| .replace(/\r\n/g, '\n') |
| .replace(/\r/g, '\n') |
| .replace(/\n{3,}/g, '\n\n'); |
| console.log('RAW_MARKDOWN_PARSED', { |
| length: cleanedMarkdown.length, |
| preview: cleanedMarkdown.substring(0, 300) |
| }); |
| return { |
| complexity: 'Medium', |
| estimatedScope: { |
| filesAffected: 5, |
| effort: 'Medium', |
| riskLevel: 'Medium' |
| }, |
| understanding: cleanedMarkdown, |
| relevantAreas: [], |
| phases: [], |
| testingChecklist: [], |
| aiPrompts: [], |
| rawMarkdown: cleanedMarkdown |
| }; |
| }; |
| |
| |
| |
| const parseFallbackPlan = (fallbackContent) => { |
| const phase = { |
| title: 'Implementation', |
| purpose: 'Execute the suggested tasks', |
| tasks: [ |
| 'Analyze the issue requirements', |
| 'Identify necessary code changes', |
| 'Update affected files', |
| 'Test the implementation' |
| ], |
| risks: ['Potential for regressions', 'Breaking changes in dependencies'] |
| }; |
| const combinedPrompt = `You are a senior full-stack developer. Your task is to implement the following issue.
|
|
|
| Issue Context:
|
| ${fallbackContent}
|
|
|
| Phase 1: Implementation
|
| Purpose: Execute the suggested tasks
|
| Tasks:
|
| - Analyze the issue requirements
|
| - Identify necessary code changes
|
| - Update affected files
|
| - Test the implementation
|
|
|
| Instructions:
|
| - Go through each phase systematically
|
| - Make the necessary code changes
|
| - Test your changes
|
| - Report your progress and any issues encountered
|
|
|
| Begin implementation now.`; |
| const rawMarkdown = `## Understanding
|
|
|
| ${fallbackContent}
|
|
|
| ## Phase 1: Implementation
|
|
|
| **Purpose:** Execute the suggested tasks
|
|
|
| **Tasks:**
|
| - Analyze the issue requirements
|
| - Identify necessary code changes
|
| - Update affected files
|
| - Test the implementation
|
|
|
| **Risks:**
|
| - ⚠️ Potential for regressions
|
| - ⚠️ Breaking changes in dependencies
|
|
|
| ### Phase 1 AI Prompt
|
|
|
| \`\`\`
|
| You are a senior full-stack developer. Your task is to analyze the issue requirements and identify necessary code changes. Go through the codebase and list all files that need to be modified along with the specific changes required. Present your findings to the user in a clear, organized manner.
|
| \`\`\`
|
|
|
| ## Combined AI Prompt
|
|
|
| \`\`\`
|
| ${combinedPrompt}
|
| \`\`\`
|
|
|
| ## Testing Checklist
|
|
|
| - [ ] Verify the fix works as expected
|
| - [ ] Check for regressions
|
| - [ ] Test edge cases`; |
| return { |
| complexity: 'Low', |
| estimatedScope: { |
| filesAffected: 5, |
| effort: 'Medium', |
| riskLevel: 'Low' |
| }, |
| understanding: fallbackContent, |
| relevantAreas: [], |
| phases: [phase], |
| testingChecklist: [ |
| 'Verify the fix works as expected', |
| 'Check for regressions', |
| 'Test edge cases' |
| ], |
| aiPrompts: [ |
| 'Help me implement the changes described in the issue', |
| 'Review my implementation for any issues' |
| ], |
| rawMarkdown |
| }; |
| }; |
| |
| |
| |
| const retrieveSingleFile = async (path, owner, repo, ref) => { |
| const { data } = await octokit_1.octokit.repos.getContent({ |
| owner, |
| repo, |
| path, |
| ref |
| }); |
| const content = Buffer.from(data.content, 'base64').toString('utf-8'); |
| return { |
| path, |
| content, |
| size: content.length |
| }; |
| }; |
| |