@juspay/yama 1.2.0 → 1.3.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 +11 -9
- package/dist/core/providers/BitbucketProvider.js +1 -1
- package/dist/features/CodeReviewer.d.ts +45 -1
- package/dist/features/CodeReviewer.js +428 -25
- package/dist/types/index.d.ts +38 -0
- package/dist/utils/Cache.js +1 -1
- package/package.json +30 -5
- package/yama.config.example.yaml +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
# [1.
|
|
1
|
+
# [1.3.0](https://github.com/juspay/yama/compare/v1.2.0...v1.3.0) (2025-09-01)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **github:** implement comprehensive automation with proper Yama branding ([a03cb7f](https://github.com/juspay/yama/commit/a03cb7f499ea7793d626686beebde907551035d0))
|
|
2
6
|
|
|
7
|
+
# [1.2.0](https://github.com/juspay/yama/compare/v1.1.1...v1.2.0) (2025-08-08)
|
|
3
8
|
|
|
4
9
|
### Features
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
- **Memory:** support memory bank path and maxToken from config file ([1bc69d5](https://github.com/juspay/yama/commit/1bc69d5bda3ac5868d7537b881007beaf6916476))
|
|
7
12
|
|
|
8
13
|
## [1.1.1](https://github.com/juspay/yama/compare/v1.1.0...v1.1.1) (2025-07-28)
|
|
9
14
|
|
|
10
|
-
|
|
11
15
|
### Bug Fixes
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
- bump version to 1.2.1 ([8964645](https://github.com/juspay/yama/commit/89646450a7dec6ffcc3ad7fb745e1414fc751d4f))
|
|
14
18
|
|
|
15
19
|
# [1.1.0](https://github.com/juspay/yama/compare/v1.0.0...v1.1.0) (2025-07-28)
|
|
16
20
|
|
|
17
|
-
|
|
18
21
|
### Features
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
- migrate from CommonJS to ESM modules ([b45559f](https://github.com/juspay/yama/commit/b45559f86d37ab3516079becfa56a9f73ff8f062))
|
|
21
24
|
|
|
22
25
|
# 1.0.0 (2025-07-25)
|
|
23
26
|
|
|
24
|
-
|
|
25
27
|
### Features
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
- add enterprise-grade CI/CD pipeline and release automation ([e385d69](https://github.com/juspay/yama/commit/e385d69d135bee72f51ac4462adcfc9a4a4be17b))
|
|
30
|
+
- v1.1.0 - Enhanced AI configuration and performance improvements ([e763e93](https://github.com/juspay/yama/commit/e763e9341c2869433097b7a6bcc9080028934e1b))
|
|
29
31
|
|
|
30
32
|
# Changelog
|
|
31
33
|
|
|
@@ -27,7 +27,7 @@ export class BitbucketProvider {
|
|
|
27
27
|
}
|
|
28
28
|
try {
|
|
29
29
|
logger.debug("Initializing Bitbucket MCP handlers...");
|
|
30
|
-
const dynamicImport =
|
|
30
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
31
31
|
const [{ BitbucketApiClient }, { BranchHandlers }, { PullRequestHandlers }, { ReviewHandlers }, { FileHandlers },] = await Promise.all([
|
|
32
32
|
dynamicImport("@nexus2520/bitbucket-mcp-server/build/utils/api-client.js"),
|
|
33
33
|
dynamicImport("@nexus2520/bitbucket-mcp-server/build/handlers/branch-handlers.js"),
|
|
@@ -12,7 +12,7 @@ export declare class CodeReviewer {
|
|
|
12
12
|
private reviewConfig;
|
|
13
13
|
constructor(bitbucketProvider: BitbucketProvider, aiConfig: AIProviderConfig, reviewConfig: CodeReviewConfig);
|
|
14
14
|
/**
|
|
15
|
-
* Review code using pre-gathered unified context (OPTIMIZED)
|
|
15
|
+
* Review code using pre-gathered unified context (OPTIMIZED with Batch Processing)
|
|
16
16
|
*/
|
|
17
17
|
reviewCodeWithContext(context: UnifiedContext, options: ReviewOptions): Promise<ReviewResult>;
|
|
18
18
|
/**
|
|
@@ -92,6 +92,46 @@ export declare class CodeReviewer {
|
|
|
92
92
|
private groupViolationsByCategory;
|
|
93
93
|
private calculateStats;
|
|
94
94
|
private generateReviewResult;
|
|
95
|
+
/**
|
|
96
|
+
* Get batch processing configuration with defaults
|
|
97
|
+
*/
|
|
98
|
+
private getBatchProcessingConfig;
|
|
99
|
+
/**
|
|
100
|
+
* Determine if batch processing should be used
|
|
101
|
+
*/
|
|
102
|
+
private shouldUseBatchProcessing;
|
|
103
|
+
/**
|
|
104
|
+
* Main batch processing method
|
|
105
|
+
*/
|
|
106
|
+
private reviewWithBatchProcessing;
|
|
107
|
+
/**
|
|
108
|
+
* Prioritize files based on security importance and file type
|
|
109
|
+
*/
|
|
110
|
+
private prioritizeFiles;
|
|
111
|
+
/**
|
|
112
|
+
* Calculate file priority based on path and content
|
|
113
|
+
*/
|
|
114
|
+
private calculateFilePriority;
|
|
115
|
+
/**
|
|
116
|
+
* Estimate token count for a file
|
|
117
|
+
*/
|
|
118
|
+
private estimateFileTokens;
|
|
119
|
+
/**
|
|
120
|
+
* Create batches from prioritized files
|
|
121
|
+
*/
|
|
122
|
+
private createBatches;
|
|
123
|
+
/**
|
|
124
|
+
* Process a single batch of files
|
|
125
|
+
*/
|
|
126
|
+
private processBatch;
|
|
127
|
+
/**
|
|
128
|
+
* Create context for a specific batch
|
|
129
|
+
*/
|
|
130
|
+
private createBatchContext;
|
|
131
|
+
/**
|
|
132
|
+
* Build analysis prompt for a specific batch
|
|
133
|
+
*/
|
|
134
|
+
private buildBatchAnalysisPrompt;
|
|
95
135
|
/**
|
|
96
136
|
* Utility methods
|
|
97
137
|
*/
|
|
@@ -100,6 +140,10 @@ export declare class CodeReviewer {
|
|
|
100
140
|
* Extract line information for comment from context
|
|
101
141
|
*/
|
|
102
142
|
private extractLineInfoForComment;
|
|
143
|
+
/**
|
|
144
|
+
* Detect programming language from file extension
|
|
145
|
+
*/
|
|
146
|
+
private detectLanguageFromFile;
|
|
103
147
|
/**
|
|
104
148
|
* Generate all possible path variations for a file
|
|
105
149
|
*/
|
|
@@ -17,22 +17,37 @@ export class CodeReviewer {
|
|
|
17
17
|
this.reviewConfig = reviewConfig;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Review code using pre-gathered unified context (OPTIMIZED)
|
|
20
|
+
* Review code using pre-gathered unified context (OPTIMIZED with Batch Processing)
|
|
21
21
|
*/
|
|
22
22
|
async reviewCodeWithContext(context, options) {
|
|
23
23
|
const startTime = Date.now();
|
|
24
24
|
try {
|
|
25
25
|
logger.phase("🧪 Conducting AI-powered code analysis...");
|
|
26
26
|
logger.info(`Analyzing ${context.diffStrategy.fileCount} files using ${context.diffStrategy.strategy} strategy`);
|
|
27
|
-
|
|
28
|
-
const
|
|
27
|
+
// Determine if we should use batch processing
|
|
28
|
+
const batchConfig = this.getBatchProcessingConfig();
|
|
29
|
+
const shouldUseBatchProcessing = this.shouldUseBatchProcessing(context, batchConfig);
|
|
30
|
+
let violations;
|
|
31
|
+
let processingStrategy;
|
|
32
|
+
if (shouldUseBatchProcessing) {
|
|
33
|
+
logger.info("🔄 Using batch processing for large PR analysis");
|
|
34
|
+
const batchResult = await this.reviewWithBatchProcessing(context, options, batchConfig);
|
|
35
|
+
violations = batchResult.violations;
|
|
36
|
+
processingStrategy = "batch-processing";
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
logger.info("⚡ Using single-request analysis for small PR");
|
|
40
|
+
const analysisPrompt = this.buildAnalysisPrompt(context, options);
|
|
41
|
+
violations = await this.analyzeWithAI(analysisPrompt, context);
|
|
42
|
+
processingStrategy = "single-request";
|
|
43
|
+
}
|
|
29
44
|
const validatedViolations = this.validateViolations(violations, context);
|
|
30
45
|
if (!options.dryRun && validatedViolations.length > 0) {
|
|
31
46
|
await this.postComments(context, validatedViolations, options);
|
|
32
47
|
}
|
|
33
48
|
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
34
|
-
const result = this.generateReviewResult(validatedViolations, duration, context);
|
|
35
|
-
logger.success(`Code review completed in ${duration}s: ${validatedViolations.length} violations found`);
|
|
49
|
+
const result = this.generateReviewResult(validatedViolations, duration, context, processingStrategy);
|
|
50
|
+
logger.success(`Code review completed in ${duration}s: ${validatedViolations.length} violations found (${processingStrategy})`);
|
|
36
51
|
return result;
|
|
37
52
|
}
|
|
38
53
|
catch (error) {
|
|
@@ -607,27 +622,14 @@ Return ONLY valid JSON:
|
|
|
607
622
|
if (violation.impact) {
|
|
608
623
|
comment += `\n\n**Impact**: ${violation.impact}`;
|
|
609
624
|
}
|
|
625
|
+
// Add suggested fix section if suggestion is provided
|
|
610
626
|
if (violation.suggestion) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
ts: "typescript",
|
|
616
|
-
tsx: "typescript",
|
|
617
|
-
res: "rescript",
|
|
618
|
-
resi: "rescript",
|
|
619
|
-
py: "python",
|
|
620
|
-
java: "java",
|
|
621
|
-
go: "go",
|
|
622
|
-
rb: "ruby",
|
|
623
|
-
php: "php",
|
|
624
|
-
sql: "sql",
|
|
625
|
-
json: "json",
|
|
626
|
-
};
|
|
627
|
-
const language = langMap[fileExt] || "text";
|
|
628
|
-
// Use the escape method for code blocks
|
|
627
|
+
comment += `\n\n**Suggested Fix**:\n`;
|
|
628
|
+
// Detect the language for syntax highlighting
|
|
629
|
+
const language = this.detectLanguageFromFile(violation.file || "");
|
|
630
|
+
// Use proper markdown escaping for code blocks
|
|
629
631
|
const escapedCodeBlock = this.escapeMarkdownCodeBlock(violation.suggestion, language);
|
|
630
|
-
comment +=
|
|
632
|
+
comment += escapedCodeBlock;
|
|
631
633
|
}
|
|
632
634
|
comment += `\n\n---\n*🛡️ Automated review by **Yama** • Powered by AI*`;
|
|
633
635
|
return comment;
|
|
@@ -903,7 +905,7 @@ ${recommendation}
|
|
|
903
905
|
filesReviewed: new Set(violations.filter((v) => v.file).map((v) => v.file)).size || 1,
|
|
904
906
|
};
|
|
905
907
|
}
|
|
906
|
-
generateReviewResult(violations, _duration, _context) {
|
|
908
|
+
generateReviewResult(violations, _duration, _context, processingStrategy) {
|
|
907
909
|
const stats = this.calculateStats(violations);
|
|
908
910
|
return {
|
|
909
911
|
violations,
|
|
@@ -915,10 +917,374 @@ ${recommendation}
|
|
|
915
917
|
majorCount: stats.majorCount,
|
|
916
918
|
minorCount: stats.minorCount,
|
|
917
919
|
suggestionCount: stats.suggestionCount,
|
|
920
|
+
processingStrategy,
|
|
918
921
|
},
|
|
919
922
|
positiveObservations: [], // Could be extracted from AI response
|
|
920
923
|
};
|
|
921
924
|
}
|
|
925
|
+
// ============================================================================
|
|
926
|
+
// BATCH PROCESSING METHODS
|
|
927
|
+
// ============================================================================
|
|
928
|
+
/**
|
|
929
|
+
* Get batch processing configuration with defaults
|
|
930
|
+
*/
|
|
931
|
+
getBatchProcessingConfig() {
|
|
932
|
+
const defaultConfig = {
|
|
933
|
+
enabled: true,
|
|
934
|
+
maxFilesPerBatch: 3,
|
|
935
|
+
prioritizeSecurityFiles: true,
|
|
936
|
+
parallelBatches: false, // Sequential for better reliability
|
|
937
|
+
batchDelayMs: 1000,
|
|
938
|
+
singleRequestThreshold: 5, // Use single request for ≤5 files
|
|
939
|
+
};
|
|
940
|
+
return {
|
|
941
|
+
...defaultConfig,
|
|
942
|
+
...this.reviewConfig.batchProcessing,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Determine if batch processing should be used
|
|
947
|
+
*/
|
|
948
|
+
shouldUseBatchProcessing(context, batchConfig) {
|
|
949
|
+
if (!batchConfig.enabled) {
|
|
950
|
+
logger.debug("Batch processing disabled in config");
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
const fileCount = context.diffStrategy.fileCount;
|
|
954
|
+
if (fileCount <= batchConfig.singleRequestThreshold) {
|
|
955
|
+
logger.debug(`File count (${fileCount}) ≤ threshold (${batchConfig.singleRequestThreshold}), using single request`);
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
// Force batch processing for file-by-file strategy with many files
|
|
959
|
+
if (context.diffStrategy.strategy === "file-by-file" && fileCount > 10) {
|
|
960
|
+
logger.debug(`File-by-file strategy with ${fileCount} files, forcing batch processing`);
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
logger.debug(`File count (${fileCount}) > threshold (${batchConfig.singleRequestThreshold}), using batch processing`);
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Main batch processing method
|
|
968
|
+
*/
|
|
969
|
+
async reviewWithBatchProcessing(context, options, batchConfig) {
|
|
970
|
+
const startTime = Date.now();
|
|
971
|
+
try {
|
|
972
|
+
// Step 1: Prioritize and organize files
|
|
973
|
+
const prioritizedFiles = await this.prioritizeFiles(context, batchConfig);
|
|
974
|
+
logger.info(`📋 Prioritized ${prioritizedFiles.length} files: ${prioritizedFiles.filter(f => f.priority === "high").length} high, ${prioritizedFiles.filter(f => f.priority === "medium").length} medium, ${prioritizedFiles.filter(f => f.priority === "low").length} low priority`);
|
|
975
|
+
// Step 2: Create batches
|
|
976
|
+
const batches = this.createBatches(prioritizedFiles, batchConfig);
|
|
977
|
+
logger.info(`📦 Created ${batches.length} batches (max ${batchConfig.maxFilesPerBatch} files per batch)`);
|
|
978
|
+
// Step 3: Process batches
|
|
979
|
+
const batchResults = [];
|
|
980
|
+
const allViolations = [];
|
|
981
|
+
for (let i = 0; i < batches.length; i++) {
|
|
982
|
+
const batch = batches[i];
|
|
983
|
+
logger.info(`🔄 Processing batch ${i + 1}/${batches.length} (${batch.files.length} files, ${batch.priority} priority)`);
|
|
984
|
+
try {
|
|
985
|
+
const batchResult = await this.processBatch(batch, context, options);
|
|
986
|
+
batchResults.push(batchResult);
|
|
987
|
+
allViolations.push(...batchResult.violations);
|
|
988
|
+
logger.info(`✅ Batch ${i + 1} completed: ${batchResult.violations.length} violations found in ${Math.round(batchResult.processingTime / 1000)}s`);
|
|
989
|
+
// Add delay between batches if configured
|
|
990
|
+
if (i < batches.length - 1 && batchConfig.batchDelayMs > 0) {
|
|
991
|
+
logger.debug(`⏳ Waiting ${batchConfig.batchDelayMs}ms before next batch`);
|
|
992
|
+
await new Promise(resolve => setTimeout(resolve, batchConfig.batchDelayMs));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
logger.error(`❌ Batch ${i + 1} failed: ${error.message}`);
|
|
997
|
+
// Record failed batch
|
|
998
|
+
batchResults.push({
|
|
999
|
+
batchIndex: i,
|
|
1000
|
+
files: batch.files,
|
|
1001
|
+
violations: [],
|
|
1002
|
+
processingTime: Date.now() - startTime,
|
|
1003
|
+
error: error.message,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const totalTime = Date.now() - startTime;
|
|
1008
|
+
const avgBatchSize = batches.reduce((sum, b) => sum + b.files.length, 0) / batches.length;
|
|
1009
|
+
logger.success(`🎯 Batch processing completed: ${allViolations.length} total violations from ${batches.length} batches in ${Math.round(totalTime / 1000)}s (avg ${avgBatchSize.toFixed(1)} files/batch)`);
|
|
1010
|
+
return { violations: allViolations, batchResults };
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
logger.error(`Batch processing failed: ${error.message}`);
|
|
1014
|
+
throw error;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Prioritize files based on security importance and file type
|
|
1019
|
+
*/
|
|
1020
|
+
async prioritizeFiles(context, batchConfig) {
|
|
1021
|
+
const files = context.pr.fileChanges || [];
|
|
1022
|
+
const prioritizedFiles = [];
|
|
1023
|
+
for (const filePath of files) {
|
|
1024
|
+
const priority = this.calculateFilePriority(filePath, batchConfig);
|
|
1025
|
+
const estimatedTokens = await this.estimateFileTokens(filePath, context);
|
|
1026
|
+
prioritizedFiles.push({
|
|
1027
|
+
path: filePath,
|
|
1028
|
+
priority,
|
|
1029
|
+
estimatedTokens,
|
|
1030
|
+
diff: context.fileDiffs?.get(filePath),
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
// Sort by priority (high -> medium -> low) then by estimated tokens (smaller first)
|
|
1034
|
+
prioritizedFiles.sort((a, b) => {
|
|
1035
|
+
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
1036
|
+
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
1037
|
+
if (priorityDiff !== 0) {
|
|
1038
|
+
return priorityDiff;
|
|
1039
|
+
}
|
|
1040
|
+
return a.estimatedTokens - b.estimatedTokens;
|
|
1041
|
+
});
|
|
1042
|
+
return prioritizedFiles;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Calculate file priority based on path and content
|
|
1046
|
+
*/
|
|
1047
|
+
calculateFilePriority(filePath, batchConfig) {
|
|
1048
|
+
if (!batchConfig.prioritizeSecurityFiles) {
|
|
1049
|
+
return "medium"; // All files same priority if not prioritizing
|
|
1050
|
+
}
|
|
1051
|
+
const path = filePath.toLowerCase();
|
|
1052
|
+
// High priority: Security-sensitive files
|
|
1053
|
+
const highPriorityPatterns = [
|
|
1054
|
+
/auth/i, /login/i, /password/i, /token/i, /jwt/i, /oauth/i,
|
|
1055
|
+
/crypto/i, /encrypt/i, /decrypt/i, /hash/i, /security/i,
|
|
1056
|
+
/payment/i, /billing/i, /transaction/i, /money/i, /wallet/i,
|
|
1057
|
+
/admin/i, /privilege/i, /permission/i, /role/i, /access/i,
|
|
1058
|
+
/config/i, /env/i, /secret/i, /key/i, /credential/i,
|
|
1059
|
+
/api/i, /endpoint/i, /route/i, /controller/i, /middleware/i,
|
|
1060
|
+
];
|
|
1061
|
+
if (highPriorityPatterns.some(pattern => pattern.test(path))) {
|
|
1062
|
+
return "high";
|
|
1063
|
+
}
|
|
1064
|
+
// Low priority: Documentation, tests, config files
|
|
1065
|
+
const lowPriorityPatterns = [
|
|
1066
|
+
/\.md$/i, /\.txt$/i, /readme/i, /changelog/i, /license/i,
|
|
1067
|
+
/test/i, /spec/i, /\.test\./i, /\.spec\./i, /__tests__/i,
|
|
1068
|
+
/\.json$/i, /\.yaml$/i, /\.yml$/i, /\.toml$/i, /\.ini$/i,
|
|
1069
|
+
/\.lock$/i, /package-lock/i, /yarn\.lock/i, /pnpm-lock/i,
|
|
1070
|
+
/\.gitignore/i, /\.eslint/i, /\.prettier/i, /tsconfig/i,
|
|
1071
|
+
/\.svg$/i, /\.png$/i, /\.jpg$/i, /\.jpeg$/i, /\.gif$/i,
|
|
1072
|
+
];
|
|
1073
|
+
if (lowPriorityPatterns.some(pattern => pattern.test(path))) {
|
|
1074
|
+
return "low";
|
|
1075
|
+
}
|
|
1076
|
+
// Medium priority: Everything else
|
|
1077
|
+
return "medium";
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Estimate token count for a file
|
|
1081
|
+
*/
|
|
1082
|
+
async estimateFileTokens(filePath, context) {
|
|
1083
|
+
try {
|
|
1084
|
+
let content = "";
|
|
1085
|
+
if (context.fileDiffs?.has(filePath)) {
|
|
1086
|
+
content = context.fileDiffs.get(filePath) || "";
|
|
1087
|
+
}
|
|
1088
|
+
else if (context.prDiff) {
|
|
1089
|
+
// Extract file content from whole diff
|
|
1090
|
+
const diffLines = context.prDiff.diff.split("\n");
|
|
1091
|
+
let inFile = false;
|
|
1092
|
+
for (const line of diffLines) {
|
|
1093
|
+
if (line.startsWith("diff --git") && line.includes(filePath)) {
|
|
1094
|
+
inFile = true;
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
if (inFile && line.startsWith("diff --git")) {
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
if (inFile) {
|
|
1101
|
+
content += line + "\n";
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
// Rough estimation: ~4 characters per token
|
|
1106
|
+
const estimatedTokens = Math.ceil(content.length / 4);
|
|
1107
|
+
// Add base overhead for context and prompts
|
|
1108
|
+
const baseOverhead = 1000;
|
|
1109
|
+
return estimatedTokens + baseOverhead;
|
|
1110
|
+
}
|
|
1111
|
+
catch (error) {
|
|
1112
|
+
logger.debug(`Error estimating tokens for ${filePath}: ${error.message}`);
|
|
1113
|
+
return 2000; // Default estimate
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Create batches from prioritized files
|
|
1118
|
+
*/
|
|
1119
|
+
createBatches(prioritizedFiles, batchConfig) {
|
|
1120
|
+
const batches = [];
|
|
1121
|
+
const maxTokensPerBatch = this.getSafeTokenLimit() * 0.7; // Use 70% of limit for safety
|
|
1122
|
+
let currentBatch = {
|
|
1123
|
+
files: [],
|
|
1124
|
+
priority: "medium",
|
|
1125
|
+
estimatedTokens: 0,
|
|
1126
|
+
batchIndex: 0,
|
|
1127
|
+
};
|
|
1128
|
+
for (const file of prioritizedFiles) {
|
|
1129
|
+
const wouldExceedTokens = currentBatch.estimatedTokens + file.estimatedTokens > maxTokensPerBatch;
|
|
1130
|
+
const wouldExceedFileCount = currentBatch.files.length >= batchConfig.maxFilesPerBatch;
|
|
1131
|
+
if ((wouldExceedTokens || wouldExceedFileCount) && currentBatch.files.length > 0) {
|
|
1132
|
+
// Finalize current batch
|
|
1133
|
+
batches.push(currentBatch);
|
|
1134
|
+
// Start new batch
|
|
1135
|
+
currentBatch = {
|
|
1136
|
+
files: [],
|
|
1137
|
+
priority: file.priority,
|
|
1138
|
+
estimatedTokens: 0,
|
|
1139
|
+
batchIndex: batches.length,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
// Add file to current batch
|
|
1143
|
+
currentBatch.files.push(file.path);
|
|
1144
|
+
currentBatch.estimatedTokens += file.estimatedTokens;
|
|
1145
|
+
// Update batch priority to highest priority file in batch
|
|
1146
|
+
if (file.priority === "high" ||
|
|
1147
|
+
(file.priority === "medium" && currentBatch.priority === "low")) {
|
|
1148
|
+
currentBatch.priority = file.priority;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
// Add final batch if it has files
|
|
1152
|
+
if (currentBatch.files.length > 0) {
|
|
1153
|
+
batches.push(currentBatch);
|
|
1154
|
+
}
|
|
1155
|
+
return batches;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Process a single batch of files
|
|
1159
|
+
*/
|
|
1160
|
+
async processBatch(batch, context, options) {
|
|
1161
|
+
const startTime = Date.now();
|
|
1162
|
+
try {
|
|
1163
|
+
// Create batch-specific context
|
|
1164
|
+
const batchContext = this.createBatchContext(batch, context);
|
|
1165
|
+
// Build batch-specific prompt
|
|
1166
|
+
const batchPrompt = this.buildBatchAnalysisPrompt(batchContext, batch, options);
|
|
1167
|
+
// Analyze with AI
|
|
1168
|
+
const violations = await this.analyzeWithAI(batchPrompt, batchContext);
|
|
1169
|
+
const processingTime = Date.now() - startTime;
|
|
1170
|
+
return {
|
|
1171
|
+
batchIndex: batch.batchIndex,
|
|
1172
|
+
files: batch.files,
|
|
1173
|
+
violations,
|
|
1174
|
+
processingTime,
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
catch (error) {
|
|
1178
|
+
const processingTime = Date.now() - startTime;
|
|
1179
|
+
return {
|
|
1180
|
+
batchIndex: batch.batchIndex,
|
|
1181
|
+
files: batch.files,
|
|
1182
|
+
violations: [],
|
|
1183
|
+
processingTime,
|
|
1184
|
+
error: error.message,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Create context for a specific batch
|
|
1190
|
+
*/
|
|
1191
|
+
createBatchContext(batch, originalContext) {
|
|
1192
|
+
// Create a filtered context containing only the files in this batch
|
|
1193
|
+
const batchFileDiffs = new Map();
|
|
1194
|
+
if (originalContext.fileDiffs) {
|
|
1195
|
+
for (const filePath of batch.files) {
|
|
1196
|
+
const diff = originalContext.fileDiffs.get(filePath);
|
|
1197
|
+
if (diff) {
|
|
1198
|
+
batchFileDiffs.set(filePath, diff);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
...originalContext,
|
|
1204
|
+
fileDiffs: batchFileDiffs,
|
|
1205
|
+
diffStrategy: {
|
|
1206
|
+
...originalContext.diffStrategy,
|
|
1207
|
+
fileCount: batch.files.length,
|
|
1208
|
+
strategy: "file-by-file", // Always use file-by-file for batches
|
|
1209
|
+
reason: `Batch processing ${batch.files.length} files`,
|
|
1210
|
+
},
|
|
1211
|
+
pr: {
|
|
1212
|
+
...originalContext.pr,
|
|
1213
|
+
fileChanges: batch.files,
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Build analysis prompt for a specific batch
|
|
1219
|
+
*/
|
|
1220
|
+
buildBatchAnalysisPrompt(batchContext, batch, options) {
|
|
1221
|
+
const diffContent = this.extractDiffContent(batchContext);
|
|
1222
|
+
return `Conduct a focused security and quality analysis of this batch of ${batch.files.length} files (${batch.priority} priority).
|
|
1223
|
+
|
|
1224
|
+
## BATCH CONTEXT:
|
|
1225
|
+
**Batch**: ${batch.batchIndex + 1}
|
|
1226
|
+
**Files**: ${batch.files.length}
|
|
1227
|
+
**Priority**: ${batch.priority}
|
|
1228
|
+
**Files in batch**: ${batch.files.join(", ")}
|
|
1229
|
+
|
|
1230
|
+
## PR CONTEXT:
|
|
1231
|
+
**Title**: ${batchContext.pr.title}
|
|
1232
|
+
**Author**: ${batchContext.pr.author}
|
|
1233
|
+
**Repository**: ${batchContext.identifier.workspace}/${batchContext.identifier.repository}
|
|
1234
|
+
|
|
1235
|
+
## PROJECT CONTEXT:
|
|
1236
|
+
${batchContext.projectContext.memoryBank.projectContext || batchContext.projectContext.memoryBank.summary}
|
|
1237
|
+
|
|
1238
|
+
## PROJECT RULES & STANDARDS:
|
|
1239
|
+
${batchContext.projectContext.clinerules || "No specific rules defined"}
|
|
1240
|
+
|
|
1241
|
+
## BATCH CODE CHANGES:
|
|
1242
|
+
${diffContent}
|
|
1243
|
+
|
|
1244
|
+
## CRITICAL INSTRUCTIONS FOR CODE SNIPPETS:
|
|
1245
|
+
|
|
1246
|
+
When you identify an issue in the code, you MUST:
|
|
1247
|
+
1. Copy the EXACT line from the diff above, including the diff prefix (+, -, or space at the beginning)
|
|
1248
|
+
2. Do NOT modify, clean, or reformat the line
|
|
1249
|
+
3. Include the complete line as it appears in the diff
|
|
1250
|
+
4. If the issue spans multiple lines, choose the most relevant single line
|
|
1251
|
+
|
|
1252
|
+
## ANALYSIS REQUIREMENTS:
|
|
1253
|
+
|
|
1254
|
+
${this.getAnalysisRequirements()}
|
|
1255
|
+
|
|
1256
|
+
### 📋 OUTPUT FORMAT
|
|
1257
|
+
Return ONLY valid JSON:
|
|
1258
|
+
{
|
|
1259
|
+
"violations": [
|
|
1260
|
+
{
|
|
1261
|
+
"type": "inline",
|
|
1262
|
+
"file": "exact/file/path.ext",
|
|
1263
|
+
"code_snippet": "EXACT line from diff INCLUDING the +/- prefix",
|
|
1264
|
+
"search_context": {
|
|
1265
|
+
"before": ["line before from diff with prefix"],
|
|
1266
|
+
"after": ["line after from diff with prefix"]
|
|
1267
|
+
},
|
|
1268
|
+
"severity": "CRITICAL|MAJOR|MINOR|SUGGESTION",
|
|
1269
|
+
"category": "security|performance|maintainability|functionality",
|
|
1270
|
+
"issue": "Brief issue title",
|
|
1271
|
+
"message": "Detailed explanation",
|
|
1272
|
+
"impact": "Potential impact description",
|
|
1273
|
+
"suggestion": "Clean, executable code fix (no diff symbols)"
|
|
1274
|
+
}
|
|
1275
|
+
],
|
|
1276
|
+
"summary": "Batch analysis summary",
|
|
1277
|
+
"positiveObservations": ["Good practices found"],
|
|
1278
|
+
"statistics": {
|
|
1279
|
+
"filesReviewed": ${batch.files.length},
|
|
1280
|
+
"totalIssues": 0,
|
|
1281
|
+
"criticalCount": 0,
|
|
1282
|
+
"majorCount": 0,
|
|
1283
|
+
"minorCount": 0,
|
|
1284
|
+
"suggestionCount": 0
|
|
1285
|
+
}
|
|
1286
|
+
}`;
|
|
1287
|
+
}
|
|
922
1288
|
/**
|
|
923
1289
|
* Utility methods
|
|
924
1290
|
*/
|
|
@@ -1016,6 +1382,43 @@ ${recommendation}
|
|
|
1016
1382
|
}
|
|
1017
1383
|
return null;
|
|
1018
1384
|
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Detect programming language from file extension
|
|
1387
|
+
*/
|
|
1388
|
+
detectLanguageFromFile(filePath) {
|
|
1389
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
1390
|
+
const languageMap = {
|
|
1391
|
+
js: "javascript",
|
|
1392
|
+
jsx: "javascript",
|
|
1393
|
+
ts: "typescript",
|
|
1394
|
+
tsx: "typescript",
|
|
1395
|
+
py: "python",
|
|
1396
|
+
java: "java",
|
|
1397
|
+
cpp: "cpp",
|
|
1398
|
+
c: "c",
|
|
1399
|
+
cs: "csharp",
|
|
1400
|
+
php: "php",
|
|
1401
|
+
rb: "ruby",
|
|
1402
|
+
go: "go",
|
|
1403
|
+
rs: "rust",
|
|
1404
|
+
res: "rescript",
|
|
1405
|
+
kt: "kotlin",
|
|
1406
|
+
swift: "swift",
|
|
1407
|
+
scala: "scala",
|
|
1408
|
+
sh: "bash",
|
|
1409
|
+
sql: "sql",
|
|
1410
|
+
json: "json",
|
|
1411
|
+
yaml: "yaml",
|
|
1412
|
+
yml: "yaml",
|
|
1413
|
+
xml: "xml",
|
|
1414
|
+
html: "html",
|
|
1415
|
+
css: "css",
|
|
1416
|
+
scss: "scss",
|
|
1417
|
+
sass: "sass",
|
|
1418
|
+
md: "markdown",
|
|
1419
|
+
};
|
|
1420
|
+
return languageMap[ext || ""] || "text";
|
|
1421
|
+
}
|
|
1019
1422
|
/**
|
|
1020
1423
|
* Generate all possible path variations for a file
|
|
1021
1424
|
*/
|
package/dist/types/index.d.ts
CHANGED
|
@@ -162,6 +162,35 @@ export interface ReviewStatistics {
|
|
|
162
162
|
majorCount: number;
|
|
163
163
|
minorCount: number;
|
|
164
164
|
suggestionCount: number;
|
|
165
|
+
batchCount?: number;
|
|
166
|
+
processingStrategy?: "single-request" | "batch-processing";
|
|
167
|
+
averageBatchSize?: number;
|
|
168
|
+
totalProcessingTime?: number;
|
|
169
|
+
}
|
|
170
|
+
export interface FileBatch {
|
|
171
|
+
files: string[];
|
|
172
|
+
priority: "high" | "medium" | "low";
|
|
173
|
+
estimatedTokens: number;
|
|
174
|
+
batchIndex: number;
|
|
175
|
+
}
|
|
176
|
+
export interface BatchResult {
|
|
177
|
+
batchIndex: number;
|
|
178
|
+
files: string[];
|
|
179
|
+
violations: Violation[];
|
|
180
|
+
processingTime: number;
|
|
181
|
+
tokenUsage?: {
|
|
182
|
+
input: number;
|
|
183
|
+
output: number;
|
|
184
|
+
total: number;
|
|
185
|
+
};
|
|
186
|
+
error?: string;
|
|
187
|
+
}
|
|
188
|
+
export type FilePriority = "high" | "medium" | "low";
|
|
189
|
+
export interface PrioritizedFile {
|
|
190
|
+
path: string;
|
|
191
|
+
priority: FilePriority;
|
|
192
|
+
estimatedTokens: number;
|
|
193
|
+
diff?: string;
|
|
165
194
|
}
|
|
166
195
|
export interface ReviewOptions {
|
|
167
196
|
workspace: string;
|
|
@@ -250,6 +279,15 @@ export interface CodeReviewConfig {
|
|
|
250
279
|
systemPrompt?: string;
|
|
251
280
|
analysisTemplate?: string;
|
|
252
281
|
focusAreas?: string[];
|
|
282
|
+
batchProcessing?: BatchProcessingConfig;
|
|
283
|
+
}
|
|
284
|
+
export interface BatchProcessingConfig {
|
|
285
|
+
enabled: boolean;
|
|
286
|
+
maxFilesPerBatch: number;
|
|
287
|
+
prioritizeSecurityFiles: boolean;
|
|
288
|
+
parallelBatches: boolean;
|
|
289
|
+
batchDelayMs: number;
|
|
290
|
+
singleRequestThreshold: number;
|
|
253
291
|
}
|
|
254
292
|
export interface DescriptionEnhancementConfig {
|
|
255
293
|
enabled: boolean;
|
package/dist/utils/Cache.js
CHANGED
|
@@ -229,7 +229,7 @@ export class Cache {
|
|
|
229
229
|
}
|
|
230
230
|
// Clean up tag associations for deleted keys
|
|
231
231
|
this.tags.forEach((keys, tag) => {
|
|
232
|
-
const validKeys = new Set(
|
|
232
|
+
const validKeys = new Set(Array.from(keys).filter((key) => this.cache.has(key)));
|
|
233
233
|
if (validKeys.size !== keys.size) {
|
|
234
234
|
this.tags.set(tag, validKeys);
|
|
235
235
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/yama",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Enterprise-grade Pull Request automation toolkit with AI-powered code review and description enhancement",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pr",
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
"test": "jest",
|
|
55
55
|
"lint": "eslint .",
|
|
56
56
|
"lint:fix": "eslint . --fix",
|
|
57
|
-
"type-check": "tsc --noEmit",
|
|
57
|
+
"type-check": "tsc --noEmit --skipLibCheck",
|
|
58
58
|
"format": "prettier --write .",
|
|
59
59
|
"format:check": "prettier --check .",
|
|
60
60
|
"docs": "typedoc src",
|
|
61
61
|
"clean": "rimraf dist",
|
|
62
|
-
"prepare": "
|
|
62
|
+
"prepare": "husky install",
|
|
63
63
|
"prepack": "npm run build && npm run test",
|
|
64
64
|
"changeset": "changeset",
|
|
65
65
|
"changeset:version": "changeset version && git add --all",
|
|
@@ -67,7 +67,20 @@
|
|
|
67
67
|
"release:dry": "npm publish --dry-run",
|
|
68
68
|
"release:github": "npm publish --registry https://npm.pkg.github.com",
|
|
69
69
|
"version:check": "npm version --no-git-tag-version",
|
|
70
|
-
"pack:verify": "npm pack && tar -tzf *.tgz | head -20"
|
|
70
|
+
"pack:verify": "npm pack && tar -tzf *.tgz | head -20",
|
|
71
|
+
"validate": "npm run validate:env && npm run validate:security",
|
|
72
|
+
"validate:all": "npm run lint && npm run type-check && npm run validate",
|
|
73
|
+
"validate:env": "node scripts/validate-env.cjs",
|
|
74
|
+
"validate:security": "node scripts/validate-security.cjs",
|
|
75
|
+
"validate:commit": "node scripts/commit-validation.cjs \"$(git log -1 --pretty=format:'%s')\"",
|
|
76
|
+
"validate:build": "node scripts/build-validations.cjs",
|
|
77
|
+
"commit:validate": "node scripts/commit-validation.cjs",
|
|
78
|
+
"pre-commit": "lint-staged",
|
|
79
|
+
"pre-push": "npm run validate && npm run test",
|
|
80
|
+
"quality:all": "npm run lint && npm run format && npm run test",
|
|
81
|
+
"quality:metrics": "node scripts/quality-metrics.cjs",
|
|
82
|
+
"quality:report": "npm run quality:metrics && echo 'Quality metrics saved to quality-metrics.json'",
|
|
83
|
+
"check:all": "npm run lint && npm run format --check && npm run validate && npm run validate:commit"
|
|
71
84
|
},
|
|
72
85
|
"dependencies": {
|
|
73
86
|
"@juspay/neurolink": "^5.1.0",
|
|
@@ -111,7 +124,9 @@
|
|
|
111
124
|
"@semantic-release/release-notes-generator": "^14.0.1",
|
|
112
125
|
"semantic-release": "^24.0.0",
|
|
113
126
|
"prettier": "^3.0.0",
|
|
114
|
-
"publint": "^0.3.0"
|
|
127
|
+
"publint": "^0.3.0",
|
|
128
|
+
"husky": "^9.0.0",
|
|
129
|
+
"lint-staged": "^15.0.0"
|
|
115
130
|
},
|
|
116
131
|
"peerDependencies": {
|
|
117
132
|
"typescript": ">=4.5.0"
|
|
@@ -134,5 +149,15 @@
|
|
|
134
149
|
"onlyBuiltDependencies": [
|
|
135
150
|
"esbuild"
|
|
136
151
|
]
|
|
152
|
+
},
|
|
153
|
+
"lint-staged": {
|
|
154
|
+
"*.{ts,tsx,js,jsx}": [
|
|
155
|
+
"eslint --fix",
|
|
156
|
+
"prettier --write"
|
|
157
|
+
],
|
|
158
|
+
"*.{json,md,yml,yaml}": [
|
|
159
|
+
"prettier --write"
|
|
160
|
+
],
|
|
161
|
+
"*.ts": []
|
|
137
162
|
}
|
|
138
163
|
}
|
package/yama.config.example.yaml
CHANGED
|
@@ -51,6 +51,15 @@ features:
|
|
|
51
51
|
- "Performance bottlenecks"
|
|
52
52
|
- "Error handling"
|
|
53
53
|
- "Code quality"
|
|
54
|
+
|
|
55
|
+
# NEW: Batch Processing Configuration
|
|
56
|
+
batchProcessing:
|
|
57
|
+
enabled: true # Enable batch processing for large PRs
|
|
58
|
+
maxFilesPerBatch: 3 # Maximum files to process in each batch
|
|
59
|
+
prioritizeSecurityFiles: true # Process security-sensitive files first
|
|
60
|
+
parallelBatches: false # Process batches sequentially for reliability
|
|
61
|
+
batchDelayMs: 1000 # Delay between batches in milliseconds
|
|
62
|
+
singleRequestThreshold: 5 # Use single request for PRs with ≤5 files
|
|
54
63
|
|
|
55
64
|
# Description Enhancement Configuration
|
|
56
65
|
descriptionEnhancement:
|
|
@@ -149,4 +158,4 @@ memoryBank:
|
|
|
149
158
|
fallbackPaths: # Optional fallback paths if primary doesn't exist
|
|
150
159
|
- "docs/memory-bank"
|
|
151
160
|
- ".memory-bank"
|
|
152
|
-
- "project-docs/context"
|
|
161
|
+
- "project-docs/context"
|