@juspay/yama 1.1.0 → 1.2.0
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/CHANGELOG.md +34 -1
- package/README.md +152 -120
- package/dist/cli/index.js +201 -200
- package/dist/core/ContextGatherer.d.ts +10 -5
- package/dist/core/ContextGatherer.js +176 -161
- package/dist/core/Guardian.d.ts +1 -1
- package/dist/core/Guardian.js +126 -122
- package/dist/core/providers/BitbucketProvider.d.ts +3 -3
- package/dist/core/providers/BitbucketProvider.js +129 -121
- package/dist/features/CodeReviewer.d.ts +7 -3
- package/dist/features/CodeReviewer.js +314 -222
- package/dist/features/DescriptionEnhancer.d.ts +3 -3
- package/dist/features/DescriptionEnhancer.js +115 -94
- package/dist/index.d.ts +11 -11
- package/dist/index.js +10 -48
- package/dist/types/index.d.ts +27 -21
- package/dist/types/index.js +13 -18
- package/dist/utils/Cache.d.ts +6 -1
- package/dist/utils/Cache.js +78 -68
- package/dist/utils/ConfigManager.d.ts +5 -1
- package/dist/utils/ConfigManager.js +301 -253
- package/dist/utils/Logger.d.ts +2 -2
- package/dist/utils/Logger.js +69 -67
- package/dist/utils/MemoryBankManager.d.ts +73 -0
- package/dist/utils/MemoryBankManager.js +310 -0
- package/dist/utils/ProviderLimits.d.ts +58 -0
- package/dist/utils/ProviderLimits.js +143 -0
- package/package.json +7 -6
- package/yama.config.example.yaml +37 -21
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Unified Context Gatherer - The foundation for all Yama operations
|
|
3
3
|
* Gathers all necessary context once and reuses it across all operations
|
|
4
4
|
*/
|
|
5
|
-
import { PRIdentifier, PRInfo, PRDiff, AIProviderConfig, DiffStrategyConfig } from
|
|
6
|
-
import { BitbucketProvider } from
|
|
5
|
+
import { PRIdentifier, PRInfo, PRDiff, AIProviderConfig, DiffStrategyConfig, MemoryBankConfig } from "../types/index.js";
|
|
6
|
+
import { BitbucketProvider } from "./providers/BitbucketProvider.js";
|
|
7
7
|
export interface ProjectContext {
|
|
8
8
|
memoryBank: {
|
|
9
9
|
summary: string;
|
|
@@ -15,7 +15,7 @@ export interface ProjectContext {
|
|
|
15
15
|
filesProcessed: number;
|
|
16
16
|
}
|
|
17
17
|
export interface DiffStrategy {
|
|
18
|
-
strategy:
|
|
18
|
+
strategy: "whole" | "file-by-file";
|
|
19
19
|
reason: string;
|
|
20
20
|
fileCount: number;
|
|
21
21
|
estimatedSize: string;
|
|
@@ -36,8 +36,9 @@ export declare class ContextGatherer {
|
|
|
36
36
|
private neurolink;
|
|
37
37
|
private bitbucketProvider;
|
|
38
38
|
private aiConfig;
|
|
39
|
+
private memoryBankManager;
|
|
39
40
|
private startTime;
|
|
40
|
-
constructor(bitbucketProvider: BitbucketProvider, aiConfig: AIProviderConfig);
|
|
41
|
+
constructor(bitbucketProvider: BitbucketProvider, aiConfig: AIProviderConfig, memoryBankConfig: MemoryBankConfig);
|
|
41
42
|
/**
|
|
42
43
|
* Main context gathering method - used by all operations
|
|
43
44
|
*/
|
|
@@ -56,6 +57,10 @@ export declare class ContextGatherer {
|
|
|
56
57
|
* Step 2: Gather project context (memory bank + clinerules)
|
|
57
58
|
*/
|
|
58
59
|
private gatherProjectContext;
|
|
60
|
+
/**
|
|
61
|
+
* Get safe token limit based on AI provider using shared utility
|
|
62
|
+
*/
|
|
63
|
+
private getSafeTokenLimit;
|
|
59
64
|
/**
|
|
60
65
|
* Parse project context with AI
|
|
61
66
|
*/
|
|
@@ -101,5 +106,5 @@ export declare class ContextGatherer {
|
|
|
101
106
|
*/
|
|
102
107
|
getStats(): any;
|
|
103
108
|
}
|
|
104
|
-
export declare function createContextGatherer(bitbucketProvider: BitbucketProvider, aiConfig: AIProviderConfig): ContextGatherer;
|
|
109
|
+
export declare function createContextGatherer(bitbucketProvider: BitbucketProvider, aiConfig: AIProviderConfig, memoryBankConfig: MemoryBankConfig): ContextGatherer;
|
|
105
110
|
//# sourceMappingURL=ContextGatherer.d.ts.map
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Unified Context Gatherer - The foundation for all Yama operations
|
|
4
3
|
* Gathers all necessary context once and reuses it across all operations
|
|
5
4
|
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.ContextGatherer = void 0;
|
|
8
|
-
exports.createContextGatherer = createContextGatherer;
|
|
9
5
|
// NeuroLink will be dynamically imported
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
import { ProviderError, } from "../types/index.js";
|
|
7
|
+
import { logger } from "../utils/Logger.js";
|
|
8
|
+
import { cache, Cache } from "../utils/Cache.js";
|
|
9
|
+
import { createMemoryBankManager } from "../utils/MemoryBankManager.js";
|
|
10
|
+
import { getProviderTokenLimit } from "../utils/ProviderLimits.js";
|
|
11
|
+
export class ContextGatherer {
|
|
12
|
+
neurolink;
|
|
13
|
+
bitbucketProvider;
|
|
14
|
+
aiConfig;
|
|
15
|
+
memoryBankManager;
|
|
16
|
+
startTime = 0;
|
|
17
|
+
constructor(bitbucketProvider, aiConfig, memoryBankConfig) {
|
|
16
18
|
this.bitbucketProvider = bitbucketProvider;
|
|
17
19
|
this.aiConfig = aiConfig;
|
|
20
|
+
this.memoryBankManager = createMemoryBankManager(memoryBankConfig, bitbucketProvider);
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
23
|
* Main context gathering method - used by all operations
|
|
@@ -23,29 +26,29 @@ class ContextGatherer {
|
|
|
23
26
|
this.startTime = Date.now();
|
|
24
27
|
const contextId = this.generateContextId(identifier);
|
|
25
28
|
const cacheHits = [];
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
logger.phase("🔍 Gathering unified context...");
|
|
30
|
+
logger.info(`Target: ${identifier.workspace}/${identifier.repository}`);
|
|
28
31
|
try {
|
|
29
32
|
// Step 1: Find and get PR information
|
|
30
33
|
const pr = await this.findAndGetPR(identifier, cacheHits, options.forceRefresh);
|
|
31
34
|
const completeIdentifier = {
|
|
32
35
|
...identifier,
|
|
33
|
-
pullRequestId: pr.id
|
|
36
|
+
pullRequestId: pr.id,
|
|
34
37
|
};
|
|
35
38
|
// Step 2: Gather project context (memory bank + clinerules)
|
|
36
39
|
const projectContext = await this.gatherProjectContext(completeIdentifier, cacheHits, options.forceRefresh);
|
|
37
40
|
// Step 3: Determine diff strategy based on file count and config
|
|
38
41
|
const diffStrategy = this.determineDiffStrategy(pr.fileChanges || [], options.diffStrategyConfig);
|
|
39
|
-
|
|
42
|
+
logger.info(`Diff strategy: ${diffStrategy.strategy} (${diffStrategy.reason})`);
|
|
40
43
|
// Step 4: Get diff data based on strategy (if requested)
|
|
41
44
|
let prDiff;
|
|
42
45
|
let fileDiffs;
|
|
43
46
|
if (options.includeDiff !== false) {
|
|
44
|
-
if (diffStrategy.strategy ===
|
|
45
|
-
prDiff = await this.getPRDiff(completeIdentifier, options.contextLines || 3, options.excludePatterns || [
|
|
47
|
+
if (diffStrategy.strategy === "whole") {
|
|
48
|
+
prDiff = await this.getPRDiff(completeIdentifier, options.contextLines || 3, options.excludePatterns || ["*.lock", "*.svg"], cacheHits, options.forceRefresh);
|
|
46
49
|
}
|
|
47
50
|
else {
|
|
48
|
-
fileDiffs = await this.getFileByFileDiffs(completeIdentifier, pr.fileChanges || [], options.contextLines || 3, options.excludePatterns || [
|
|
51
|
+
fileDiffs = await this.getFileByFileDiffs(completeIdentifier, pr.fileChanges || [], options.contextLines || 3, options.excludePatterns || ["*.lock", "*.svg"], cacheHits, options.forceRefresh);
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
const gatheringDuration = Date.now() - this.startTime;
|
|
@@ -59,108 +62,99 @@ class ContextGatherer {
|
|
|
59
62
|
contextId,
|
|
60
63
|
gatheredAt: new Date().toISOString(),
|
|
61
64
|
cacheHits,
|
|
62
|
-
gatheringDuration
|
|
65
|
+
gatheringDuration,
|
|
63
66
|
};
|
|
64
|
-
|
|
67
|
+
logger.success(`Context gathered in ${Math.round(gatheringDuration / 1000)}s ` +
|
|
65
68
|
`(${cacheHits.length} cache hits, ${diffStrategy.fileCount} files, ${diffStrategy.estimatedSize})`);
|
|
66
69
|
// Cache the complete context for reuse
|
|
67
70
|
this.cacheContext(context);
|
|
68
71
|
return context;
|
|
69
72
|
}
|
|
70
73
|
catch (error) {
|
|
71
|
-
|
|
72
|
-
throw new
|
|
74
|
+
logger.error(`Context gathering failed: ${error.message}`);
|
|
75
|
+
throw new ProviderError(`Failed to gather context: ${error.message}`);
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
/**
|
|
76
79
|
* Step 1: Find PR and get detailed information
|
|
77
80
|
*/
|
|
78
81
|
async findAndGetPR(identifier, cacheHits, forceRefresh = false) {
|
|
79
|
-
|
|
82
|
+
logger.debug("Step 1: Finding and getting PR information...");
|
|
80
83
|
// If PR ID is provided, get details directly
|
|
81
84
|
if (identifier.pullRequestId) {
|
|
82
|
-
const cacheKey =
|
|
83
|
-
if (!forceRefresh &&
|
|
84
|
-
cacheHits.push(
|
|
85
|
+
const cacheKey = Cache.keys.prInfo(identifier.workspace, identifier.repository, identifier.pullRequestId);
|
|
86
|
+
if (!forceRefresh && cache.has(cacheKey)) {
|
|
87
|
+
cacheHits.push("pr-details");
|
|
85
88
|
}
|
|
86
|
-
return
|
|
87
|
-
|
|
89
|
+
return cache.getOrSet(cacheKey, async () => {
|
|
90
|
+
logger.debug(`Getting PR details: ${identifier.workspace}/${identifier.repository}#${identifier.pullRequestId}`);
|
|
88
91
|
return await this.bitbucketProvider.getPRDetails(identifier);
|
|
89
|
-
}, 1800
|
|
90
|
-
);
|
|
92
|
+
}, 1800);
|
|
91
93
|
}
|
|
92
94
|
// If branch is provided, find PR first
|
|
93
95
|
if (identifier.branch) {
|
|
94
|
-
const branchCacheKey =
|
|
95
|
-
if (!forceRefresh &&
|
|
96
|
-
cacheHits.push(
|
|
96
|
+
const branchCacheKey = Cache.keys.branchInfo(identifier.workspace, identifier.repository, identifier.branch);
|
|
97
|
+
if (!forceRefresh && cache.has(branchCacheKey)) {
|
|
98
|
+
cacheHits.push("branch-pr-lookup");
|
|
97
99
|
}
|
|
98
|
-
const prInfo = await
|
|
99
|
-
|
|
100
|
+
const prInfo = await cache.getOrSet(branchCacheKey, async () => {
|
|
101
|
+
logger.debug(`Finding PR for branch: ${identifier.workspace}/${identifier.repository}@${identifier.branch}`);
|
|
100
102
|
return await this.bitbucketProvider.findPRForBranch(identifier);
|
|
101
|
-
}, 3600
|
|
102
|
-
);
|
|
103
|
+
}, 3600);
|
|
103
104
|
// Now get full PR details
|
|
104
|
-
const detailsCacheKey =
|
|
105
|
-
if (!forceRefresh &&
|
|
106
|
-
cacheHits.push(
|
|
105
|
+
const detailsCacheKey = Cache.keys.prInfo(identifier.workspace, identifier.repository, prInfo.id);
|
|
106
|
+
if (!forceRefresh && cache.has(detailsCacheKey)) {
|
|
107
|
+
cacheHits.push("pr-details-from-branch");
|
|
107
108
|
}
|
|
108
|
-
return
|
|
109
|
+
return cache.getOrSet(detailsCacheKey, async () => {
|
|
109
110
|
return await this.bitbucketProvider.getPRDetails({
|
|
110
111
|
...identifier,
|
|
111
|
-
pullRequestId: prInfo.id
|
|
112
|
+
pullRequestId: prInfo.id,
|
|
112
113
|
});
|
|
113
|
-
}, 1800
|
|
114
|
-
);
|
|
114
|
+
}, 1800);
|
|
115
115
|
}
|
|
116
|
-
throw new
|
|
116
|
+
throw new ProviderError("Either pullRequestId or branch must be provided");
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
119
|
* Step 2: Gather project context (memory bank + clinerules)
|
|
120
120
|
*/
|
|
121
121
|
async gatherProjectContext(identifier, cacheHits, forceRefresh = false) {
|
|
122
|
-
|
|
123
|
-
const cacheKey =
|
|
124
|
-
if (!forceRefresh &&
|
|
125
|
-
cacheHits.push(
|
|
122
|
+
logger.debug("Step 2: Gathering project context...");
|
|
123
|
+
const cacheKey = Cache.keys.projectContext(identifier.workspace, identifier.repository, identifier.branch || "main");
|
|
124
|
+
if (!forceRefresh && cache.has(cacheKey)) {
|
|
125
|
+
cacheHits.push("project-context");
|
|
126
126
|
}
|
|
127
|
-
return
|
|
127
|
+
return cache.getOrSet(cacheKey, async () => {
|
|
128
128
|
try {
|
|
129
|
-
//
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
129
|
+
// Use MemoryBankManager to get memory bank files
|
|
130
|
+
const memoryBankResult = await this.memoryBankManager.getMemoryBankFiles(identifier, forceRefresh);
|
|
131
|
+
if (memoryBankResult.files.length === 0) {
|
|
132
|
+
logger.debug("No memory bank files found");
|
|
133
133
|
return {
|
|
134
134
|
memoryBank: {
|
|
135
|
-
summary:
|
|
136
|
-
projectContext:
|
|
137
|
-
patterns:
|
|
138
|
-
standards:
|
|
135
|
+
summary: "No project context available",
|
|
136
|
+
projectContext: "None",
|
|
137
|
+
patterns: "None",
|
|
138
|
+
standards: "None",
|
|
139
139
|
},
|
|
140
|
-
clinerules:
|
|
141
|
-
filesProcessed: 0
|
|
140
|
+
clinerules: "",
|
|
141
|
+
filesProcessed: 0,
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
|
-
//
|
|
144
|
+
// Convert MemoryBankFile[] to Record<string, string> for AI processing
|
|
145
145
|
const fileContents = {};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
Logger_1.logger.debug(`✓ Got content for: ${file.name}`);
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
Logger_1.logger.debug(`Could not read file ${file.name}: ${error.message}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
146
|
+
memoryBankResult.files.forEach((file) => {
|
|
147
|
+
fileContents[file.name] = file.content;
|
|
148
|
+
});
|
|
149
|
+
logger.debug(`✓ Loaded ${memoryBankResult.files.length} memory bank files from ${memoryBankResult.resolvedPath}${memoryBankResult.fallbackUsed ? " (fallback)" : ""}`);
|
|
156
150
|
// Get .clinerules file
|
|
157
|
-
let clinerules =
|
|
151
|
+
let clinerules = "";
|
|
158
152
|
try {
|
|
159
|
-
clinerules = await this.bitbucketProvider.getFileContent(identifier.workspace, identifier.repository,
|
|
160
|
-
|
|
153
|
+
clinerules = await this.bitbucketProvider.getFileContent(identifier.workspace, identifier.repository, ".clinerules", identifier.branch || "main");
|
|
154
|
+
logger.debug("✓ Got .clinerules content");
|
|
161
155
|
}
|
|
162
156
|
catch (error) {
|
|
163
|
-
|
|
157
|
+
logger.debug(`Could not read .clinerules: ${error.message}`);
|
|
164
158
|
}
|
|
165
159
|
// Parse and summarize with AI
|
|
166
160
|
const contextData = await this.parseProjectContextWithAI(fileContents, clinerules);
|
|
@@ -171,27 +165,43 @@ Patterns: ${contextData.patterns}
|
|
|
171
165
|
Standards: ${contextData.standards}`,
|
|
172
166
|
projectContext: contextData.projectContext,
|
|
173
167
|
patterns: contextData.patterns,
|
|
174
|
-
standards: contextData.standards
|
|
168
|
+
standards: contextData.standards,
|
|
175
169
|
},
|
|
176
170
|
clinerules,
|
|
177
|
-
filesProcessed:
|
|
171
|
+
filesProcessed: memoryBankResult.filesProcessed,
|
|
178
172
|
};
|
|
179
173
|
}
|
|
180
174
|
catch (error) {
|
|
181
|
-
|
|
175
|
+
logger.debug(`Failed to gather project context: ${error.message}`);
|
|
182
176
|
return {
|
|
183
177
|
memoryBank: {
|
|
184
|
-
summary:
|
|
185
|
-
projectContext:
|
|
186
|
-
patterns:
|
|
187
|
-
standards:
|
|
178
|
+
summary: "Context gathering failed",
|
|
179
|
+
projectContext: "Failed to load",
|
|
180
|
+
patterns: "Failed to load",
|
|
181
|
+
standards: "Failed to load",
|
|
188
182
|
},
|
|
189
|
-
clinerules:
|
|
190
|
-
filesProcessed: 0
|
|
183
|
+
clinerules: "",
|
|
184
|
+
filesProcessed: 0,
|
|
191
185
|
};
|
|
192
186
|
}
|
|
193
|
-
}, 7200
|
|
194
|
-
|
|
187
|
+
}, 7200);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get safe token limit based on AI provider using shared utility
|
|
191
|
+
*/
|
|
192
|
+
getSafeTokenLimit() {
|
|
193
|
+
const provider = this.aiConfig.provider || "auto";
|
|
194
|
+
const configuredTokens = this.aiConfig.maxTokens;
|
|
195
|
+
// Use conservative limits for ContextGatherer (safer for large context processing)
|
|
196
|
+
const providerLimit = getProviderTokenLimit(provider, true);
|
|
197
|
+
// Use the smaller of configured tokens or provider limit
|
|
198
|
+
if (configuredTokens && configuredTokens > 0) {
|
|
199
|
+
const safeLimit = Math.min(configuredTokens, providerLimit);
|
|
200
|
+
logger.debug(`Token limit: configured=${configuredTokens}, provider=${providerLimit}, using=${safeLimit}`);
|
|
201
|
+
return safeLimit;
|
|
202
|
+
}
|
|
203
|
+
logger.debug(`Token limit: using provider default=${providerLimit} for ${provider}`);
|
|
204
|
+
return providerLimit;
|
|
195
205
|
}
|
|
196
206
|
/**
|
|
197
207
|
* Parse project context with AI
|
|
@@ -213,50 +223,54 @@ Extract and summarize the content and return ONLY this JSON format:
|
|
|
213
223
|
try {
|
|
214
224
|
// Initialize NeuroLink with eval-based dynamic import
|
|
215
225
|
if (!this.neurolink) {
|
|
216
|
-
const
|
|
217
|
-
const { NeuroLink } = await dynamicImport('@juspay/neurolink');
|
|
226
|
+
const { NeuroLink } = await import("@juspay/neurolink");
|
|
218
227
|
this.neurolink = new NeuroLink();
|
|
219
228
|
}
|
|
220
229
|
// Context for project analysis
|
|
221
230
|
const aiContext = {
|
|
222
|
-
operation:
|
|
231
|
+
operation: "project-context-analysis",
|
|
223
232
|
fileCount: Object.keys(fileContents).length,
|
|
224
233
|
hasClinerules: !!clinerules,
|
|
225
|
-
analysisType:
|
|
234
|
+
analysisType: "memory-bank-synthesis",
|
|
226
235
|
};
|
|
236
|
+
// Get safe token limit based on provider
|
|
237
|
+
const safeMaxTokens = this.getSafeTokenLimit();
|
|
238
|
+
logger.debug(`Using AI provider: ${this.aiConfig.provider || "auto"}`);
|
|
239
|
+
logger.debug(`Configured maxTokens: ${this.aiConfig.maxTokens}`);
|
|
240
|
+
logger.debug(`Safe maxTokens limit: ${safeMaxTokens}`);
|
|
227
241
|
const result = await this.neurolink.generate({
|
|
228
242
|
input: { text: prompt },
|
|
229
|
-
systemPrompt:
|
|
243
|
+
systemPrompt: "You are an Expert Project Analyst. Synthesize project context from documentation and configuration files to help AI understand the codebase architecture, patterns, and business domain.",
|
|
230
244
|
provider: this.aiConfig.provider,
|
|
231
245
|
model: this.aiConfig.model,
|
|
232
246
|
temperature: 0.3,
|
|
233
|
-
maxTokens:
|
|
234
|
-
timeout:
|
|
247
|
+
maxTokens: safeMaxTokens, // Use provider-aware safe token limit
|
|
248
|
+
timeout: "10m", // Allow longer processing for quality
|
|
235
249
|
context: aiContext,
|
|
236
250
|
enableAnalytics: this.aiConfig.enableAnalytics || true,
|
|
237
|
-
enableEvaluation: false // Not needed for context synthesis
|
|
251
|
+
enableEvaluation: false, // Not needed for context synthesis
|
|
238
252
|
});
|
|
239
253
|
// Log context analysis
|
|
240
254
|
if (result.analytics) {
|
|
241
|
-
|
|
255
|
+
logger.debug(`Context Analysis - Files: ${Object.keys(fileContents).length}, Provider: ${result.provider}`);
|
|
242
256
|
}
|
|
243
257
|
// Modern NeuroLink returns { content: string }
|
|
244
258
|
const response = this.parseAIResponse(result);
|
|
245
259
|
if (response.success) {
|
|
246
260
|
return {
|
|
247
|
-
projectContext: response.projectContext ||
|
|
248
|
-
patterns: response.patterns ||
|
|
249
|
-
standards: response.standards ||
|
|
261
|
+
projectContext: response.projectContext || "None",
|
|
262
|
+
patterns: response.patterns || "None",
|
|
263
|
+
standards: response.standards || "None",
|
|
250
264
|
};
|
|
251
265
|
}
|
|
252
|
-
throw new Error(
|
|
266
|
+
throw new Error("AI parsing failed");
|
|
253
267
|
}
|
|
254
268
|
catch (error) {
|
|
255
|
-
|
|
269
|
+
logger.warn(`AI context parsing failed, using fallback: ${error.message}`);
|
|
256
270
|
return {
|
|
257
|
-
projectContext:
|
|
258
|
-
patterns:
|
|
259
|
-
standards:
|
|
271
|
+
projectContext: "AI parsing unavailable",
|
|
272
|
+
patterns: "Standard patterns assumed",
|
|
273
|
+
standards: "Standard quality requirements",
|
|
260
274
|
};
|
|
261
275
|
}
|
|
262
276
|
}
|
|
@@ -267,94 +281,96 @@ Extract and summarize the content and return ONLY this JSON format:
|
|
|
267
281
|
const fileCount = fileChanges.length;
|
|
268
282
|
// Get threshold values from config or use defaults
|
|
269
283
|
const wholeDiffMaxFiles = config?.thresholds?.wholeDiffMaxFiles ?? 2;
|
|
270
|
-
// Note: fileByFileMinFiles is currently same as wholeDiffMaxFiles + 1
|
|
284
|
+
// Note: fileByFileMinFiles is currently same as wholeDiffMaxFiles + 1
|
|
271
285
|
// but kept separate for future flexibility
|
|
272
286
|
// Check if force strategy is configured
|
|
273
|
-
if (config?.forceStrategy && config.forceStrategy !==
|
|
287
|
+
if (config?.forceStrategy && config.forceStrategy !== "auto") {
|
|
274
288
|
return {
|
|
275
289
|
strategy: config.forceStrategy,
|
|
276
290
|
reason: `Forced by configuration`,
|
|
277
291
|
fileCount,
|
|
278
|
-
estimatedSize: this.estimateDiffSize(fileCount)
|
|
292
|
+
estimatedSize: this.estimateDiffSize(fileCount),
|
|
279
293
|
};
|
|
280
294
|
}
|
|
281
295
|
// Determine strategy based on thresholds
|
|
282
|
-
let strategy =
|
|
283
|
-
let reason =
|
|
296
|
+
let strategy = "whole";
|
|
297
|
+
let reason = "";
|
|
284
298
|
if (fileCount === 0) {
|
|
285
|
-
strategy =
|
|
286
|
-
reason =
|
|
299
|
+
strategy = "whole";
|
|
300
|
+
reason = "No files to analyze";
|
|
287
301
|
}
|
|
288
302
|
else if (fileCount <= wholeDiffMaxFiles) {
|
|
289
|
-
strategy =
|
|
303
|
+
strategy = "whole";
|
|
290
304
|
reason = `${fileCount} file(s) ≤ ${wholeDiffMaxFiles} (threshold), using whole diff`;
|
|
291
305
|
}
|
|
292
306
|
else {
|
|
293
|
-
strategy =
|
|
307
|
+
strategy = "file-by-file";
|
|
294
308
|
reason = `${fileCount} file(s) > ${wholeDiffMaxFiles} (threshold), using file-by-file`;
|
|
295
309
|
}
|
|
296
310
|
return {
|
|
297
311
|
strategy,
|
|
298
312
|
reason,
|
|
299
313
|
fileCount,
|
|
300
|
-
estimatedSize: this.estimateDiffSize(fileCount)
|
|
314
|
+
estimatedSize: this.estimateDiffSize(fileCount),
|
|
301
315
|
};
|
|
302
316
|
}
|
|
303
317
|
/**
|
|
304
318
|
* Estimate diff size based on file count
|
|
305
319
|
*/
|
|
306
320
|
estimateDiffSize(fileCount) {
|
|
307
|
-
if (fileCount === 0)
|
|
308
|
-
return
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (fileCount <=
|
|
314
|
-
return
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
321
|
+
if (fileCount === 0) {
|
|
322
|
+
return "0 KB";
|
|
323
|
+
}
|
|
324
|
+
if (fileCount <= 2) {
|
|
325
|
+
return "Small (~5-20 KB)";
|
|
326
|
+
}
|
|
327
|
+
if (fileCount <= 5) {
|
|
328
|
+
return "Small (~10-50 KB)";
|
|
329
|
+
}
|
|
330
|
+
if (fileCount <= 20) {
|
|
331
|
+
return "Medium (~50-200 KB)";
|
|
332
|
+
}
|
|
333
|
+
if (fileCount <= 50) {
|
|
334
|
+
return "Large (~200-500 KB)";
|
|
335
|
+
}
|
|
336
|
+
return "Very Large (>500 KB)";
|
|
318
337
|
}
|
|
319
338
|
/**
|
|
320
339
|
* Get whole PR diff
|
|
321
340
|
*/
|
|
322
341
|
async getPRDiff(identifier, contextLines, excludePatterns, cacheHits, forceRefresh = false) {
|
|
323
|
-
|
|
324
|
-
const cacheKey =
|
|
325
|
-
if (!forceRefresh &&
|
|
326
|
-
cacheHits.push(
|
|
342
|
+
logger.debug("Getting whole PR diff...");
|
|
343
|
+
const cacheKey = Cache.keys.prDiff(identifier.workspace, identifier.repository, identifier.pullRequestId);
|
|
344
|
+
if (!forceRefresh && cache.has(cacheKey)) {
|
|
345
|
+
cacheHits.push("pr-diff");
|
|
327
346
|
}
|
|
328
|
-
return
|
|
347
|
+
return cache.getOrSet(cacheKey, async () => {
|
|
329
348
|
return await this.bitbucketProvider.getPRDiff(identifier, contextLines, excludePatterns);
|
|
330
|
-
}, 1800
|
|
331
|
-
);
|
|
349
|
+
}, 1800);
|
|
332
350
|
}
|
|
333
351
|
/**
|
|
334
352
|
* Get file-by-file diffs for large changesets
|
|
335
353
|
*/
|
|
336
354
|
async getFileByFileDiffs(identifier, fileChanges, contextLines, excludePatterns, cacheHits, forceRefresh = false) {
|
|
337
|
-
|
|
355
|
+
logger.debug(`Getting file-by-file diffs for ${fileChanges.length} files...`);
|
|
338
356
|
const fileDiffs = new Map();
|
|
339
357
|
// Filter out excluded files
|
|
340
|
-
const filteredFiles = fileChanges.filter(file => !excludePatterns.some(pattern => new RegExp(pattern.replace(/\*/g,
|
|
341
|
-
|
|
358
|
+
const filteredFiles = fileChanges.filter((file) => !excludePatterns.some((pattern) => new RegExp(pattern.replace(/\*/g, ".*")).test(file)));
|
|
359
|
+
logger.debug(`Processing ${filteredFiles.length} files after exclusions`);
|
|
342
360
|
// Process files in batches for better performance
|
|
343
361
|
const batchSize = 5;
|
|
344
362
|
for (let i = 0; i < filteredFiles.length; i += batchSize) {
|
|
345
363
|
const batch = filteredFiles.slice(i, i + batchSize);
|
|
346
364
|
const batchPromises = batch.map(async (file) => {
|
|
347
365
|
const fileCacheKey = `file-diff:${identifier.workspace}:${identifier.repository}:${identifier.pullRequestId}:${file}`;
|
|
348
|
-
if (!forceRefresh &&
|
|
366
|
+
if (!forceRefresh && cache.has(fileCacheKey)) {
|
|
349
367
|
cacheHits.push(`file-diff-${file}`);
|
|
350
368
|
}
|
|
351
|
-
return
|
|
369
|
+
return cache.getOrSet(fileCacheKey, async () => {
|
|
352
370
|
// Use include_patterns to get diff for just this file
|
|
353
|
-
const fileDiff = await this.bitbucketProvider.getPRDiff(identifier, contextLines, excludePatterns, [file]
|
|
354
|
-
);
|
|
371
|
+
const fileDiff = await this.bitbucketProvider.getPRDiff(identifier, contextLines, excludePatterns, [file]);
|
|
355
372
|
return fileDiff.diff;
|
|
356
|
-
}, 1800
|
|
357
|
-
);
|
|
373
|
+
}, 1800);
|
|
358
374
|
});
|
|
359
375
|
const batchResults = await Promise.all(batchPromises);
|
|
360
376
|
batch.forEach((file, index) => {
|
|
@@ -362,10 +378,10 @@ Extract and summarize the content and return ONLY this JSON format:
|
|
|
362
378
|
});
|
|
363
379
|
// Small delay between batches to avoid overwhelming the API
|
|
364
380
|
if (i + batchSize < filteredFiles.length) {
|
|
365
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
381
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
366
382
|
}
|
|
367
383
|
}
|
|
368
|
-
|
|
384
|
+
logger.debug(`✓ Got diffs for ${fileDiffs.size} files`);
|
|
369
385
|
return fileDiffs;
|
|
370
386
|
}
|
|
371
387
|
/**
|
|
@@ -373,12 +389,12 @@ Extract and summarize the content and return ONLY this JSON format:
|
|
|
373
389
|
*/
|
|
374
390
|
cacheContext(context) {
|
|
375
391
|
const contextCacheKey = `context:${context.contextId}`;
|
|
376
|
-
|
|
392
|
+
cache.set(contextCacheKey, context, 1800); // 30 minutes
|
|
377
393
|
// Tag it for easy invalidation
|
|
378
|
-
|
|
394
|
+
cache.setWithTags(contextCacheKey, context, [
|
|
379
395
|
`workspace:${context.identifier.workspace}`,
|
|
380
396
|
`repository:${context.identifier.repository}`,
|
|
381
|
-
`pr:${context.identifier.pullRequestId}
|
|
397
|
+
`pr:${context.identifier.pullRequestId}`,
|
|
382
398
|
], 1800);
|
|
383
399
|
}
|
|
384
400
|
/**
|
|
@@ -387,9 +403,9 @@ Extract and summarize the content and return ONLY this JSON format:
|
|
|
387
403
|
async getCachedContext(identifier) {
|
|
388
404
|
const contextId = this.generateContextId(identifier);
|
|
389
405
|
const contextCacheKey = `context:${contextId}`;
|
|
390
|
-
const cached =
|
|
406
|
+
const cached = cache.get(contextCacheKey);
|
|
391
407
|
if (cached) {
|
|
392
|
-
|
|
408
|
+
logger.debug(`✓ Using cached context: ${contextId}`);
|
|
393
409
|
return cached;
|
|
394
410
|
}
|
|
395
411
|
return null;
|
|
@@ -398,9 +414,9 @@ Extract and summarize the content and return ONLY this JSON format:
|
|
|
398
414
|
* Invalidate context cache for a specific PR
|
|
399
415
|
*/
|
|
400
416
|
invalidateContext(identifier) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
417
|
+
cache.invalidateTag(`pr:${identifier.pullRequestId}`);
|
|
418
|
+
cache.invalidateTag(`workspace:${identifier.workspace}`);
|
|
419
|
+
logger.debug(`Context cache invalidated for PR ${identifier.pullRequestId}`);
|
|
404
420
|
}
|
|
405
421
|
/**
|
|
406
422
|
* Generate unique context ID
|
|
@@ -409,11 +425,11 @@ Extract and summarize the content and return ONLY this JSON format:
|
|
|
409
425
|
const parts = [
|
|
410
426
|
identifier.workspace,
|
|
411
427
|
identifier.repository,
|
|
412
|
-
identifier.pullRequestId || identifier.branch ||
|
|
428
|
+
identifier.pullRequestId || identifier.branch || "unknown",
|
|
413
429
|
];
|
|
414
|
-
return Buffer.from(parts.join(
|
|
415
|
-
.toString(
|
|
416
|
-
.replace(/[+/=]/g,
|
|
430
|
+
return Buffer.from(parts.join(":"))
|
|
431
|
+
.toString("base64")
|
|
432
|
+
.replace(/[+/=]/g, "")
|
|
417
433
|
.substring(0, 16);
|
|
418
434
|
}
|
|
419
435
|
/**
|
|
@@ -421,16 +437,16 @@ Extract and summarize the content and return ONLY this JSON format:
|
|
|
421
437
|
*/
|
|
422
438
|
parseAIResponse(result) {
|
|
423
439
|
try {
|
|
424
|
-
const responseText = result.content || result.text || result.response ||
|
|
440
|
+
const responseText = result.content || result.text || result.response || "";
|
|
425
441
|
if (!responseText) {
|
|
426
|
-
return { success: false, error:
|
|
442
|
+
return { success: false, error: "Empty response" };
|
|
427
443
|
}
|
|
428
444
|
// Find JSON in response
|
|
429
445
|
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
|
430
446
|
if (jsonMatch) {
|
|
431
447
|
return JSON.parse(jsonMatch[0]);
|
|
432
448
|
}
|
|
433
|
-
return { success: false, error:
|
|
449
|
+
return { success: false, error: "No JSON found" };
|
|
434
450
|
}
|
|
435
451
|
catch (error) {
|
|
436
452
|
return { success: false, error: error.message };
|
|
@@ -442,14 +458,13 @@ Extract and summarize the content and return ONLY this JSON format:
|
|
|
442
458
|
getStats() {
|
|
443
459
|
return {
|
|
444
460
|
lastGatheringDuration: this.startTime ? Date.now() - this.startTime : 0,
|
|
445
|
-
cacheStats:
|
|
446
|
-
cacheHitRatio:
|
|
461
|
+
cacheStats: cache.stats(),
|
|
462
|
+
cacheHitRatio: cache.getHitRatio(),
|
|
447
463
|
};
|
|
448
464
|
}
|
|
449
465
|
}
|
|
450
|
-
exports.ContextGatherer = ContextGatherer;
|
|
451
466
|
// Export factory function
|
|
452
|
-
function createContextGatherer(bitbucketProvider, aiConfig) {
|
|
453
|
-
return new ContextGatherer(bitbucketProvider, aiConfig);
|
|
467
|
+
export function createContextGatherer(bitbucketProvider, aiConfig, memoryBankConfig) {
|
|
468
|
+
return new ContextGatherer(bitbucketProvider, aiConfig, memoryBankConfig);
|
|
454
469
|
}
|
|
455
470
|
//# sourceMappingURL=ContextGatherer.js.map
|