@juspay/yama 1.1.1 โ 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 +16 -7
- package/README.md +1 -1
- package/dist/core/ContextGatherer.d.ts +8 -3
- package/dist/core/ContextGatherer.js +40 -21
- package/dist/core/Guardian.js +5 -1
- package/dist/core/providers/BitbucketProvider.js +1 -1
- package/dist/features/CodeReviewer.d.ts +49 -1
- package/dist/features/CodeReviewer.js +452 -26
- package/dist/types/index.d.ts +44 -0
- package/dist/utils/Cache.d.ts +5 -0
- package/dist/utils/Cache.js +17 -1
- package/dist/utils/ConfigManager.d.ts +4 -0
- package/dist/utils/ConfigManager.js +21 -1
- 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 +30 -5
- package/yama.config.example.yaml +19 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// NeuroLink will be dynamically imported
|
|
6
6
|
import { ProviderError, } from "../types/index.js";
|
|
7
7
|
import { logger } from "../utils/Logger.js";
|
|
8
|
+
import { getProviderTokenLimit } from "../utils/ProviderLimits.js";
|
|
8
9
|
export class CodeReviewer {
|
|
9
10
|
neurolink;
|
|
10
11
|
bitbucketProvider;
|
|
@@ -16,22 +17,37 @@ export class CodeReviewer {
|
|
|
16
17
|
this.reviewConfig = reviewConfig;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
|
-
* Review code using pre-gathered unified context (OPTIMIZED)
|
|
20
|
+
* Review code using pre-gathered unified context (OPTIMIZED with Batch Processing)
|
|
20
21
|
*/
|
|
21
22
|
async reviewCodeWithContext(context, options) {
|
|
22
23
|
const startTime = Date.now();
|
|
23
24
|
try {
|
|
24
25
|
logger.phase("๐งช Conducting AI-powered code analysis...");
|
|
25
26
|
logger.info(`Analyzing ${context.diffStrategy.fileCount} files using ${context.diffStrategy.strategy} strategy`);
|
|
26
|
-
|
|
27
|
-
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
|
+
}
|
|
28
44
|
const validatedViolations = this.validateViolations(violations, context);
|
|
29
45
|
if (!options.dryRun && validatedViolations.length > 0) {
|
|
30
46
|
await this.postComments(context, validatedViolations, options);
|
|
31
47
|
}
|
|
32
48
|
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
33
|
-
const result = this.generateReviewResult(validatedViolations, duration, context);
|
|
34
|
-
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})`);
|
|
35
51
|
return result;
|
|
36
52
|
}
|
|
37
53
|
catch (error) {
|
|
@@ -397,6 +413,23 @@ Return ONLY valid JSON:
|
|
|
397
413
|
// Legacy method - now delegates to new structure
|
|
398
414
|
return this.buildCoreAnalysisPrompt(context);
|
|
399
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Get safe token limit based on AI provider using shared utility
|
|
418
|
+
*/
|
|
419
|
+
getSafeTokenLimit() {
|
|
420
|
+
const provider = this.aiConfig.provider || "auto";
|
|
421
|
+
const configuredTokens = this.aiConfig.maxTokens;
|
|
422
|
+
// Use conservative limits for CodeReviewer (safer for large diffs)
|
|
423
|
+
const providerLimit = getProviderTokenLimit(provider, true);
|
|
424
|
+
// Use the smaller of configured tokens or provider limit
|
|
425
|
+
if (configuredTokens && configuredTokens > 0) {
|
|
426
|
+
const safeLimit = Math.min(configuredTokens, providerLimit);
|
|
427
|
+
logger.debug(`Token limit: configured=${configuredTokens}, provider=${providerLimit}, using=${safeLimit}`);
|
|
428
|
+
return safeLimit;
|
|
429
|
+
}
|
|
430
|
+
logger.debug(`Token limit: using provider default=${providerLimit} for ${provider}`);
|
|
431
|
+
return providerLimit;
|
|
432
|
+
}
|
|
400
433
|
/**
|
|
401
434
|
* Analyze code with AI using the enhanced prompt
|
|
402
435
|
*/
|
|
@@ -427,13 +460,18 @@ Return ONLY valid JSON:
|
|
|
427
460
|
};
|
|
428
461
|
// Simplified, focused prompt without context pollution
|
|
429
462
|
const corePrompt = this.buildCoreAnalysisPrompt(context);
|
|
463
|
+
// Get safe token limit based on provider
|
|
464
|
+
const safeMaxTokens = this.getSafeTokenLimit();
|
|
465
|
+
logger.debug(`Using AI provider: ${this.aiConfig.provider || "auto"}`);
|
|
466
|
+
logger.debug(`Configured maxTokens: ${this.aiConfig.maxTokens}`);
|
|
467
|
+
logger.debug(`Safe maxTokens limit: ${safeMaxTokens}`);
|
|
430
468
|
const result = await this.neurolink.generate({
|
|
431
469
|
input: { text: corePrompt },
|
|
432
470
|
systemPrompt: this.getSecurityReviewSystemPrompt(),
|
|
433
471
|
provider: this.aiConfig.provider || "auto", // Auto-select best provider
|
|
434
472
|
model: this.aiConfig.model || "best", // Use most capable model
|
|
435
473
|
temperature: this.aiConfig.temperature || 0.3, // Lower for more focused analysis
|
|
436
|
-
maxTokens:
|
|
474
|
+
maxTokens: safeMaxTokens, // Use provider-aware safe token limit
|
|
437
475
|
timeout: "15m", // Allow plenty of time for thorough analysis
|
|
438
476
|
context: aiContext,
|
|
439
477
|
enableAnalytics: this.aiConfig.enableAnalytics || true,
|
|
@@ -584,27 +622,14 @@ Return ONLY valid JSON:
|
|
|
584
622
|
if (violation.impact) {
|
|
585
623
|
comment += `\n\n**Impact**: ${violation.impact}`;
|
|
586
624
|
}
|
|
625
|
+
// Add suggested fix section if suggestion is provided
|
|
587
626
|
if (violation.suggestion) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
ts: "typescript",
|
|
593
|
-
tsx: "typescript",
|
|
594
|
-
res: "rescript",
|
|
595
|
-
resi: "rescript",
|
|
596
|
-
py: "python",
|
|
597
|
-
java: "java",
|
|
598
|
-
go: "go",
|
|
599
|
-
rb: "ruby",
|
|
600
|
-
php: "php",
|
|
601
|
-
sql: "sql",
|
|
602
|
-
json: "json",
|
|
603
|
-
};
|
|
604
|
-
const language = langMap[fileExt] || "text";
|
|
605
|
-
// 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
|
|
606
631
|
const escapedCodeBlock = this.escapeMarkdownCodeBlock(violation.suggestion, language);
|
|
607
|
-
comment +=
|
|
632
|
+
comment += escapedCodeBlock;
|
|
608
633
|
}
|
|
609
634
|
comment += `\n\n---\n*๐ก๏ธ Automated review by **Yama** โข Powered by AI*`;
|
|
610
635
|
return comment;
|
|
@@ -880,7 +905,7 @@ ${recommendation}
|
|
|
880
905
|
filesReviewed: new Set(violations.filter((v) => v.file).map((v) => v.file)).size || 1,
|
|
881
906
|
};
|
|
882
907
|
}
|
|
883
|
-
generateReviewResult(violations, _duration, _context) {
|
|
908
|
+
generateReviewResult(violations, _duration, _context, processingStrategy) {
|
|
884
909
|
const stats = this.calculateStats(violations);
|
|
885
910
|
return {
|
|
886
911
|
violations,
|
|
@@ -892,10 +917,374 @@ ${recommendation}
|
|
|
892
917
|
majorCount: stats.majorCount,
|
|
893
918
|
minorCount: stats.minorCount,
|
|
894
919
|
suggestionCount: stats.suggestionCount,
|
|
920
|
+
processingStrategy,
|
|
895
921
|
},
|
|
896
922
|
positiveObservations: [], // Could be extracted from AI response
|
|
897
923
|
};
|
|
898
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
|
+
}
|
|
899
1288
|
/**
|
|
900
1289
|
* Utility methods
|
|
901
1290
|
*/
|
|
@@ -993,6 +1382,43 @@ ${recommendation}
|
|
|
993
1382
|
}
|
|
994
1383
|
return null;
|
|
995
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
|
+
}
|
|
996
1422
|
/**
|
|
997
1423
|
* Generate all possible path variations for a file
|
|
998
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;
|
|
@@ -233,6 +262,7 @@ export interface GuardianConfig {
|
|
|
233
262
|
securityScan?: SecurityScanConfig;
|
|
234
263
|
analytics?: AnalyticsConfig;
|
|
235
264
|
};
|
|
265
|
+
memoryBank?: MemoryBankConfig;
|
|
236
266
|
cache?: CacheConfig;
|
|
237
267
|
performance?: PerformanceConfig;
|
|
238
268
|
rules?: CustomRulesConfig;
|
|
@@ -249,6 +279,15 @@ export interface CodeReviewConfig {
|
|
|
249
279
|
systemPrompt?: string;
|
|
250
280
|
analysisTemplate?: string;
|
|
251
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;
|
|
252
291
|
}
|
|
253
292
|
export interface DescriptionEnhancementConfig {
|
|
254
293
|
enabled: boolean;
|
|
@@ -277,6 +316,11 @@ export interface AnalyticsConfig {
|
|
|
277
316
|
trackMetrics: boolean;
|
|
278
317
|
exportFormat: "json" | "csv" | "yaml";
|
|
279
318
|
}
|
|
319
|
+
export interface MemoryBankConfig {
|
|
320
|
+
enabled: boolean;
|
|
321
|
+
path: string;
|
|
322
|
+
fallbackPaths?: string[];
|
|
323
|
+
}
|
|
280
324
|
export interface CacheConfig {
|
|
281
325
|
enabled: boolean;
|
|
282
326
|
ttl: string;
|
package/dist/utils/Cache.d.ts
CHANGED
|
@@ -57,6 +57,10 @@ export declare class Cache implements ICache {
|
|
|
57
57
|
* Invalidate all keys with a specific tag
|
|
58
58
|
*/
|
|
59
59
|
invalidateTag(tag: string): number;
|
|
60
|
+
/**
|
|
61
|
+
* Invalidate all keys matching a pattern
|
|
62
|
+
*/
|
|
63
|
+
invalidatePattern(pattern: string): number;
|
|
60
64
|
/**
|
|
61
65
|
* Cache key generators for common patterns
|
|
62
66
|
*/
|
|
@@ -69,6 +73,7 @@ export declare class Cache implements ICache {
|
|
|
69
73
|
aiResponse: (prompt: string, provider: string, model: string) => string;
|
|
70
74
|
projectContext: (workspace: string, repository: string, branch: string) => string;
|
|
71
75
|
reviewResult: (workspace: string, repository: string, prId: string | number, configHash: string) => string;
|
|
76
|
+
memoryBankFiles: (workspace: string, repository: string, branch: string, path: string) => string;
|
|
72
77
|
};
|
|
73
78
|
/**
|
|
74
79
|
* Smart cache warming for common patterns
|
package/dist/utils/Cache.js
CHANGED
|
@@ -166,6 +166,21 @@ export class Cache {
|
|
|
166
166
|
logger.debug(`Invalidated tag "${tag}": ${deleted} keys`);
|
|
167
167
|
return deleted;
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Invalidate all keys matching a pattern
|
|
171
|
+
*/
|
|
172
|
+
invalidatePattern(pattern) {
|
|
173
|
+
const regex = new RegExp(pattern.replace(/\*/g, ".*"));
|
|
174
|
+
const allKeys = this.keys();
|
|
175
|
+
let deleted = 0;
|
|
176
|
+
allKeys.forEach((key) => {
|
|
177
|
+
if (regex.test(key)) {
|
|
178
|
+
deleted += this.del(key);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
logger.debug(`Invalidated pattern "${pattern}": ${deleted} keys`);
|
|
182
|
+
return deleted;
|
|
183
|
+
}
|
|
169
184
|
/**
|
|
170
185
|
* Cache key generators for common patterns
|
|
171
186
|
*/
|
|
@@ -182,6 +197,7 @@ export class Cache {
|
|
|
182
197
|
},
|
|
183
198
|
projectContext: (workspace, repository, branch) => `context:${workspace}:${repository}:${branch}`,
|
|
184
199
|
reviewResult: (workspace, repository, prId, configHash) => `review:${workspace}:${repository}:${prId}:${configHash}`,
|
|
200
|
+
memoryBankFiles: (workspace, repository, branch, path) => `memory-bank:${workspace}:${repository}:${branch}:${path}`,
|
|
185
201
|
};
|
|
186
202
|
/**
|
|
187
203
|
* Smart cache warming for common patterns
|
|
@@ -213,7 +229,7 @@ export class Cache {
|
|
|
213
229
|
}
|
|
214
230
|
// Clean up tag associations for deleted keys
|
|
215
231
|
this.tags.forEach((keys, tag) => {
|
|
216
|
-
const validKeys = new Set(
|
|
232
|
+
const validKeys = new Set(Array.from(keys).filter((key) => this.cache.has(key)));
|
|
217
233
|
if (validKeys.size !== keys.size) {
|
|
218
234
|
this.tags.set(tag, validKeys);
|
|
219
235
|
}
|