@claude-flow/cli 3.0.0-alpha.2 → 3.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -6
- package/bin/cli.js +0 -0
- package/dist/src/commands/agent.d.ts.map +1 -1
- package/dist/src/commands/agent.js +43 -27
- package/dist/src/commands/agent.js.map +1 -1
- package/dist/src/commands/analyze.d.ts +19 -0
- package/dist/src/commands/analyze.d.ts.map +1 -0
- package/dist/src/commands/analyze.js +1823 -0
- package/dist/src/commands/analyze.js.map +1 -0
- package/dist/src/commands/claims.d.ts +10 -0
- package/dist/src/commands/claims.d.ts.map +1 -0
- package/dist/src/commands/claims.js +288 -0
- package/dist/src/commands/claims.js.map +1 -0
- package/dist/src/commands/completions.d.ts +10 -0
- package/dist/src/commands/completions.d.ts.map +1 -0
- package/dist/src/commands/completions.js +539 -0
- package/dist/src/commands/completions.js.map +1 -0
- package/dist/src/commands/config.js +2 -2
- package/dist/src/commands/config.js.map +1 -1
- package/dist/src/commands/daemon.d.ts +8 -0
- package/dist/src/commands/daemon.d.ts.map +1 -0
- package/dist/src/commands/daemon.js +545 -0
- package/dist/src/commands/daemon.js.map +1 -0
- package/dist/src/commands/deployment.d.ts +10 -0
- package/dist/src/commands/deployment.d.ts.map +1 -0
- package/dist/src/commands/deployment.js +289 -0
- package/dist/src/commands/deployment.js.map +1 -0
- package/dist/src/commands/doctor.d.ts +10 -0
- package/dist/src/commands/doctor.d.ts.map +1 -0
- package/dist/src/commands/doctor.js +429 -0
- package/dist/src/commands/doctor.js.map +1 -0
- package/dist/src/commands/embeddings.d.ts +18 -0
- package/dist/src/commands/embeddings.d.ts.map +1 -0
- package/dist/src/commands/embeddings.js +616 -0
- package/dist/src/commands/embeddings.js.map +1 -0
- package/dist/src/commands/hive-mind.d.ts.map +1 -1
- package/dist/src/commands/hive-mind.js +252 -35
- package/dist/src/commands/hive-mind.js.map +1 -1
- package/dist/src/commands/hooks.d.ts.map +1 -1
- package/dist/src/commands/hooks.js +326 -2
- package/dist/src/commands/hooks.js.map +1 -1
- package/dist/src/commands/index.d.ts +13 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +52 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/mcp.js +4 -4
- package/dist/src/commands/mcp.js.map +1 -1
- package/dist/src/commands/memory.d.ts.map +1 -1
- package/dist/src/commands/memory.js +236 -170
- package/dist/src/commands/memory.js.map +1 -1
- package/dist/src/commands/migrate.js +1 -1
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/neural.d.ts +10 -0
- package/dist/src/commands/neural.d.ts.map +1 -0
- package/dist/src/commands/neural.js +224 -0
- package/dist/src/commands/neural.js.map +1 -0
- package/dist/src/commands/performance.d.ts +10 -0
- package/dist/src/commands/performance.d.ts.map +1 -0
- package/dist/src/commands/performance.js +262 -0
- package/dist/src/commands/performance.js.map +1 -0
- package/dist/src/commands/plugins.d.ts +10 -0
- package/dist/src/commands/plugins.d.ts.map +1 -0
- package/dist/src/commands/plugins.js +280 -0
- package/dist/src/commands/plugins.js.map +1 -0
- package/dist/src/commands/process.d.ts.map +1 -1
- package/dist/src/commands/process.js +95 -20
- package/dist/src/commands/process.js.map +1 -1
- package/dist/src/commands/providers.d.ts +10 -0
- package/dist/src/commands/providers.d.ts.map +1 -0
- package/dist/src/commands/providers.js +232 -0
- package/dist/src/commands/providers.js.map +1 -0
- package/dist/src/commands/route.d.ts +16 -0
- package/dist/src/commands/route.d.ts.map +1 -0
- package/dist/src/commands/route.js +603 -0
- package/dist/src/commands/route.js.map +1 -0
- package/dist/src/commands/security.d.ts +10 -0
- package/dist/src/commands/security.d.ts.map +1 -0
- package/dist/src/commands/security.js +261 -0
- package/dist/src/commands/security.js.map +1 -0
- package/dist/src/commands/start.js +2 -2
- package/dist/src/commands/start.js.map +1 -1
- package/dist/src/commands/status.d.ts.map +1 -1
- package/dist/src/commands/status.js +26 -2
- package/dist/src/commands/status.js.map +1 -1
- package/dist/src/commands/swarm.js +6 -6
- package/dist/src/commands/swarm.js.map +1 -1
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +63 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/init/claudemd-generator.d.ts.map +1 -1
- package/dist/src/init/claudemd-generator.js +218 -362
- package/dist/src/init/claudemd-generator.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +5 -0
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/settings-generator.d.ts.map +1 -1
- package/dist/src/init/settings-generator.js +22 -12
- package/dist/src/init/settings-generator.js.map +1 -1
- package/dist/src/mcp-client.d.ts.map +1 -1
- package/dist/src/mcp-client.js +17 -1
- package/dist/src/mcp-client.js.map +1 -1
- package/dist/src/mcp-server.d.ts.map +1 -1
- package/dist/src/mcp-server.js +5 -0
- package/dist/src/mcp-server.js.map +1 -1
- package/dist/src/mcp-tools/agent-tools.d.ts +1 -1
- package/dist/src/mcp-tools/agent-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/agent-tools.js +350 -14
- package/dist/src/mcp-tools/agent-tools.js.map +1 -1
- package/dist/src/mcp-tools/analyze-tools.d.ts +38 -0
- package/dist/src/mcp-tools/analyze-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/analyze-tools.js +317 -0
- package/dist/src/mcp-tools/analyze-tools.js.map +1 -0
- package/dist/src/mcp-tools/config-tools.d.ts +1 -1
- package/dist/src/mcp-tools/config-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/config-tools.js +262 -15
- package/dist/src/mcp-tools/config-tools.js.map +1 -1
- package/dist/src/mcp-tools/hive-mind-tools.d.ts +8 -0
- package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/hive-mind-tools.js +447 -0
- package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -0
- package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +80 -15
- package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
- package/dist/src/mcp-tools/index.d.ts +6 -0
- package/dist/src/mcp-tools/index.d.ts.map +1 -1
- package/dist/src/mcp-tools/index.js +6 -0
- package/dist/src/mcp-tools/index.js.map +1 -1
- package/dist/src/mcp-tools/memory-tools.d.ts +1 -1
- package/dist/src/mcp-tools/memory-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/memory-tools.js +157 -9
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/mcp-tools/session-tools.d.ts +8 -0
- package/dist/src/mcp-tools/session-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/session-tools.js +315 -0
- package/dist/src/mcp-tools/session-tools.js.map +1 -0
- package/dist/src/mcp-tools/swarm-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/swarm-tools.js +37 -2
- package/dist/src/mcp-tools/swarm-tools.js.map +1 -1
- package/dist/src/mcp-tools/task-tools.d.ts +8 -0
- package/dist/src/mcp-tools/task-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/task-tools.js +302 -0
- package/dist/src/mcp-tools/task-tools.js.map +1 -0
- package/dist/src/mcp-tools/workflow-tools.d.ts +8 -0
- package/dist/src/mcp-tools/workflow-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/workflow-tools.js +481 -0
- package/dist/src/mcp-tools/workflow-tools.js.map +1 -0
- package/dist/src/output.d.ts +16 -0
- package/dist/src/output.d.ts.map +1 -1
- package/dist/src/output.js +42 -0
- package/dist/src/output.js.map +1 -1
- package/dist/src/ruvector/ast-analyzer.d.ts +67 -0
- package/dist/src/ruvector/ast-analyzer.d.ts.map +1 -0
- package/dist/src/ruvector/ast-analyzer.js +277 -0
- package/dist/src/ruvector/ast-analyzer.js.map +1 -0
- package/dist/src/ruvector/coverage-router.d.ts +145 -0
- package/dist/src/ruvector/coverage-router.d.ts.map +1 -0
- package/dist/src/ruvector/coverage-router.js +451 -0
- package/dist/src/ruvector/coverage-router.js.map +1 -0
- package/dist/src/ruvector/coverage-tools.d.ts +33 -0
- package/dist/src/ruvector/coverage-tools.d.ts.map +1 -0
- package/dist/src/ruvector/coverage-tools.js +157 -0
- package/dist/src/ruvector/coverage-tools.js.map +1 -0
- package/dist/src/ruvector/diff-classifier.d.ts +175 -0
- package/dist/src/ruvector/diff-classifier.d.ts.map +1 -0
- package/dist/src/ruvector/diff-classifier.js +662 -0
- package/dist/src/ruvector/diff-classifier.js.map +1 -0
- package/dist/src/ruvector/graph-analyzer.d.ts +174 -0
- package/dist/src/ruvector/graph-analyzer.d.ts.map +1 -0
- package/dist/src/ruvector/graph-analyzer.js +878 -0
- package/dist/src/ruvector/graph-analyzer.js.map +1 -0
- package/dist/src/ruvector/index.d.ts +27 -0
- package/dist/src/ruvector/index.d.ts.map +1 -0
- package/dist/src/ruvector/index.js +49 -0
- package/dist/src/ruvector/index.js.map +1 -0
- package/dist/src/ruvector/q-learning-router.d.ts +211 -0
- package/dist/src/ruvector/q-learning-router.d.ts.map +1 -0
- package/dist/src/ruvector/q-learning-router.js +681 -0
- package/dist/src/ruvector/q-learning-router.js.map +1 -0
- package/dist/src/ruvector/vector-db.d.ts +69 -0
- package/dist/src/ruvector/vector-db.d.ts.map +1 -0
- package/dist/src/ruvector/vector-db.js +243 -0
- package/dist/src/ruvector/vector-db.js.map +1 -0
- package/dist/src/services/index.d.ts +7 -0
- package/dist/src/services/index.d.ts.map +1 -0
- package/dist/src/services/index.js +6 -0
- package/dist/src/services/index.js.map +1 -0
- package/dist/src/services/worker-daemon.d.ts +153 -0
- package/dist/src/services/worker-daemon.d.ts.map +1 -0
- package/dist/src/services/worker-daemon.js +567 -0
- package/dist/src/services/worker-daemon.js.map +1 -0
- package/dist/src/suggest.d.ts +53 -0
- package/dist/src/suggest.d.ts.map +1 -0
- package/dist/src/suggest.js +200 -0
- package/dist/src/suggest.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +28 -6
- package/.agentic-flow/intelligence.json +0 -16
- package/.claude-flow/metrics/agent-metrics.json +0 -1
- package/.claude-flow/metrics/performance.json +0 -87
- package/.claude-flow/metrics/task-metrics.json +0 -10
- package/__tests__/README.md +0 -140
- package/__tests__/TEST_SUMMARY.md +0 -144
- package/__tests__/cli.test.ts +0 -558
- package/__tests__/commands.test.ts +0 -726
- package/__tests__/config-adapter.test.ts +0 -362
- package/__tests__/config-loading.test.ts +0 -106
- package/__tests__/coverage/.tmp/coverage-0.json +0 -1
- package/__tests__/coverage/.tmp/coverage-1.json +0 -1
- package/__tests__/coverage/.tmp/coverage-2.json +0 -1
- package/__tests__/coverage/.tmp/coverage-3.json +0 -1
- package/__tests__/coverage/.tmp/coverage-4.json +0 -1
- package/__tests__/coverage/.tmp/coverage-5.json +0 -1
- package/__tests__/mcp-client.test.ts +0 -480
- package/__tests__/p1-commands.test.ts +0 -1064
- package/docs/CONFIG_LOADING.md +0 -236
- package/docs/IMPLEMENTATION_COMPLETE.md +0 -421
- package/docs/MCP_CLIENT_GUIDE.md +0 -620
- package/docs/REFACTORING_SUMMARY.md +0 -247
- package/src/commands/agent.ts +0 -941
- package/src/commands/config.ts +0 -452
- package/src/commands/hive-mind.ts +0 -762
- package/src/commands/hooks.ts +0 -2603
- package/src/commands/index.ts +0 -115
- package/src/commands/init.ts +0 -597
- package/src/commands/mcp.ts +0 -753
- package/src/commands/memory.ts +0 -1063
- package/src/commands/migrate.ts +0 -447
- package/src/commands/process.ts +0 -617
- package/src/commands/session.ts +0 -891
- package/src/commands/start.ts +0 -457
- package/src/commands/status.ts +0 -705
- package/src/commands/swarm.ts +0 -648
- package/src/commands/task.ts +0 -792
- package/src/commands/workflow.ts +0 -742
- package/src/config-adapter.ts +0 -210
- package/src/index.ts +0 -383
- package/src/infrastructure/in-memory-repositories.ts +0 -310
- package/src/init/claudemd-generator.ts +0 -631
- package/src/init/executor.ts +0 -756
- package/src/init/helpers-generator.ts +0 -628
- package/src/init/index.ts +0 -60
- package/src/init/mcp-generator.ts +0 -83
- package/src/init/settings-generator.ts +0 -274
- package/src/init/statusline-generator.ts +0 -211
- package/src/init/types.ts +0 -447
- package/src/mcp-client.ts +0 -227
- package/src/mcp-server.ts +0 -571
- package/src/mcp-tools/agent-tools.ts +0 -92
- package/src/mcp-tools/config-tools.ts +0 -88
- package/src/mcp-tools/hooks-tools.ts +0 -1819
- package/src/mcp-tools/index.ts +0 -12
- package/src/mcp-tools/memory-tools.ts +0 -89
- package/src/mcp-tools/swarm-tools.ts +0 -69
- package/src/mcp-tools/types.ts +0 -33
- package/src/output.ts +0 -593
- package/src/parser.ts +0 -417
- package/src/prompt.ts +0 -619
- package/src/types.ts +0 -287
- package/tmp.json +0 -0
- package/tsconfig.json +0 -16
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -13
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff Classifier for Change Analysis
|
|
3
|
+
*/
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
maxDiffSize: 10000,
|
|
6
|
+
classifyByImpact: true,
|
|
7
|
+
detectRefactoring: true,
|
|
8
|
+
minConfidence: 0.5,
|
|
9
|
+
};
|
|
10
|
+
const CLASSIFICATION_PATTERNS = {
|
|
11
|
+
feature: [/^feat/, /add.*feature/, /implement/, /new.*functionality/i],
|
|
12
|
+
bugfix: [/^fix/, /bug/, /patch/, /resolve.*issue/i, /hotfix/i],
|
|
13
|
+
refactor: [/^refactor/, /restructure/, /reorganize/, /cleanup/i, /rename/i],
|
|
14
|
+
docs: [/^docs?/, /documentation/, /readme/i, /comment/i, /\.md$/i],
|
|
15
|
+
test: [/^test/, /spec/, /\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /__tests__/],
|
|
16
|
+
config: [/^config/, /\.config\./, /package\.json/, /tsconfig/, /\.env/],
|
|
17
|
+
style: [/^style/, /format/, /lint/, /prettier/, /eslint/],
|
|
18
|
+
};
|
|
19
|
+
const IMPACT_KEYWORDS = {
|
|
20
|
+
security: 3, auth: 3, payment: 3, database: 2, api: 2, core: 2,
|
|
21
|
+
util: 1, helper: 1, test: 0, mock: 0, fixture: 0,
|
|
22
|
+
};
|
|
23
|
+
export class DiffClassifier {
|
|
24
|
+
config;
|
|
25
|
+
ruvectorEngine = null;
|
|
26
|
+
useNative = false;
|
|
27
|
+
classificationCache = new Map();
|
|
28
|
+
constructor(config = {}) {
|
|
29
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
30
|
+
}
|
|
31
|
+
async initialize() {
|
|
32
|
+
try {
|
|
33
|
+
// @ruvector/diff is optional - gracefully fallback if not installed
|
|
34
|
+
const ruvector = await import('@ruvector/diff').catch(() => null);
|
|
35
|
+
if (ruvector) {
|
|
36
|
+
this.ruvectorEngine = ruvector.createDiffClassifier?.(this.config);
|
|
37
|
+
this.useNative = !!this.ruvectorEngine;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
this.useNative = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
parseDiff(diffContent) {
|
|
45
|
+
const files = [];
|
|
46
|
+
const fileBlocks = diffContent.split(/^diff --git/m).filter(Boolean);
|
|
47
|
+
for (const block of fileBlocks) {
|
|
48
|
+
const pathMatch = block.match(/a\/(.+?)\s+b\/(.+)/);
|
|
49
|
+
if (!pathMatch)
|
|
50
|
+
continue;
|
|
51
|
+
const path = pathMatch[2];
|
|
52
|
+
const hunks = this.parseHunks(block);
|
|
53
|
+
const additions = hunks.reduce((sum, h) => sum + h.changes.filter(c => c.type === 'add').length, 0);
|
|
54
|
+
const deletions = hunks.reduce((sum, h) => sum + h.changes.filter(c => c.type === 'remove').length, 0);
|
|
55
|
+
const classification = this.classifyFile(path, hunks);
|
|
56
|
+
files.push({ path, hunks, additions, deletions, classification });
|
|
57
|
+
}
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
classify(files) {
|
|
61
|
+
const overall = this.computeOverallClassification(files);
|
|
62
|
+
const stats = {
|
|
63
|
+
totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
|
|
64
|
+
totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
|
|
65
|
+
filesChanged: files.length,
|
|
66
|
+
avgConfidence: files.length > 0 ? files.reduce((sum, f) => sum + f.classification.confidence, 0) / files.length : 0,
|
|
67
|
+
};
|
|
68
|
+
return { files, overall, stats, timestamp: Date.now() };
|
|
69
|
+
}
|
|
70
|
+
classifyCommitMessage(message) {
|
|
71
|
+
const lowerMessage = message.toLowerCase();
|
|
72
|
+
for (const [type, patterns] of Object.entries(CLASSIFICATION_PATTERNS)) {
|
|
73
|
+
for (const pattern of patterns) {
|
|
74
|
+
if (pattern.test(lowerMessage))
|
|
75
|
+
return type;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return 'unknown';
|
|
79
|
+
}
|
|
80
|
+
getStats() {
|
|
81
|
+
return { useNative: this.useNative, cacheSize: this.classificationCache.size };
|
|
82
|
+
}
|
|
83
|
+
clearCache() { this.classificationCache.clear(); }
|
|
84
|
+
parseHunks(block) {
|
|
85
|
+
const hunks = [];
|
|
86
|
+
const hunkMatches = block.matchAll(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@([^\n]*)\n([\s\S]*?)(?=@@|$)/g);
|
|
87
|
+
for (const match of hunkMatches) {
|
|
88
|
+
const oldStart = parseInt(match[1], 10);
|
|
89
|
+
const oldLines = parseInt(match[2] || '1', 10);
|
|
90
|
+
const newStart = parseInt(match[3], 10);
|
|
91
|
+
const newLines = parseInt(match[4] || '1', 10);
|
|
92
|
+
const content = match[6] || '';
|
|
93
|
+
const changes = this.parseChanges(content, newStart);
|
|
94
|
+
hunks.push({ oldStart, oldLines, newStart, newLines, content, changes });
|
|
95
|
+
}
|
|
96
|
+
return hunks;
|
|
97
|
+
}
|
|
98
|
+
parseChanges(content, startLine) {
|
|
99
|
+
const changes = [];
|
|
100
|
+
const lines = content.split('\n');
|
|
101
|
+
let lineNumber = startLine;
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (line.startsWith('+')) {
|
|
104
|
+
changes.push({ type: 'add', lineNumber, content: line.substring(1) });
|
|
105
|
+
lineNumber++;
|
|
106
|
+
}
|
|
107
|
+
else if (line.startsWith('-')) {
|
|
108
|
+
changes.push({ type: 'remove', lineNumber: -1, content: line.substring(1) });
|
|
109
|
+
}
|
|
110
|
+
else if (line.startsWith(' ') || line === '') {
|
|
111
|
+
changes.push({ type: 'context', lineNumber, content: line.substring(1) || '' });
|
|
112
|
+
lineNumber++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return changes;
|
|
116
|
+
}
|
|
117
|
+
classifyFile(path, hunks) {
|
|
118
|
+
const cacheKey = this.getCacheKey(path, hunks);
|
|
119
|
+
const cached = this.classificationCache.get(cacheKey);
|
|
120
|
+
if (cached)
|
|
121
|
+
return cached;
|
|
122
|
+
const primary = this.determinePrimaryClassification(path, hunks);
|
|
123
|
+
const secondary = this.determineSecondaryClassifications(path, hunks, primary);
|
|
124
|
+
const confidence = this.calculateConfidence(path, hunks, primary);
|
|
125
|
+
const impactLevel = this.determineImpactLevel(path, hunks);
|
|
126
|
+
const suggestedReviewers = this.suggestReviewers(path, primary, impactLevel);
|
|
127
|
+
const testingStrategy = this.determineTestingStrategy(path, primary, impactLevel);
|
|
128
|
+
const riskFactors = this.identifyRiskFactors(path, hunks, impactLevel);
|
|
129
|
+
const classification = { primary, secondary, confidence, impactLevel, suggestedReviewers, testingStrategy, riskFactors };
|
|
130
|
+
this.classificationCache.set(cacheKey, classification);
|
|
131
|
+
return classification;
|
|
132
|
+
}
|
|
133
|
+
getCacheKey(path, hunks) {
|
|
134
|
+
const hunkSummary = hunks.map(h => h.oldStart + ':' + h.newStart).join(',');
|
|
135
|
+
return path + ':' + hunkSummary;
|
|
136
|
+
}
|
|
137
|
+
determinePrimaryClassification(path, hunks) {
|
|
138
|
+
for (const [type, patterns] of Object.entries(CLASSIFICATION_PATTERNS)) {
|
|
139
|
+
for (const pattern of patterns) {
|
|
140
|
+
if (pattern.test(path))
|
|
141
|
+
return type;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const allContent = hunks.flatMap(h => h.changes.map(c => c.content)).join('\n').toLowerCase();
|
|
145
|
+
if (/function|class|interface|type\s+\w+/.test(allContent) && hunks.some(h => h.changes.filter(c => c.type === 'add').length > 10))
|
|
146
|
+
return 'feature';
|
|
147
|
+
if (/fix|bug|issue|error|exception/.test(allContent))
|
|
148
|
+
return 'bugfix';
|
|
149
|
+
if (this.config.detectRefactoring && this.isRefactoring(hunks))
|
|
150
|
+
return 'refactor';
|
|
151
|
+
return 'unknown';
|
|
152
|
+
}
|
|
153
|
+
isRefactoring(hunks) {
|
|
154
|
+
let totalAdds = 0, totalRemoves = 0;
|
|
155
|
+
for (const hunk of hunks) {
|
|
156
|
+
for (const change of hunk.changes) {
|
|
157
|
+
if (change.type === 'add')
|
|
158
|
+
totalAdds++;
|
|
159
|
+
else if (change.type === 'remove')
|
|
160
|
+
totalRemoves++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const ratio = totalAdds > 0 ? totalRemoves / totalAdds : 0;
|
|
164
|
+
return ratio > 0.7 && ratio < 1.4 && totalAdds > 5;
|
|
165
|
+
}
|
|
166
|
+
determineSecondaryClassifications(path, hunks, primary) {
|
|
167
|
+
const secondary = [];
|
|
168
|
+
for (const [type, patterns] of Object.entries(CLASSIFICATION_PATTERNS)) {
|
|
169
|
+
if (type === primary)
|
|
170
|
+
continue;
|
|
171
|
+
for (const pattern of patterns) {
|
|
172
|
+
if (pattern.test(path)) {
|
|
173
|
+
secondary.push(type);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return secondary.slice(0, 3);
|
|
179
|
+
}
|
|
180
|
+
calculateConfidence(path, hunks, primary) {
|
|
181
|
+
let confidence = 0.5;
|
|
182
|
+
for (const patterns of Object.values(CLASSIFICATION_PATTERNS)) {
|
|
183
|
+
for (const pattern of patterns) {
|
|
184
|
+
if (pattern.test(path)) {
|
|
185
|
+
confidence += 0.2;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const totalChanges = hunks.reduce((sum, h) => sum + h.changes.length, 0);
|
|
191
|
+
if (totalChanges > 10)
|
|
192
|
+
confidence += 0.1;
|
|
193
|
+
if (totalChanges > 50)
|
|
194
|
+
confidence += 0.1;
|
|
195
|
+
if (primary !== 'unknown')
|
|
196
|
+
confidence += 0.1;
|
|
197
|
+
return Math.min(1, confidence);
|
|
198
|
+
}
|
|
199
|
+
determineImpactLevel(path, hunks) {
|
|
200
|
+
let score = 0;
|
|
201
|
+
const lowerPath = path.toLowerCase();
|
|
202
|
+
for (const [keyword, weight] of Object.entries(IMPACT_KEYWORDS)) {
|
|
203
|
+
if (lowerPath.includes(keyword))
|
|
204
|
+
score = Math.max(score, weight);
|
|
205
|
+
}
|
|
206
|
+
const totalChanges = hunks.reduce((sum, h) => sum + h.changes.filter(c => c.type !== 'context').length, 0);
|
|
207
|
+
if (totalChanges > 100)
|
|
208
|
+
score = Math.max(score, 2);
|
|
209
|
+
if (totalChanges > 300)
|
|
210
|
+
score = Math.max(score, 3);
|
|
211
|
+
if (score >= 3)
|
|
212
|
+
return 'critical';
|
|
213
|
+
if (score >= 2)
|
|
214
|
+
return 'high';
|
|
215
|
+
if (score >= 1)
|
|
216
|
+
return 'medium';
|
|
217
|
+
return 'low';
|
|
218
|
+
}
|
|
219
|
+
suggestReviewers(path, primary, impact) {
|
|
220
|
+
const reviewers = [];
|
|
221
|
+
const typeReviewers = { feature: ['tech-lead', 'product-owner'], bugfix: ['qa-engineer', 'developer'], refactor: ['senior-developer', 'architect'], docs: ['tech-writer', 'developer'], test: ['qa-engineer', 'developer'], config: ['devops', 'tech-lead'], style: ['developer'], unknown: ['developer'] };
|
|
222
|
+
reviewers.push(...(typeReviewers[primary] || typeReviewers.unknown));
|
|
223
|
+
if (impact === 'critical' || impact === 'high')
|
|
224
|
+
reviewers.push('security-reviewer');
|
|
225
|
+
if (/security|auth/.test(path))
|
|
226
|
+
reviewers.push('security-team');
|
|
227
|
+
if (/database|migration/.test(path))
|
|
228
|
+
reviewers.push('dba');
|
|
229
|
+
return [...new Set(reviewers)].slice(0, 4);
|
|
230
|
+
}
|
|
231
|
+
determineTestingStrategy(path, primary, impact) {
|
|
232
|
+
const strategies = [];
|
|
233
|
+
if (primary !== 'test')
|
|
234
|
+
strategies.push('unit-tests');
|
|
235
|
+
if (primary === 'feature')
|
|
236
|
+
strategies.push('integration-tests');
|
|
237
|
+
if (impact === 'high' || impact === 'critical') {
|
|
238
|
+
strategies.push('regression-tests');
|
|
239
|
+
strategies.push('e2e-tests');
|
|
240
|
+
}
|
|
241
|
+
if (/api|endpoint|route|handler/.test(path))
|
|
242
|
+
strategies.push('api-contract-tests');
|
|
243
|
+
if (/security|auth|crypto/.test(path))
|
|
244
|
+
strategies.push('security-audit');
|
|
245
|
+
return strategies.slice(0, 5);
|
|
246
|
+
}
|
|
247
|
+
identifyRiskFactors(path, hunks, impact) {
|
|
248
|
+
const risks = [];
|
|
249
|
+
const totalChanges = hunks.reduce((sum, h) => sum + h.changes.length, 0);
|
|
250
|
+
if (totalChanges > 200)
|
|
251
|
+
risks.push('Large change set - increased review time needed');
|
|
252
|
+
if (impact === 'critical')
|
|
253
|
+
risks.push('Critical system component - requires careful review');
|
|
254
|
+
if (impact === 'high')
|
|
255
|
+
risks.push('High-impact area - monitor after deployment');
|
|
256
|
+
if (/security|auth/.test(path))
|
|
257
|
+
risks.push('Security-sensitive code');
|
|
258
|
+
if (/database|migration/.test(path))
|
|
259
|
+
risks.push('Database changes - ensure backup strategy');
|
|
260
|
+
if (/config|env/.test(path))
|
|
261
|
+
risks.push('Configuration changes - verify all environments');
|
|
262
|
+
const allContent = hunks.flatMap(h => h.changes.map(c => c.content)).join('\n');
|
|
263
|
+
if (/TODO|FIXME|HACK/.test(allContent))
|
|
264
|
+
risks.push('Contains TODO/FIXME comments');
|
|
265
|
+
if (/password|secret|key|token/i.test(allContent))
|
|
266
|
+
risks.push('Potential secrets in code');
|
|
267
|
+
return risks.slice(0, 5);
|
|
268
|
+
}
|
|
269
|
+
computeOverallClassification(files) {
|
|
270
|
+
if (files.length === 0)
|
|
271
|
+
return { primary: 'unknown', secondary: [], confidence: 0, impactLevel: 'low', suggestedReviewers: [], testingStrategy: [], riskFactors: [] };
|
|
272
|
+
const primaryCounts = {};
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
const p = file.classification.primary;
|
|
275
|
+
primaryCounts[p] = (primaryCounts[p] || 0) + 1;
|
|
276
|
+
}
|
|
277
|
+
let primary = 'unknown';
|
|
278
|
+
let maxCount = 0;
|
|
279
|
+
for (const [type, count] of Object.entries(primaryCounts)) {
|
|
280
|
+
if (count > maxCount) {
|
|
281
|
+
maxCount = count;
|
|
282
|
+
primary = type;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const secondaryCounts = {};
|
|
286
|
+
for (const file of files) {
|
|
287
|
+
for (const s of file.classification.secondary) {
|
|
288
|
+
secondaryCounts[s] = (secondaryCounts[s] || 0) + 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const secondary = Object.entries(secondaryCounts).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
|
|
292
|
+
const confidence = files.reduce((sum, f) => sum + f.classification.confidence, 0) / files.length;
|
|
293
|
+
const impactOrder = ['low', 'medium', 'high', 'critical'];
|
|
294
|
+
let impactLevel = 'low';
|
|
295
|
+
for (const file of files) {
|
|
296
|
+
if (impactOrder.indexOf(file.classification.impactLevel) > impactOrder.indexOf(impactLevel))
|
|
297
|
+
impactLevel = file.classification.impactLevel;
|
|
298
|
+
}
|
|
299
|
+
const reviewers = [...new Set(files.flatMap(f => f.classification.suggestedReviewers))].slice(0, 5);
|
|
300
|
+
const testingStrategy = [...new Set(files.flatMap(f => f.classification.testingStrategy))].slice(0, 5);
|
|
301
|
+
const riskFactors = [...new Set(files.flatMap(f => f.classification.riskFactors))].slice(0, 5);
|
|
302
|
+
return { primary, secondary, confidence, impactLevel, suggestedReviewers: reviewers, testingStrategy, riskFactors };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
export function createDiffClassifier(config) {
|
|
306
|
+
return new DiffClassifier(config);
|
|
307
|
+
}
|
|
308
|
+
// ============================================================================
|
|
309
|
+
// Optimized Git Diff Functions
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// Cache for diff results (TTL-based)
|
|
312
|
+
const diffCache = new Map();
|
|
313
|
+
const CACHE_TTL_MS = 5000; // 5 seconds - short TTL since diffs change frequently
|
|
314
|
+
/**
|
|
315
|
+
* Get git diff statistics using SINGLE combined command (optimized)
|
|
316
|
+
* Replaces two separate git commands with one
|
|
317
|
+
*/
|
|
318
|
+
export function getGitDiffNumstat(ref = 'HEAD') {
|
|
319
|
+
// Check cache first
|
|
320
|
+
const cacheKey = `numstat:${ref}`;
|
|
321
|
+
const cached = diffCache.get(cacheKey);
|
|
322
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
323
|
+
return cached.files;
|
|
324
|
+
}
|
|
325
|
+
const { execSync } = require('child_process');
|
|
326
|
+
try {
|
|
327
|
+
// OPTIMIZATION: Single combined command instead of two separate calls
|
|
328
|
+
// Uses --diff-filter to get status and --numstat in one pass
|
|
329
|
+
const output = execSync(`git diff --numstat --diff-filter=ACDMRTUXB ${ref} && echo "---STATUS---" && git diff --name-status ${ref}`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 } // 10MB buffer for large diffs
|
|
330
|
+
);
|
|
331
|
+
const [numstatPart, statusPart] = output.split('---STATUS---');
|
|
332
|
+
// Parse status (usually smaller, parse first)
|
|
333
|
+
const statusMap = new Map();
|
|
334
|
+
if (statusPart) {
|
|
335
|
+
for (const line of statusPart.trim().split('\n')) {
|
|
336
|
+
if (!line)
|
|
337
|
+
continue;
|
|
338
|
+
const [status, ...pathParts] = line.split('\t');
|
|
339
|
+
const path = pathParts[pathParts.length - 1] || pathParts[0];
|
|
340
|
+
if (path)
|
|
341
|
+
statusMap.set(path, status.charAt(0));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Parse numstat
|
|
345
|
+
const files = [];
|
|
346
|
+
if (numstatPart) {
|
|
347
|
+
for (const line of numstatPart.trim().split('\n')) {
|
|
348
|
+
if (!line)
|
|
349
|
+
continue;
|
|
350
|
+
const [addStr, delStr, path] = line.split('\t');
|
|
351
|
+
if (!path)
|
|
352
|
+
continue;
|
|
353
|
+
const additions = addStr === '-' ? 0 : parseInt(addStr, 10) || 0;
|
|
354
|
+
const deletions = delStr === '-' ? 0 : parseInt(delStr, 10) || 0;
|
|
355
|
+
const binary = addStr === '-' && delStr === '-';
|
|
356
|
+
const statusChar = statusMap.get(path) || 'M';
|
|
357
|
+
let status = 'modified';
|
|
358
|
+
switch (statusChar) {
|
|
359
|
+
case 'A':
|
|
360
|
+
status = 'added';
|
|
361
|
+
break;
|
|
362
|
+
case 'D':
|
|
363
|
+
status = 'deleted';
|
|
364
|
+
break;
|
|
365
|
+
case 'R':
|
|
366
|
+
status = 'renamed';
|
|
367
|
+
break;
|
|
368
|
+
default: status = 'modified';
|
|
369
|
+
}
|
|
370
|
+
files.push({ path, status, additions, deletions, hunks: 1, binary });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Cache the result
|
|
374
|
+
diffCache.set(cacheKey, { files, timestamp: Date.now() });
|
|
375
|
+
return files;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Async version of getGitDiffNumstat for non-blocking operation
|
|
383
|
+
*/
|
|
384
|
+
export async function getGitDiffNumstatAsync(ref = 'HEAD') {
|
|
385
|
+
// Check cache first
|
|
386
|
+
const cacheKey = `numstat:${ref}`;
|
|
387
|
+
const cached = diffCache.get(cacheKey);
|
|
388
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
389
|
+
return cached.files;
|
|
390
|
+
}
|
|
391
|
+
const { exec } = require('child_process');
|
|
392
|
+
const { promisify } = require('util');
|
|
393
|
+
const execAsync = promisify(exec);
|
|
394
|
+
try {
|
|
395
|
+
const { stdout } = await execAsync(`git diff --numstat --diff-filter=ACDMRTUXB ${ref} && echo "---STATUS---" && git diff --name-status ${ref}`, { maxBuffer: 10 * 1024 * 1024 });
|
|
396
|
+
const [numstatPart, statusPart] = stdout.split('---STATUS---');
|
|
397
|
+
const statusMap = new Map();
|
|
398
|
+
if (statusPart) {
|
|
399
|
+
for (const line of statusPart.trim().split('\n')) {
|
|
400
|
+
if (!line)
|
|
401
|
+
continue;
|
|
402
|
+
const [status, ...pathParts] = line.split('\t');
|
|
403
|
+
const path = pathParts[pathParts.length - 1] || pathParts[0];
|
|
404
|
+
if (path)
|
|
405
|
+
statusMap.set(path, status.charAt(0));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const files = [];
|
|
409
|
+
if (numstatPart) {
|
|
410
|
+
for (const line of numstatPart.trim().split('\n')) {
|
|
411
|
+
if (!line)
|
|
412
|
+
continue;
|
|
413
|
+
const [addStr, delStr, path] = line.split('\t');
|
|
414
|
+
if (!path)
|
|
415
|
+
continue;
|
|
416
|
+
const additions = addStr === '-' ? 0 : parseInt(addStr, 10) || 0;
|
|
417
|
+
const deletions = delStr === '-' ? 0 : parseInt(delStr, 10) || 0;
|
|
418
|
+
const binary = addStr === '-' && delStr === '-';
|
|
419
|
+
const statusChar = statusMap.get(path) || 'M';
|
|
420
|
+
let status = 'modified';
|
|
421
|
+
switch (statusChar) {
|
|
422
|
+
case 'A':
|
|
423
|
+
status = 'added';
|
|
424
|
+
break;
|
|
425
|
+
case 'D':
|
|
426
|
+
status = 'deleted';
|
|
427
|
+
break;
|
|
428
|
+
case 'R':
|
|
429
|
+
status = 'renamed';
|
|
430
|
+
break;
|
|
431
|
+
default: status = 'modified';
|
|
432
|
+
}
|
|
433
|
+
files.push({ path, status, additions, deletions, hunks: 1, binary });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
diffCache.set(cacheKey, { files, timestamp: Date.now() });
|
|
437
|
+
return files;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Clear the diff cache (call when git state changes)
|
|
445
|
+
*/
|
|
446
|
+
export function clearDiffCache() {
|
|
447
|
+
diffCache.clear();
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Assess risk for a single file
|
|
451
|
+
*/
|
|
452
|
+
export function assessFileRisk(file) {
|
|
453
|
+
const reasons = [];
|
|
454
|
+
let score = 0;
|
|
455
|
+
// Size-based risk
|
|
456
|
+
const totalChanges = file.additions + file.deletions;
|
|
457
|
+
if (totalChanges > 300) {
|
|
458
|
+
score += 30;
|
|
459
|
+
reasons.push('Large change size (>300 lines)');
|
|
460
|
+
}
|
|
461
|
+
else if (totalChanges > 100) {
|
|
462
|
+
score += 15;
|
|
463
|
+
reasons.push('Medium change size (>100 lines)');
|
|
464
|
+
}
|
|
465
|
+
// Path-based risk
|
|
466
|
+
const lowerPath = file.path.toLowerCase();
|
|
467
|
+
if (/security|auth|crypto|password/.test(lowerPath)) {
|
|
468
|
+
score += 40;
|
|
469
|
+
reasons.push('Security-sensitive file');
|
|
470
|
+
}
|
|
471
|
+
if (/payment|billing|transaction/.test(lowerPath)) {
|
|
472
|
+
score += 35;
|
|
473
|
+
reasons.push('Payment-related file');
|
|
474
|
+
}
|
|
475
|
+
if (/database|migration|schema/.test(lowerPath)) {
|
|
476
|
+
score += 25;
|
|
477
|
+
reasons.push('Database-related file');
|
|
478
|
+
}
|
|
479
|
+
if (/core|main|index/.test(lowerPath)) {
|
|
480
|
+
score += 15;
|
|
481
|
+
reasons.push('Core module');
|
|
482
|
+
}
|
|
483
|
+
if (/config|env|settings/.test(lowerPath)) {
|
|
484
|
+
score += 20;
|
|
485
|
+
reasons.push('Configuration file');
|
|
486
|
+
}
|
|
487
|
+
// Status-based risk
|
|
488
|
+
if (file.status === 'deleted') {
|
|
489
|
+
score += 10;
|
|
490
|
+
reasons.push('File deleted');
|
|
491
|
+
}
|
|
492
|
+
// Binary file risk
|
|
493
|
+
if (file.binary) {
|
|
494
|
+
score += 5;
|
|
495
|
+
reasons.push('Binary file');
|
|
496
|
+
}
|
|
497
|
+
let risk = 'low';
|
|
498
|
+
if (score >= 60)
|
|
499
|
+
risk = 'critical';
|
|
500
|
+
else if (score >= 40)
|
|
501
|
+
risk = 'high';
|
|
502
|
+
else if (score >= 20)
|
|
503
|
+
risk = 'medium';
|
|
504
|
+
return { file: file.path, risk, score: Math.min(100, score), reasons };
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Assess overall risk from files and file risks
|
|
508
|
+
*/
|
|
509
|
+
export function assessOverallRisk(files, fileRisks) {
|
|
510
|
+
const breakdown = { low: 0, medium: 0, high: 0, critical: 0 };
|
|
511
|
+
let totalScore = 0;
|
|
512
|
+
for (const fr of fileRisks) {
|
|
513
|
+
breakdown[fr.risk]++;
|
|
514
|
+
totalScore += fr.score;
|
|
515
|
+
}
|
|
516
|
+
const avgScore = fileRisks.length > 0 ? totalScore / fileRisks.length : 0;
|
|
517
|
+
// Weight more heavily towards high/critical files
|
|
518
|
+
const weightedScore = avgScore + (breakdown.critical * 15) + (breakdown.high * 10);
|
|
519
|
+
let overall = 'low';
|
|
520
|
+
if (weightedScore >= 60 || breakdown.critical > 0)
|
|
521
|
+
overall = 'critical';
|
|
522
|
+
else if (weightedScore >= 40 || breakdown.high > 1)
|
|
523
|
+
overall = 'high';
|
|
524
|
+
else if (weightedScore >= 20 || breakdown.medium > 2)
|
|
525
|
+
overall = 'medium';
|
|
526
|
+
return { overall, score: Math.min(100, Math.round(weightedScore)), breakdown };
|
|
527
|
+
}
|
|
528
|
+
// Singleton classifier instance for reuse
|
|
529
|
+
let classifierInstance = null;
|
|
530
|
+
function getClassifier() {
|
|
531
|
+
if (!classifierInstance) {
|
|
532
|
+
classifierInstance = new DiffClassifier();
|
|
533
|
+
}
|
|
534
|
+
return classifierInstance;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Classify a diff based on files (uses singleton classifier)
|
|
538
|
+
*/
|
|
539
|
+
export function classifyDiff(files) {
|
|
540
|
+
const classifier = getClassifier();
|
|
541
|
+
const fileDiffs = files.map(f => ({
|
|
542
|
+
path: f.path,
|
|
543
|
+
hunks: [],
|
|
544
|
+
additions: f.additions,
|
|
545
|
+
deletions: f.deletions,
|
|
546
|
+
classification: classifier['classifyFile'](f.path, []),
|
|
547
|
+
}));
|
|
548
|
+
return classifier['computeOverallClassification'](fileDiffs);
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Suggest reviewers based on files and risks
|
|
552
|
+
*/
|
|
553
|
+
export function suggestReviewers(files, fileRisks) {
|
|
554
|
+
const reviewers = new Set();
|
|
555
|
+
for (const file of files) {
|
|
556
|
+
const lowerPath = file.path.toLowerCase();
|
|
557
|
+
if (/security|auth|crypto/.test(lowerPath))
|
|
558
|
+
reviewers.add('security-team');
|
|
559
|
+
if (/database|migration/.test(lowerPath))
|
|
560
|
+
reviewers.add('dba');
|
|
561
|
+
if (/api|endpoint|route/.test(lowerPath))
|
|
562
|
+
reviewers.add('api-owner');
|
|
563
|
+
if (/test|spec/.test(lowerPath))
|
|
564
|
+
reviewers.add('qa-engineer');
|
|
565
|
+
if (/config|deploy|ci/.test(lowerPath))
|
|
566
|
+
reviewers.add('devops');
|
|
567
|
+
if (/ui|component|style/.test(lowerPath))
|
|
568
|
+
reviewers.add('frontend-lead');
|
|
569
|
+
if (/model|service|repository/.test(lowerPath))
|
|
570
|
+
reviewers.add('backend-lead');
|
|
571
|
+
}
|
|
572
|
+
// Add based on risk
|
|
573
|
+
const hasHighRisk = fileRisks.some(fr => fr.risk === 'high' || fr.risk === 'critical');
|
|
574
|
+
if (hasHighRisk) {
|
|
575
|
+
reviewers.add('tech-lead');
|
|
576
|
+
reviewers.add('senior-developer');
|
|
577
|
+
}
|
|
578
|
+
// Default reviewer
|
|
579
|
+
if (reviewers.size === 0) {
|
|
580
|
+
reviewers.add('developer');
|
|
581
|
+
}
|
|
582
|
+
return Array.from(reviewers).slice(0, 5);
|
|
583
|
+
}
|
|
584
|
+
// Analysis result cache
|
|
585
|
+
const analysisCache = new Map();
|
|
586
|
+
const ANALYSIS_CACHE_TTL_MS = 3000; // 3 seconds
|
|
587
|
+
/**
|
|
588
|
+
* Analyze a diff with full analysis (optimized with caching)
|
|
589
|
+
*/
|
|
590
|
+
export async function analyzeDiff(options) {
|
|
591
|
+
const ref = options.ref || 'HEAD';
|
|
592
|
+
// Check analysis cache (unless skipCache is true)
|
|
593
|
+
if (!options.skipCache) {
|
|
594
|
+
const cached = analysisCache.get(ref);
|
|
595
|
+
if (cached && Date.now() - cached.timestamp < ANALYSIS_CACHE_TTL_MS) {
|
|
596
|
+
return cached.result;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// Use async git diff for non-blocking operation
|
|
600
|
+
const files = await getGitDiffNumstatAsync(ref);
|
|
601
|
+
// Parallel file risk assessment for large diffs
|
|
602
|
+
const fileRisks = files.length > 20
|
|
603
|
+
? await Promise.all(files.map(f => Promise.resolve(assessFileRisk(f))))
|
|
604
|
+
: files.map(assessFileRisk);
|
|
605
|
+
const risk = assessOverallRisk(files, fileRisks);
|
|
606
|
+
const classification = classifyDiff(files);
|
|
607
|
+
const recommendedReviewers = suggestReviewers(files, fileRisks);
|
|
608
|
+
const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
|
|
609
|
+
const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
|
610
|
+
const result = {
|
|
611
|
+
ref,
|
|
612
|
+
timestamp: Date.now(),
|
|
613
|
+
files,
|
|
614
|
+
risk,
|
|
615
|
+
classification,
|
|
616
|
+
summary: `${files.length} files changed (+${totalAdditions}/-${totalDeletions}), ${risk.overall} risk`,
|
|
617
|
+
fileRisks,
|
|
618
|
+
recommendedReviewers,
|
|
619
|
+
};
|
|
620
|
+
// Cache the result
|
|
621
|
+
analysisCache.set(ref, { result, timestamp: Date.now() });
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Synchronous version of analyzeDiff for backward compatibility
|
|
626
|
+
*/
|
|
627
|
+
export function analyzeDiffSync(options) {
|
|
628
|
+
const ref = options.ref || 'HEAD';
|
|
629
|
+
// Check analysis cache
|
|
630
|
+
const cached = analysisCache.get(ref);
|
|
631
|
+
if (cached && Date.now() - cached.timestamp < ANALYSIS_CACHE_TTL_MS) {
|
|
632
|
+
return cached.result;
|
|
633
|
+
}
|
|
634
|
+
const files = getGitDiffNumstat(ref);
|
|
635
|
+
const fileRisks = files.map(assessFileRisk);
|
|
636
|
+
const risk = assessOverallRisk(files, fileRisks);
|
|
637
|
+
const classification = classifyDiff(files);
|
|
638
|
+
const recommendedReviewers = suggestReviewers(files, fileRisks);
|
|
639
|
+
const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
|
|
640
|
+
const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
|
641
|
+
const result = {
|
|
642
|
+
ref,
|
|
643
|
+
timestamp: Date.now(),
|
|
644
|
+
files,
|
|
645
|
+
risk,
|
|
646
|
+
classification,
|
|
647
|
+
summary: `${files.length} files changed (+${totalAdditions}/-${totalDeletions}), ${risk.overall} risk`,
|
|
648
|
+
fileRisks,
|
|
649
|
+
recommendedReviewers,
|
|
650
|
+
};
|
|
651
|
+
analysisCache.set(ref, { result, timestamp: Date.now() });
|
|
652
|
+
return result;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Clear all diff-related caches
|
|
656
|
+
*/
|
|
657
|
+
export function clearAllDiffCaches() {
|
|
658
|
+
diffCache.clear();
|
|
659
|
+
analysisCache.clear();
|
|
660
|
+
classifierInstance?.clearCache();
|
|
661
|
+
}
|
|
662
|
+
//# sourceMappingURL=diff-classifier.js.map
|