| | "use strict"; |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | Object.defineProperty(exports, "__esModule", { value: true }); |
| | exports.parseDiff = parseDiff; |
| | exports.classifyChange = classifyChange; |
| | exports.calculateRiskScore = calculateRiskScore; |
| | exports.analyzeFileDiff = analyzeFileDiff; |
| | exports.getCommitDiff = getCommitDiff; |
| | exports.getStagedDiff = getStagedDiff; |
| | exports.getUnstagedDiff = getUnstagedDiff; |
| | exports.analyzeCommit = analyzeCommit; |
| | exports.findSimilarCommits = findSimilarCommits; |
| | const child_process_1 = require("child_process"); |
| | const onnx_embedder_1 = require("./onnx-embedder"); |
| | |
| | |
| | |
| | function parseDiff(diff) { |
| | const hunks = []; |
| | const lines = diff.split('\n'); |
| | let currentFile = ''; |
| | let currentHunk = null; |
| | for (const line of lines) { |
| | |
| | if (line.startsWith('diff --git')) { |
| | const match = line.match(/diff --git a\/(.+) b\/(.+)/); |
| | if (match) { |
| | currentFile = match[2]; |
| | } |
| | } |
| | |
| | if (line.startsWith('@@')) { |
| | if (currentHunk) { |
| | hunks.push(currentHunk); |
| | } |
| | const match = line.match(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/); |
| | if (match) { |
| | currentHunk = { |
| | file: currentFile, |
| | oldStart: parseInt(match[1]), |
| | oldLines: parseInt(match[2] || '1'), |
| | newStart: parseInt(match[3]), |
| | newLines: parseInt(match[4] || '1'), |
| | content: '', |
| | additions: [], |
| | deletions: [], |
| | }; |
| | } |
| | } |
| | else if (currentHunk) { |
| | |
| | if (line.startsWith('+') && !line.startsWith('+++')) { |
| | currentHunk.additions.push(line.substring(1)); |
| | currentHunk.content += line + '\n'; |
| | } |
| | else if (line.startsWith('-') && !line.startsWith('---')) { |
| | currentHunk.deletions.push(line.substring(1)); |
| | currentHunk.content += line + '\n'; |
| | } |
| | else if (line.startsWith(' ')) { |
| | currentHunk.content += line + '\n'; |
| | } |
| | } |
| | } |
| | if (currentHunk) { |
| | hunks.push(currentHunk); |
| | } |
| | return hunks; |
| | } |
| | |
| | |
| | |
| | function classifyChange(diff, message = '') { |
| | const lowerMessage = message.toLowerCase(); |
| | const lowerDiff = diff.toLowerCase(); |
| | |
| | if (/\b(fix|bug|issue|error|crash|patch)\b/.test(lowerMessage)) |
| | return 'bugfix'; |
| | if (/\b(feat|feature|add|new|implement)\b/.test(lowerMessage)) |
| | return 'feature'; |
| | if (/\b(refactor|clean|improve|optimize)\b/.test(lowerMessage)) |
| | return 'refactor'; |
| | if (/\b(doc|readme|comment|jsdoc)\b/.test(lowerMessage)) |
| | return 'docs'; |
| | if (/\b(test|spec|coverage)\b/.test(lowerMessage)) |
| | return 'test'; |
| | if (/\b(config|ci|cd|build|deps)\b/.test(lowerMessage)) |
| | return 'config'; |
| | |
| | if (/\.(md|txt|rst)$/.test(diff)) |
| | return 'docs'; |
| | if (/\.(test|spec)\.[jt]sx?/.test(diff)) |
| | return 'test'; |
| | if (/\.(json|ya?ml|toml|ini)$/.test(diff)) |
| | return 'config'; |
| | |
| | if (/\bcatch\b|\btry\b|\berror\b/.test(lowerDiff) && /\bfix\b/.test(lowerDiff)) |
| | return 'bugfix'; |
| | if (/\bfunction\b|\bclass\b|\bexport\b/.test(lowerDiff)) |
| | return 'feature'; |
| | return 'unknown'; |
| | } |
| | |
| | |
| | |
| | function calculateRiskScore(analysis) { |
| | let risk = 0; |
| | |
| | const totalChanges = analysis.totalAdditions + analysis.totalDeletions; |
| | if (totalChanges > 500) |
| | risk += 0.3; |
| | else if (totalChanges > 200) |
| | risk += 0.2; |
| | else if (totalChanges > 50) |
| | risk += 0.1; |
| | |
| | if (analysis.complexity > 20) |
| | risk += 0.2; |
| | else if (analysis.complexity > 10) |
| | risk += 0.1; |
| | |
| | if (analysis.file.includes('auth') || analysis.file.includes('security')) |
| | risk += 0.2; |
| | if (analysis.file.includes('database') || analysis.file.includes('migration')) |
| | risk += 0.15; |
| | if (analysis.file.includes('api') || analysis.file.includes('endpoint')) |
| | risk += 0.1; |
| | |
| | for (const hunk of analysis.hunks) { |
| | for (const del of hunk.deletions) { |
| | if (/\bcatch\b|\berror\b|\bvalidat/.test(del)) |
| | risk += 0.1; |
| | if (/\bif\b.*\bnull\b|\bundefined\b/.test(del)) |
| | risk += 0.05; |
| | } |
| | } |
| | return Math.min(1, risk); |
| | } |
| | |
| | |
| | |
| | async function analyzeFileDiff(file, diff, message = '') { |
| | const hunks = parseDiff(diff).filter(h => h.file === file || h.file === ''); |
| | const totalAdditions = hunks.reduce((sum, h) => sum + h.additions.length, 0); |
| | const totalDeletions = hunks.reduce((sum, h) => sum + h.deletions.length, 0); |
| | |
| | let complexity = 0; |
| | for (const hunk of hunks) { |
| | for (const add of hunk.additions) { |
| | if (/\bif\b|\belse\b|\bfor\b|\bwhile\b|\bswitch\b|\bcatch\b|\?/.test(add)) { |
| | complexity++; |
| | } |
| | } |
| | } |
| | const category = classifyChange(diff, message); |
| | const analysis = { |
| | file, |
| | hunks, |
| | totalAdditions, |
| | totalDeletions, |
| | complexity, |
| | riskScore: 0, |
| | category, |
| | }; |
| | analysis.riskScore = calculateRiskScore(analysis); |
| | |
| | if ((0, onnx_embedder_1.isReady)()) { |
| | const diffText = hunks.map(h => h.content).join('\n'); |
| | const result = await (0, onnx_embedder_1.embed)(`${category} change in ${file}: ${diffText.substring(0, 500)}`); |
| | analysis.embedding = result.embedding; |
| | } |
| | return analysis; |
| | } |
| | |
| | |
| | |
| | function getCommitDiff(commitHash = 'HEAD') { |
| | try { |
| | return (0, child_process_1.execSync)(`git show ${commitHash} --format="" 2>/dev/null`, { |
| | encoding: 'utf8', |
| | maxBuffer: 10 * 1024 * 1024, |
| | }); |
| | } |
| | catch { |
| | return ''; |
| | } |
| | } |
| | |
| | |
| | |
| | function getStagedDiff() { |
| | try { |
| | return (0, child_process_1.execSync)('git diff --cached 2>/dev/null', { |
| | encoding: 'utf8', |
| | maxBuffer: 10 * 1024 * 1024, |
| | }); |
| | } |
| | catch { |
| | return ''; |
| | } |
| | } |
| | |
| | |
| | |
| | function getUnstagedDiff() { |
| | try { |
| | return (0, child_process_1.execSync)('git diff 2>/dev/null', { |
| | encoding: 'utf8', |
| | maxBuffer: 10 * 1024 * 1024, |
| | }); |
| | } |
| | catch { |
| | return ''; |
| | } |
| | } |
| | |
| | |
| | |
| | async function analyzeCommit(commitHash = 'HEAD') { |
| | const diff = getCommitDiff(commitHash); |
| | |
| | let message = '', author = '', date = ''; |
| | try { |
| | const info = (0, child_process_1.execSync)(`git log -1 --format="%s|%an|%aI" ${commitHash} 2>/dev/null`, { |
| | encoding: 'utf8', |
| | }).trim(); |
| | [message, author, date] = info.split('|'); |
| | } |
| | catch { } |
| | |
| | const hunks = parseDiff(diff); |
| | const fileHunks = new Map(); |
| | for (const hunk of hunks) { |
| | if (!fileHunks.has(hunk.file)) { |
| | fileHunks.set(hunk.file, []); |
| | } |
| | fileHunks.get(hunk.file).push(hunk); |
| | } |
| | |
| | const files = []; |
| | for (const [file, fileHunkList] of fileHunks) { |
| | const fileDiff = fileHunkList.map(h => h.content).join('\n'); |
| | const analysis = await analyzeFileDiff(file, diff, message); |
| | files.push(analysis); |
| | } |
| | const totalAdditions = files.reduce((sum, f) => sum + f.totalAdditions, 0); |
| | const totalDeletions = files.reduce((sum, f) => sum + f.totalDeletions, 0); |
| | const riskScore = files.length > 0 |
| | ? files.reduce((sum, f) => sum + f.riskScore, 0) / files.length |
| | : 0; |
| | |
| | let embedding; |
| | if ((0, onnx_embedder_1.isReady)()) { |
| | const commitText = `${message}\n\nFiles changed: ${files.map(f => f.file).join(', ')}\n+${totalAdditions} -${totalDeletions}`; |
| | const result = await (0, onnx_embedder_1.embed)(commitText); |
| | embedding = result.embedding; |
| | } |
| | return { |
| | hash: commitHash, |
| | message, |
| | author, |
| | date, |
| | files, |
| | totalAdditions, |
| | totalDeletions, |
| | riskScore, |
| | embedding, |
| | }; |
| | } |
| | |
| | |
| | |
| | async function findSimilarCommits(currentDiff, recentCommits = 50, topK = 5) { |
| | if (!(0, onnx_embedder_1.isReady)()) { |
| | await (0, onnx_embedder_1.initOnnxEmbedder)(); |
| | } |
| | |
| | const currentEmbedding = (await (0, onnx_embedder_1.embed)(currentDiff.substring(0, 1000))).embedding; |
| | |
| | let commits = []; |
| | try { |
| | commits = (0, child_process_1.execSync)(`git log -${recentCommits} --format="%H" 2>/dev/null`, { |
| | encoding: 'utf8', |
| | }).trim().split('\n'); |
| | } |
| | catch { |
| | return []; |
| | } |
| | |
| | const results = []; |
| | for (const hash of commits.slice(0, Math.min(commits.length, recentCommits))) { |
| | const analysis = await analyzeCommit(hash); |
| | if (analysis.embedding) { |
| | const similarity = cosineSimilarity(currentEmbedding, analysis.embedding); |
| | results.push({ hash, similarity, message: analysis.message }); |
| | } |
| | } |
| | return results |
| | .sort((a, b) => b.similarity - a.similarity) |
| | .slice(0, topK); |
| | } |
| | function cosineSimilarity(a, b) { |
| | if (a.length !== b.length) |
| | return 0; |
| | let dotProduct = 0; |
| | let normA = 0; |
| | let normB = 0; |
| | for (let i = 0; i < a.length; i++) { |
| | dotProduct += a[i] * b[i]; |
| | normA += a[i] * a[i]; |
| | normB += b[i] * b[i]; |
| | } |
| | const magnitude = Math.sqrt(normA) * Math.sqrt(normB); |
| | return magnitude === 0 ? 0 : dotProduct / magnitude; |
| | } |
| | exports.default = { |
| | parseDiff, |
| | classifyChange, |
| | calculateRiskScore, |
| | analyzeFileDiff, |
| | analyzeCommit, |
| | getCommitDiff, |
| | getStagedDiff, |
| | getUnstagedDiff, |
| | findSimilarCommits, |
| | }; |
| |
|