Spaces:
Sleeping
Sleeping
| import logger from '../utils/logger.js'; | |
| class GitHubReactor { | |
| constructor(githubService, aiProvider, memory, developer, conversationSimulator, codeReviewEngine) { | |
| this.github = githubService; | |
| this.ai = aiProvider; | |
| this.memory = memory; | |
| this.developer = developer; | |
| this.conversation = conversationSimulator; | |
| this.reviewEngine = codeReviewEngine; | |
| this.lastActivity = {}; | |
| } | |
| async react() { | |
| logger.info('Starting human-like GitHub reaction cycle'); | |
| try { | |
| await this.developer.initialize(); | |
| } catch (e) { | |
| logger.warn(`Developer init warning: ${e.message}`); | |
| } | |
| const issues = await this._safeCall(() => this.github.listIssues('open'), []); | |
| const prs = await this._safeCall(() => this.github.listPullRequests('open'), []); | |
| let actions; | |
| if (issues.length === 0 && prs.length === 0) { | |
| logger.info('No existing issues or PRs, starting fresh workflow'); | |
| actions = [ | |
| { fn: () => this._createNewIssue(), weight: 0.8 }, | |
| { fn: () => this._createStarterPR(), weight: 0.2 }, | |
| ]; | |
| } else { | |
| actions = [ | |
| { fn: () => this._checkAndRespondToIssues(), weight: 0.15 }, | |
| { fn: () => this._checkAndReviewPRs(), weight: 0.2 }, | |
| { fn: () => this._createNewIssue(), weight: 0.1 }, | |
| { fn: () => this._workOnExistingIssue(), weight: 0.25 }, | |
| { fn: () => this._updateExistingPR(), weight: 0.1 }, | |
| { fn: () => this._mergeReadyPR(), weight: 0.1 }, | |
| { fn: () => this._discussOpenIssue(), weight: 0.1 }, | |
| ]; | |
| } | |
| const action = this._weightedRandom(actions); | |
| try { | |
| await action.fn(); | |
| } catch (error) { | |
| logger.error(`Action failed: ${error.message}`); | |
| } | |
| this.lastActivity.lastAction = Date.now(); | |
| } | |
| async _safeCall(fn, fallback) { | |
| try { | |
| return await fn(); | |
| } catch (error) { | |
| logger.error(`GitHub API call failed: ${error.message}`); | |
| return fallback; | |
| } | |
| } | |
| async _checkAndRespondToIssues() { | |
| logger.info('Checking for issues to respond to'); | |
| const issues = await this._safeCall(() => this.github.listIssues('open'), []); | |
| const unrespondedIssues = issues.filter(issue => | |
| !this.memory.has(`responded-issue-${issue.number}`) | |
| ); | |
| if (unrespondedIssues.length === 0) { | |
| logger.info('No new issues to respond to'); | |
| return; | |
| } | |
| const issue = unrespondedIssues[Math.floor(Math.random() * unrespondedIssues.length)]; | |
| try { | |
| const comments = await this._safeCall(() => this.github.getIssueComments(issue.number), []); | |
| const comment = await this.conversation.generateIssueComment(issue, comments); | |
| await this._safeCall(() => this.github.addIssueComment(issue.number, comment)); | |
| this.memory.remember(`responded-issue-${issue.number}`, { | |
| respondedAt: Date.now(), | |
| commentPreview: comment.slice(0, 100), | |
| }, { | |
| tags: ['discussion', 'issue'], | |
| issueTitle: issue.title, | |
| }); | |
| logger.info(`Responded to issue #${issue.number}`); | |
| } catch (error) { | |
| logger.error(`Failed to respond to issue #${issue.number}: ${error.message}`); | |
| } | |
| } | |
| async _checkAndReviewPRs() { | |
| logger.info('Checking for PRs to review'); | |
| const prs = await this._safeCall(() => this.github.listPullRequests('open'), []); | |
| const unreviewedPRs = prs.filter(pr => { | |
| const reviewData = this.memory.recallWithMetadata(`reviewed-pr-${pr.number}`); | |
| if (reviewData && reviewData.value) return false; | |
| const createdAt = new Date(pr.created_at).getTime(); | |
| const hoursSinceCreation = (Date.now() - createdAt) / (1000 * 60 * 60); | |
| if (hoursSinceCreation < 1) { | |
| logger.debug(`PR #${pr.number} is too new (${hoursSinceCreation.toFixed(1)}h), skipping review`); | |
| return false; | |
| } | |
| return true; | |
| }); | |
| if (unreviewedPRs.length === 0) { | |
| logger.info('No PRs ready for review'); | |
| return; | |
| } | |
| const pr = unreviewedPRs[Math.floor(Math.random() * unreviewedPRs.length)]; | |
| try { | |
| await this.developer.adaptRoleFromContext(pr.title, []); | |
| const comment = await this.conversation.generatePRComment(pr, [], []); | |
| const reviewDecision = await this.ai.generateReviewDecision(pr.title, pr.body); | |
| const reviewType = reviewDecision.includes('approve') ? 'approve' : | |
| reviewDecision.includes('request') ? 'request_changes' : 'comment'; | |
| if (reviewType === 'approve') { | |
| await this._safeCall(() => this.github.addPullRequestReview(pr.number, 'APPROVE', comment, pr.head?.sha)); | |
| logger.info(`✅ Approved PR #${pr.number}`); | |
| } else if (reviewType === 'request_changes') { | |
| await this._safeCall(() => this.github.addPullRequestReview(pr.number, 'REQUEST_CHANGES', comment, pr.head?.sha)); | |
| logger.info(`🔧 Requested changes on PR #${pr.number}`); | |
| } else { | |
| await this._safeCall(() => this.github.addPullRequestReview(pr.number, 'COMMENT', comment, pr.head?.sha)); | |
| logger.info(`💬 Commented on PR #${pr.number}`); | |
| } | |
| this.memory.remember(`reviewed-pr-${pr.number}`, { | |
| reviewType, | |
| reviewedAt: Date.now(), | |
| commentPreview: comment.slice(0, 100), | |
| }, { | |
| tags: ['review', 'pr'], | |
| prTitle: pr.title, | |
| }); | |
| } catch (error) { | |
| logger.error(`Failed to review PR #${pr.number}: ${error.message}`); | |
| } | |
| } | |
| async _createNewIssue() { | |
| logger.info('Creating new issue'); | |
| try { | |
| const projectContext = await this._getProjectContext(); | |
| const title = await this.ai.generateIssueTitle(projectContext); | |
| const body = await this.ai.generateIssueBody(title, projectContext); | |
| if (!title || title.length < 5) { | |
| logger.warn('Generated issue title too short, skipping'); | |
| return; | |
| } | |
| const labels = this._getRoleBasedLabels(); | |
| const issue = await this._safeCall(() => this.github.createIssue(title, body, labels)); | |
| if (issue) { | |
| this.memory.remember(`created-issue-${issue.number}`, { | |
| createdAt: Date.now(), | |
| title: issue.title, | |
| }, { | |
| tags: ['created', 'issue'], | |
| }); | |
| logger.info(`Created issue #${issue.number}: ${title}`); | |
| } | |
| } catch (error) { | |
| logger.error(`Failed to create issue: ${error.message}`); | |
| } | |
| } | |
| async _createStarterPR() { | |
| logger.info('Creating starter PR for project setup'); | |
| try { | |
| const branchName = 'chore/project-setup'; | |
| await this._safeCall(() => this.github.createBranch(branchName)); | |
| const files = [ | |
| { path: 'src/index.js', content: 'export { default } from "./app.js";\n' }, | |
| { path: 'src/app.js', content: 'class App {\n constructor() {\n this.name = "activity-simulator";\n }\n}\nexport default App;\n' }, | |
| { path: 'README.md', content: '# Activity Simulator\n\nHuman-like developer behavior simulator.\n' }, | |
| ]; | |
| for (const file of files) { | |
| await this._safeCall(() => | |
| this.github.createOrUpdateFile(file.path, file.content, `chore: add ${file.path}`, branchName) | |
| ); | |
| } | |
| const pr = await this._safeCall(() => | |
| this.github.createPullRequest( | |
| 'chore: initial project setup', | |
| '## Summary\n\nInitial project structure with basic files.\n\n## Files Added\n\n- `src/index.js` - Main entry point\n- `src/app.js` - Application class\n- `README.md` - Project documentation', | |
| branchName | |
| ) | |
| ); | |
| if (pr) { | |
| logger.info(`Created starter PR #${pr.number}`); | |
| } | |
| } catch (error) { | |
| logger.error(`Failed to create starter PR: ${error.message}`); | |
| } | |
| } | |
| async _workOnExistingIssue() { | |
| logger.info('Working on existing issue'); | |
| const issues = await this._safeCall(() => this.github.listIssues('open'), []); | |
| const workableIssues = issues.filter(issue => | |
| !this.memory.has(`working-on-${issue.number}`) && | |
| !issue.labels?.some(l => l.name === 'in-progress') | |
| ); | |
| if (workableIssues.length === 0) { | |
| logger.info('No workable issues found'); | |
| return; | |
| } | |
| const issue = workableIssues[Math.floor(Math.random() * workableIssues.length)]; | |
| const cleanTitle = this._cleanTitle(issue.title); | |
| try { | |
| await this.developer.adaptRoleFromContext(cleanTitle, []); | |
| await this._safeCall(() => this.github.addLabels(issue.number, ['in-progress'])); | |
| const branchName = this._generateBranchName(cleanTitle); | |
| await this._safeCall(() => this.github.createBranch(branchName)); | |
| const action = this.developer.getRoleBasedAction(); | |
| const files = this._generateMultipleFiles(action, cleanTitle); | |
| for (let i = 0; i < files.length; i++) { | |
| const file = files[i]; | |
| const commitMsg = i === 0 | |
| ? `${this._getCommitPrefix(cleanTitle)}: implement ${cleanTitle.toLowerCase()}` | |
| : `${this._getCommitPrefix(cleanTitle)}: add ${file.description}`; | |
| await this._safeCall(() => | |
| this.github.createOrUpdateFile(file.path, file.content, commitMsg, branchName) | |
| ); | |
| logger.info(`Created file ${i + 1}/${files.length}: ${file.path}`); | |
| } | |
| const prTitle = `${this._getCommitPrefix(cleanTitle)}: ${this._titleToSentence(cleanTitle)}`; | |
| const prBody = this._generateDetailedPRBody(issue, cleanTitle, files); | |
| const pr = await this._safeCall(() => | |
| this.github.createPullRequest(prTitle, prBody, branchName) | |
| ); | |
| if (pr) { | |
| this.memory.remember(`working-on-${issue.number}`, { | |
| branch: branchName, | |
| prNumber: pr.number, | |
| startedAt: Date.now(), | |
| fileCount: files.length, | |
| }, { | |
| tags: ['active-issue', 'open-pr'], | |
| }); | |
| logger.info(`Created PR #${pr.number} with ${files.length} files for issue #${issue.number}`); | |
| } | |
| } catch (error) { | |
| logger.error(`Failed to work on issue #${issue.number}: ${error.message}`); | |
| } | |
| } | |
| async _updateExistingPR() { | |
| logger.info('Checking for PRs that need updates'); | |
| const prs = await this._safeCall(() => this.github.listPullRequests('open'), []); | |
| const prsNeedingChanges = prs.filter(pr => { | |
| const reviewData = this.memory.recallWithMetadata(`reviewed-pr-${pr.number}`); | |
| return reviewData && reviewData.value?.reviewType === 'request_changes'; | |
| }); | |
| if (prsNeedingChanges.length === 0) { | |
| logger.info('No PRs need updates'); | |
| return; | |
| } | |
| const pr = prsNeedingChanges[0]; | |
| try { | |
| const reviewData = this.memory.recallWithMetadata(`reviewed-pr-${pr.number}`); | |
| const files = await this._safeCall(() => this.github.getPullRequestFiles(pr.number), []); | |
| if (files.length === 0) return; | |
| const file = files[Math.floor(Math.random() * files.length)]; | |
| const codeContent = await this._generateFixCode(file, reviewData); | |
| await this._safeCall(() => | |
| this.github.createOrUpdateFile( | |
| file.filename, | |
| codeContent, | |
| 'fix: address review feedback', | |
| pr.head?.ref || 'main' | |
| ) | |
| ); | |
| const followUp = await this.conversation.generateFollowUpComment(pr, reviewData.value?.commentPreview || 'review feedback'); | |
| await this._safeCall(() => this.github.addPullRequestComment(pr.number, followUp)); | |
| this.memory.remember(`reviewed-pr-${pr.number}`, { | |
| ...reviewData.value, | |
| reviewType: 'addressed', | |
| addressedAt: Date.now(), | |
| }); | |
| logger.info(`Updated PR #${pr.number} with review feedback`); | |
| } catch (error) { | |
| logger.error(`Failed to update PR #${pr.number}: ${error.message}`); | |
| } | |
| } | |
| async _mergeReadyPR() { | |
| logger.info('Checking for PRs ready to merge'); | |
| const prs = await this._safeCall(() => this.github.listPullRequests('open'), []); | |
| const approvedPRs = prs.filter(pr => { | |
| const reviewData = this.memory.recallWithMetadata(`reviewed-pr-${pr.number}`); | |
| return reviewData && reviewData.value?.reviewType === 'approve'; | |
| }); | |
| if (approvedPRs.length === 0) { | |
| logger.info('No approved PRs to merge'); | |
| return; | |
| } | |
| const pr = approvedPRs[Math.floor(Math.random() * approvedPRs.length)]; | |
| try { | |
| await this._safeCall(() => this.github.mergePullRequest(pr.number)); | |
| logger.info(`🎉 Merged PR #${pr.number}`); | |
| if (pr.head?.ref) { | |
| await this._safeCall(() => this.github.deleteBranch(pr.head.ref)); | |
| logger.info(`🗑️ Deleted branch: ${pr.head.ref}`); | |
| } | |
| const issueMatch = pr.body?.match(/Closes #(\d+)/i); | |
| if (issueMatch) { | |
| const issueNumber = parseInt(issueMatch[1]); | |
| await this._safeCall(() => this.github.closeIssue(issueNumber)); | |
| logger.info(`✅ Closed issue #${issueNumber} after PR merged`); | |
| } | |
| this.memory.forget(`reviewed-pr-${pr.number}`); | |
| this.memory.forget(`open-pr-${pr.number}`); | |
| } catch (error) { | |
| logger.error(`Failed to merge PR #${pr.number}: ${error.message}`); | |
| } | |
| } | |
| async _discussOpenIssue() { | |
| logger.info('Starting discussion on open issue'); | |
| const issues = await this._safeCall(() => this.github.listIssues('open'), []); | |
| const discussableIssues = issues.filter(issue => | |
| !this.memory.has(`discussed-issue-${issue.number}`) | |
| ); | |
| if (discussableIssues.length === 0) { | |
| logger.info('No issues to discuss'); | |
| return; | |
| } | |
| const issue = discussableIssues[Math.floor(Math.random() * discussableIssues.length)]; | |
| try { | |
| const comments = await this._safeCall(() => this.github.getIssueComments(issue.number), []); | |
| if (comments.length === 0) { | |
| const clarification = await this.conversation.generateIssueClarification(issue); | |
| await this._safeCall(() => this.github.addIssueComment(issue.number, clarification)); | |
| } else { | |
| const discussion = await this.conversation.generateIssueComment(issue, comments); | |
| await this._safeCall(() => this.github.addIssueComment(issue.number, discussion)); | |
| } | |
| this.memory.remember(`discussed-issue-${issue.number}`, { | |
| discussedAt: Date.now(), | |
| }, { | |
| tags: ['discussion', 'issue'], | |
| }); | |
| logger.info(`Discussed issue #${issue.number}`); | |
| } catch (error) { | |
| logger.error(`Failed to discuss issue #${issue.number}: ${error.message}`); | |
| } | |
| } | |
| _generateMultipleFiles(action, issueTitle) { | |
| const files = []; | |
| const fileSets = { | |
| 'add-feature': [ | |
| { path: `src/features/${this._slugify(issueTitle)}.js`, description: 'main feature implementation', content: this._generateFeatureCode(issueTitle) }, | |
| { path: `src/features/${this._slugify(issueTitle)}.test.js`, description: 'unit tests', content: this._generateTestCode(issueTitle) }, | |
| { path: `docs/features/${this._slugify(issueTitle)}.md`, description: 'documentation', content: this._generateDocs(issueTitle) }, | |
| ], | |
| 'fix-bug': [ | |
| { path: `src/fixes/${this._slugify(issueTitle)}.js`, description: 'bug fix implementation', content: this._generateFixCode(issueTitle) }, | |
| { path: `tests/${this._slugify(issueTitle)}.test.js`, description: 'regression test', content: this._generateTestCode(issueTitle) }, | |
| ], | |
| 'add-api-endpoint': [ | |
| { path: `src/api/routes/${this._slugify(issueTitle)}.js`, description: 'API route handler', content: this._generateAPICode(issueTitle) }, | |
| { path: `src/api/middleware/${this._slugify(issueTitle)}.js`, description: 'request validation', content: this._generateMiddlewareCode(issueTitle) }, | |
| { path: `docs/api/${this._slugify(issueTitle)}.md`, description: 'API documentation', content: this._generateDocs(issueTitle) }, | |
| ], | |
| 'add-tests': [ | |
| { path: `tests/unit/${this._slugify(issueTitle)}.test.js`, description: 'unit tests', content: this._generateTestCode(issueTitle) }, | |
| { path: `tests/integration/${this._slugify(issueTitle)}.test.js`, description: 'integration tests', content: this._generateIntegrationTestCode(issueTitle) }, | |
| ], | |
| 'update-docs': [ | |
| { path: `docs/${this._slugify(issueTitle)}.md`, description: 'documentation update', content: this._generateDocs(issueTitle) }, | |
| { path: 'README.md', description: 'README update', content: this._generateREADMEUpdate(issueTitle) }, | |
| ], | |
| }; | |
| const matchedSet = fileSets[action] || fileSets['add-feature']; | |
| return matchedSet.slice(0, Math.min(matchedSet.length, 2 + Math.floor(Math.random() * 2))); | |
| } | |
| _generateFeatureCode(title) { | |
| return `/** | |
| * ${title} | |
| * Auto-generated implementation | |
| */ | |
| class ${this._className(title)} { | |
| constructor(options = {}) { | |
| this.enabled = options.enabled ?? true; | |
| this.config = options.config || {}; | |
| } | |
| async execute(input) { | |
| if (!this.enabled) { | |
| throw new Error('Feature is disabled'); | |
| } | |
| const validated = this._validate(input); | |
| const result = await this._process(validated); | |
| return this._format(result); | |
| } | |
| _validate(input) { | |
| if (!input) { | |
| throw new Error('Input is required'); | |
| } | |
| return input; | |
| } | |
| async _process(input) { | |
| return { processed: true, timestamp: Date.now(), data: input }; | |
| } | |
| _format(result) { | |
| return { success: true, data: result }; | |
| } | |
| } | |
| export default ${this._className(title)}; | |
| `; | |
| } | |
| _generateTestCode(title) { | |
| return `import { describe, it, expect } from 'vitest'; | |
| import ${this._className(title)} from '../src/features/${this._slugify(title)}.js'; | |
| describe('${this._className(title)}', () => { | |
| it('should initialize with default options', () => { | |
| const instance = new ${this._className(title)}(); | |
| expect(instance.enabled).toBe(true); | |
| }); | |
| it('should process valid input', async () => { | |
| const instance = new ${this._className(title)}(); | |
| const result = await instance.execute({ test: 'data' }); | |
| expect(result.success).toBe(true); | |
| }); | |
| it('should throw error for disabled feature', async () => { | |
| const instance = new ${this._className(title)}({ enabled: false }); | |
| await expect(instance.execute({})).rejects.toThrow('Feature is disabled'); | |
| }); | |
| it('should throw error for null input', async () => { | |
| const instance = new ${this._className(title)}(); | |
| await expect(instance.execute(null)).rejects.toThrow('Input is required'); | |
| }); | |
| }); | |
| `; | |
| } | |
| _generateAPICode(title) { | |
| return `import express from 'express'; | |
| const router = express.Router(); | |
| /** | |
| * ${title} | |
| * @route GET /api/${this._slugify(title)} | |
| */ | |
| router.get('/${this._slugify(title)}', async (req, res) => { | |
| try { | |
| const result = await process${this._className(title)}(req.query); | |
| res.json({ success: true, data: result }); | |
| } catch (error) { | |
| res.status(400).json({ success: false, error: error.message }); | |
| } | |
| }); | |
| async function process${this._className(title)}(query) { | |
| return { processed: true, query }; | |
| } | |
| export default router; | |
| `; | |
| } | |
| _generateMiddlewareCode(title) { | |
| return `/** | |
| * Validation middleware for ${title} | |
| */ | |
| export function validate${this._className(title)}(req, res, next) { | |
| const { input } = req.body; | |
| if (!input) { | |
| return res.status(400).json({ error: 'Input is required' }); | |
| } | |
| req.validatedInput = input; | |
| next(); | |
| } | |
| `; | |
| } | |
| _generateFixCode(title) { | |
| return `/** | |
| * Fix for: ${title} | |
| */ | |
| export function fix${this._className(title)}(data) { | |
| if (!data || typeof data !== 'object') { | |
| return { error: 'Invalid data format' }; | |
| } | |
| const sanitized = Object.entries(data) | |
| .filter(([_, v]) => v !== null && v !== undefined) | |
| .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}); | |
| return { fixed: true, data: sanitized }; | |
| } | |
| `; | |
| } | |
| _generateIntegrationTestCode(title) { | |
| return `import { describe, it, expect } from 'vitest'; | |
| describe('${this._className(title)} Integration', () => { | |
| it('should work end-to-end', async () => { | |
| const response = await fetch('/api/${this._slugify(title)}', { | |
| method: 'GET', | |
| headers: { 'Content-Type': 'application/json' }, | |
| }); | |
| expect(response.status).toBe(200); | |
| const data = await response.json(); | |
| expect(data.success).toBe(true); | |
| }); | |
| }); | |
| `; | |
| } | |
| _generateDocs(title) { | |
| return `# ${title} | |
| ## Overview | |
| This feature implements ${title.toLowerCase()}. | |
| ## Usage | |
| \`\`\`javascript | |
| import ${this._className(title)} from './features/${this._slugify(title)}.js'; | |
| const instance = new ${this._className(title)}(); | |
| const result = await instance.execute({ key: 'value' }); | |
| \`\`\` | |
| ## API Reference | |
| | Method | Description | | |
| |--------|-------------| | |
| | \`execute(input)\` | Process the input and return result | | |
| ## Examples | |
| See tests for usage examples. | |
| `; | |
| } | |
| _generateREADMEUpdate(title) { | |
| return `# Project | |
| ## Features | |
| - ${title} | |
| `; | |
| } | |
| _generateDetailedPRBody(issue, cleanTitle, files) { | |
| const fileList = files.map(f => `- \`${f.path}\` - ${f.description}`).join('\n'); | |
| return `## ${cleanTitle} | |
| **Closes:** #${issue.number} | |
| ### What | |
| Implemented ${cleanTitle.toLowerCase()} as described in the issue. | |
| ### Changes | |
| ${fileList} | |
| ### Testing | |
| - [x] Manual testing completed | |
| - [x] Unit tests added | |
| - [x] Integration tests pass | |
| ### Notes | |
| Ready for review. All tests pass locally. | |
| `; | |
| } | |
| _generatePRBody(issue, branchName) { | |
| const roleContext = this.developer.getRoleContext(); | |
| return `## ${issue.title} | |
| **Closes:** #${issue.number} | |
| ### What | |
| Implemented ${issue.title.toLowerCase()} as discussed in the issue. | |
| ### Why | |
| This improves the overall ${roleContext.focus[0]} of the project. | |
| ### Testing | |
| - [ ] Manual testing | |
| - [ ] Unit tests pass | |
| ### Notes | |
| ${roleContext.phrases?.[Math.floor(Math.random() * roleContext.phrases.length)] || 'Ready for review.'}`; | |
| } | |
| async _getProjectContext() { | |
| try { | |
| const commits = await this._safeCall(() => this.github.getCommitHistory('main', 5), []); | |
| const messages = commits.map(c => c.commit.message).join(', '); | |
| return messages ? `Recent commits: ${messages}` : 'JavaScript/TypeScript project'; | |
| } catch { | |
| return 'JavaScript/TypeScript project'; | |
| } | |
| } | |
| _generateBranchName(issueTitle) { | |
| const sanitized = issueTitle | |
| .toLowerCase() | |
| .replace(/[^a-z0-9\s-]/g, '') | |
| .replace(/\s+/g, '-') | |
| .replace(/-+/g, '-') | |
| .slice(0, 50); | |
| const prefix = this._getCommitPrefix(issueTitle); | |
| return `${prefix}/${sanitized}`; | |
| } | |
| _getCommitPrefix(title) { | |
| const cleanTitle = title.replace(/^(feat|fix|docs|refactor|test|chore):\s*/i, '').trim(); | |
| const lower = cleanTitle.toLowerCase(); | |
| if (lower.startsWith('fix ') || lower.includes('bug') || lower.includes('error') || lower.includes('race condition') || lower.includes('circular dependency')) return 'fix'; | |
| if (lower.startsWith('add ') || lower.startsWith('implement ') || lower.includes('support') || lower.includes('new')) return 'feat'; | |
| if (lower.startsWith('refactor') || lower.includes('clean') || lower.includes('improve')) return 'refactor'; | |
| if (lower.startsWith('test') || lower.includes('coverage')) return 'test'; | |
| if (lower.startsWith('doc') || lower.includes('readme')) return 'docs'; | |
| return 'feat'; | |
| } | |
| _cleanTitle(title) { | |
| let cleaned = title.trim(); | |
| while (/^(feat|fix|docs|refactor|test|chore):\s*/i.test(cleaned)) { | |
| cleaned = cleaned.replace(/^(feat|fix|docs|refactor|test|chore):\s*/i, '').trim(); | |
| } | |
| cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1); | |
| return cleaned; | |
| } | |
| _titleToSentence(title) { | |
| const cleaned = this._cleanTitle(title); | |
| const lower = cleaned.toLowerCase(); | |
| let sentence = cleaned; | |
| if (lower.startsWith('fix ') || lower.startsWith('add ') || lower.startsWith('implement ') || | |
| lower.startsWith('update ') || lower.startsWith('refactor ') || lower.startsWith('improve ')) { | |
| sentence = cleaned.charAt(0).toLowerCase() + cleaned.slice(1); | |
| } | |
| return sentence; | |
| } | |
| _slugify(text) { | |
| return text.toLowerCase() | |
| .replace(/[^a-z0-9\s-]/g, '') | |
| .replace(/\s+/g, '-') | |
| .replace(/-+/g, '-') | |
| .slice(0, 40) || 'feature'; | |
| } | |
| _className(text) { | |
| return this._slugify(text) | |
| .split('-') | |
| .map(word => word.charAt(0).toUpperCase() + word.slice(1)) | |
| .join('') || 'Feature'; | |
| } | |
| _getRoleBasedLabels() { | |
| const roleContext = this.developer.getRoleContext(); | |
| const labels = []; | |
| if (roleContext.focus.some(f => f.includes('security') || f.includes('vulnerability'))) { | |
| labels.push('security'); | |
| } | |
| if (roleContext.focus.some(f => f.includes('performance') || f.includes('optimize'))) { | |
| labels.push('performance'); | |
| } | |
| if (roleContext.focus.some(f => f.includes('UI') || f.includes('design'))) { | |
| labels.push('frontend'); | |
| } | |
| if (roleContext.focus.some(f => f.includes('API') || f.includes('database'))) { | |
| labels.push('backend'); | |
| } | |
| if (labels.length === 0) { | |
| labels.push('enhancement'); | |
| } | |
| return labels; | |
| } | |
| _weightedRandom(actions) { | |
| const totalWeight = actions.reduce((sum, a) => sum + a.weight, 0); | |
| let random = Math.random() * totalWeight; | |
| for (const action of actions) { | |
| random -= action.weight; | |
| if (random <= 0) { | |
| return action; | |
| } | |
| } | |
| return actions[actions.length - 1]; | |
| } | |
| } | |
| export default GitHubReactor; | |