@mate_tsaava/pr-review 1.0.1 → 1.0.3
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/LICENSE.md +21 -0
- package/dist/cli.js +10 -4
- package/dist/llm/adapter.d.ts +1 -0
- package/dist/llm/azure-openai.js +1 -0
- package/dist/llm/claude.d.ts +1 -0
- package/dist/llm/claude.js +28 -20
- package/dist/llm/openai.js +1 -0
- package/dist/reviewer.d.ts +1 -0
- package/dist/reviewer.js +56 -10
- package/dist/utils/batcher.js +1 -1
- package/package.json +9 -10
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|
package/dist/cli.js
CHANGED
|
@@ -58,14 +58,14 @@ yargs(hideBin(process.argv))
|
|
|
58
58
|
default: false,
|
|
59
59
|
})
|
|
60
60
|
.option("max-tokens", {
|
|
61
|
-
describe: "Max tokens per batch
|
|
61
|
+
describe: "Max tokens per batch",
|
|
62
62
|
type: "number",
|
|
63
|
-
default:
|
|
63
|
+
default: 40000,
|
|
64
64
|
})
|
|
65
65
|
.option("max-files", {
|
|
66
|
-
describe: "Max files per batch
|
|
66
|
+
describe: "Max files per batch",
|
|
67
67
|
type: "number",
|
|
68
|
-
default:
|
|
68
|
+
default: 3,
|
|
69
69
|
})
|
|
70
70
|
.option("config", {
|
|
71
71
|
alias: "c",
|
|
@@ -77,6 +77,11 @@ yargs(hideBin(process.argv))
|
|
|
77
77
|
describe: "Show detailed logs (prompts, responses, API calls)",
|
|
78
78
|
type: "boolean",
|
|
79
79
|
default: false,
|
|
80
|
+
})
|
|
81
|
+
.option("review-context", {
|
|
82
|
+
describe: "Also flag issues in context lines (legacy code near changes)",
|
|
83
|
+
type: "boolean",
|
|
84
|
+
default: false,
|
|
80
85
|
});
|
|
81
86
|
}, async (argv) => {
|
|
82
87
|
const { loadConfig } = await import("./config.js");
|
|
@@ -102,6 +107,7 @@ yargs(hideBin(process.argv))
|
|
|
102
107
|
maxTokensPerBatch: argv.maxTokens,
|
|
103
108
|
maxFilesPerBatch: argv.maxFiles,
|
|
104
109
|
verbose: argv.verbose,
|
|
110
|
+
reviewContext: argv.reviewContext,
|
|
105
111
|
});
|
|
106
112
|
// Print summary
|
|
107
113
|
console.log("\n" + chalk.bold("Result:"));
|
package/dist/llm/adapter.d.ts
CHANGED
package/dist/llm/azure-openai.js
CHANGED
|
@@ -50,6 +50,7 @@ export class AzureOpenAIAdapter {
|
|
|
50
50
|
- Author: ${prompt.prMetadata.author}
|
|
51
51
|
- Source Branch: ${prompt.prMetadata.sourceBranch}
|
|
52
52
|
- Target Branch: ${prompt.prMetadata.targetBranch}
|
|
53
|
+
- Description: ${prompt.prMetadata.description || "No description"}
|
|
53
54
|
|
|
54
55
|
## File Diffs
|
|
55
56
|
`;
|
package/dist/llm/claude.d.ts
CHANGED
package/dist/llm/claude.js
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
-
const
|
|
2
|
+
const SYSTEM_PROMPT_BASE = `You are a code reviewer. Follow the provided rules file EXACTLY.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
- category: Type of issue (Security, Architecture, Naming, Performance, Clean Code, etc.)
|
|
10
|
-
- codeSnippet: Quote the EXACT problematic code (1-3 lines max) so there's no ambiguity
|
|
11
|
-
- message: A witty, memorable comment about the issue
|
|
12
|
-
- fix: The specific action to fix the issue, referencing the exact lines
|
|
4
|
+
STRICT RULES:
|
|
5
|
+
1. NO DUPLICATES - Flag each issue ONCE only. If branch name is invalid, ONE issue. If PR title is invalid, ONE issue.
|
|
6
|
+
2. ONLY FLAG PROBLEMS - Never create issues for correct code. If code is fine, don't mention it.
|
|
7
|
+
3. LINE NUMBERS - Read from diff @@ header. Example: "@@ -120,6 +145,10 @@" means new code starts at line 145. Use the NEW file line number (after the +).
|
|
8
|
+
4. For metadata issues: file="/_metadata", line=1 (branch) or line=2 (title)
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
2. NEVER comment on lines starting with "-" (deleted/old code). These are being REMOVED from the codebase.
|
|
17
|
-
3. When a line is CHANGED, it appears as "-old" then "+new" - only review the "+new" version.
|
|
18
|
-
4. The "file" field MUST be the exact path from the diff (starts with /)
|
|
19
|
-
5. The "line" field MUST be a positive integer from the NEW code's line numbers (the + lines)
|
|
20
|
-
6. If old problematic code is being deleted and replaced with better code, that is GOOD - do not flag the old code as an issue.
|
|
10
|
+
"file" = exact path from diff header (starts with /)`;
|
|
11
|
+
const DIFF_RULES_STRICT = `
|
|
21
12
|
|
|
22
|
-
|
|
13
|
+
DIFF RULES (STRICT MODE - only review changed lines):
|
|
14
|
+
- ONLY review "+" lines (new code being added)
|
|
15
|
+
- NEVER flag "-" lines (old code being removed)
|
|
16
|
+
- NEVER flag " " lines (context/unchanged code) - these existed before this PR
|
|
17
|
+
- If code doesn't start with "+", SKIP IT completely`;
|
|
18
|
+
const DIFF_RULES_WITH_CONTEXT = `
|
|
19
|
+
|
|
20
|
+
DIFF RULES (CONTEXT MODE - review changes and surrounding code):
|
|
21
|
+
- Review "+" lines (new code being added) - PRIMARY focus
|
|
22
|
+
- Review " " lines (context) if they have issues near the changes
|
|
23
|
+
- NEVER flag "-" lines (old code being removed)
|
|
24
|
+
- For context issues, be helpful but less strict (use MEDIUM severity max)`;
|
|
25
|
+
const JSON_FORMAT = `
|
|
26
|
+
|
|
27
|
+
Return your response as valid JSON:
|
|
23
28
|
{
|
|
24
29
|
"issues": [
|
|
25
30
|
{
|
|
@@ -45,14 +50,17 @@ export class ClaudeAdapter {
|
|
|
45
50
|
client;
|
|
46
51
|
model;
|
|
47
52
|
verbose;
|
|
53
|
+
reviewContext;
|
|
48
54
|
constructor(config) {
|
|
49
55
|
this.client = new Anthropic({ apiKey: config.apiKey });
|
|
50
56
|
this.model = config.model;
|
|
51
57
|
this.verbose = config.verbose || false;
|
|
58
|
+
this.reviewContext = config.reviewContext || false;
|
|
52
59
|
}
|
|
53
60
|
async review(prompt) {
|
|
54
61
|
const userPrompt = this.buildUserPrompt(prompt);
|
|
55
|
-
const
|
|
62
|
+
const diffRules = this.reviewContext ? DIFF_RULES_WITH_CONTEXT : DIFF_RULES_STRICT;
|
|
63
|
+
const systemPrompt = SYSTEM_PROMPT_BASE + diffRules + JSON_FORMAT + "\n\n" + prompt.rules;
|
|
56
64
|
const maxRetries = 3;
|
|
57
65
|
let lastError = null;
|
|
58
66
|
if (this.verbose) {
|
|
@@ -73,7 +81,7 @@ export class ClaudeAdapter {
|
|
|
73
81
|
try {
|
|
74
82
|
const response = await this.client.messages.create({
|
|
75
83
|
model: this.model,
|
|
76
|
-
max_tokens:
|
|
84
|
+
max_tokens: 8192,
|
|
77
85
|
system: systemPrompt,
|
|
78
86
|
messages: [
|
|
79
87
|
{
|
package/dist/llm/openai.js
CHANGED
|
@@ -50,6 +50,7 @@ export class OpenAIAdapter {
|
|
|
50
50
|
- Author: ${prompt.prMetadata.author}
|
|
51
51
|
- Source Branch: ${prompt.prMetadata.sourceBranch}
|
|
52
52
|
- Target Branch: ${prompt.prMetadata.targetBranch}
|
|
53
|
+
- Description: ${prompt.prMetadata.description || "No description"}
|
|
53
54
|
|
|
54
55
|
## File Diffs
|
|
55
56
|
`;
|
package/dist/reviewer.d.ts
CHANGED
package/dist/reviewer.js
CHANGED
|
@@ -28,8 +28,8 @@ export class PRReviewer {
|
|
|
28
28
|
// 2. Load review rules
|
|
29
29
|
const rules = this.loadRules();
|
|
30
30
|
// 3. Create batches
|
|
31
|
-
const maxTokens = options.maxTokensPerBatch ||
|
|
32
|
-
const maxFiles = options.maxFilesPerBatch ||
|
|
31
|
+
const maxTokens = options.maxTokensPerBatch || 40000;
|
|
32
|
+
const maxFiles = options.maxFilesPerBatch || 3;
|
|
33
33
|
const diffs = prDiff.diffs.map((d) => ({ path: d.path, diff: d.diff }));
|
|
34
34
|
const totalTokens = estimateDiffTokens(diffs);
|
|
35
35
|
const batches = createBatches(diffs, maxTokens, maxFiles);
|
|
@@ -42,6 +42,7 @@ export class PRReviewer {
|
|
|
42
42
|
model: this.config.llm.model || "claude-sonnet-4-20250514",
|
|
43
43
|
endpoint: this.config.llm.endpoint,
|
|
44
44
|
verbose: options.verbose,
|
|
45
|
+
reviewContext: options.reviewContext,
|
|
45
46
|
});
|
|
46
47
|
const allIssues = [];
|
|
47
48
|
try {
|
|
@@ -67,17 +68,51 @@ export class PRReviewer {
|
|
|
67
68
|
catch (error) {
|
|
68
69
|
throw new Error(`Failed to analyze PR with ${this.config.llm.provider}: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
70
|
}
|
|
70
|
-
// 5.
|
|
71
|
+
// 5. Deduplicate metadata issues (same metadata sent to each batch)
|
|
72
|
+
// Use line-only key since metadata line numbers uniquely identify the issue
|
|
73
|
+
// (line 1 = branch naming, line 2 = PR title, etc.)
|
|
74
|
+
const allMetadataIssues = allIssues.filter((i) => i.file === "/_metadata");
|
|
75
|
+
const allCodeIssues = allIssues.filter((i) => i.file !== "/_metadata");
|
|
76
|
+
const uniqueMetadata = new Map();
|
|
77
|
+
for (const issue of allMetadataIssues) {
|
|
78
|
+
const key = `${issue.line}`;
|
|
79
|
+
if (!uniqueMetadata.has(key)) {
|
|
80
|
+
uniqueMetadata.set(key, issue);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// 6. Generate combined result
|
|
84
|
+
const metadataIssues = [...uniqueMetadata.values()];
|
|
85
|
+
const codeIssues = allCodeIssues;
|
|
86
|
+
const deduplicatedIssues = [...metadataIssues, ...codeIssues];
|
|
71
87
|
const result = {
|
|
72
|
-
issues:
|
|
73
|
-
summary: this.generateSummary(
|
|
88
|
+
issues: deduplicatedIssues,
|
|
89
|
+
summary: this.generateSummary(deduplicatedIssues),
|
|
74
90
|
};
|
|
75
91
|
// 4. Post comments (unless dry run)
|
|
76
92
|
let postedComments = 0;
|
|
77
93
|
let summaryPosted = false;
|
|
78
94
|
if (!options.dryRun) {
|
|
79
95
|
console.log("\nPosting comments...");
|
|
80
|
-
|
|
96
|
+
// Post metadata issues as a single thread comment (not inline)
|
|
97
|
+
if (metadataIssues.length > 0) {
|
|
98
|
+
try {
|
|
99
|
+
const metadataContent = metadataIssues
|
|
100
|
+
.map((issue) => this.formatComment(issue))
|
|
101
|
+
.join("\n\n---\n\n");
|
|
102
|
+
await this.poster.postSummaryComment(options.repositoryId, options.pullRequestId, {
|
|
103
|
+
content: `## ⚠️ PR Metadata Issues\n\n${metadataContent}`,
|
|
104
|
+
});
|
|
105
|
+
postedComments += metadataIssues.length;
|
|
106
|
+
for (const issue of metadataIssues) {
|
|
107
|
+
console.log(` • ${issue.severity}: ${issue.category} (${issue.message.slice(0, 50)}...)`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error(` ✗ Failed to post metadata comment: ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Post code issues as inline comments
|
|
115
|
+
for (const issue of codeIssues) {
|
|
81
116
|
try {
|
|
82
117
|
await this.poster.postInlineComment(options.repositoryId, options.pullRequestId, {
|
|
83
118
|
filePath: issue.file,
|
|
@@ -102,10 +137,21 @@ export class PRReviewer {
|
|
|
102
137
|
}
|
|
103
138
|
else {
|
|
104
139
|
console.log("\nDry run - would post these comments:");
|
|
105
|
-
|
|
106
|
-
console.log(
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
if (metadataIssues.length > 0) {
|
|
141
|
+
console.log("\n 📋 METADATA ISSUES:");
|
|
142
|
+
for (const issue of metadataIssues) {
|
|
143
|
+
console.log(` [${issue.severity}] ${issue.category}`);
|
|
144
|
+
console.log(` ${issue.message}`);
|
|
145
|
+
console.log(` Fix: ${issue.fix}\n`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (codeIssues.length > 0) {
|
|
149
|
+
console.log("\n 📝 CODE ISSUES:");
|
|
150
|
+
for (const issue of codeIssues) {
|
|
151
|
+
console.log(` [${issue.severity}] ${issue.category} (${issue.file}:${issue.line})`);
|
|
152
|
+
console.log(` ${issue.message}`);
|
|
153
|
+
console.log(` Fix: ${issue.fix}\n`);
|
|
154
|
+
}
|
|
109
155
|
}
|
|
110
156
|
console.log(`Summary: ${result.summary}`);
|
|
111
157
|
}
|
package/dist/utils/batcher.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { estimateTokens } from "./tokens.js";
|
|
2
|
-
export function createBatches(diffs, maxTokensPerBatch =
|
|
2
|
+
export function createBatches(diffs, maxTokensPerBatch = 40000, maxFilesPerBatch = 3) {
|
|
3
3
|
// Add token estimates
|
|
4
4
|
const filesWithTokens = diffs.map((d) => ({
|
|
5
5
|
...d,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mate_tsaava/pr-review",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "AI-powered code review CLI for Azure DevOps pull requests",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,12 +17,6 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"dist"
|
|
19
19
|
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "tsc && cp -r src/rules dist/",
|
|
22
|
-
"clean": "rm -rf dist",
|
|
23
|
-
"test": "vitest run",
|
|
24
|
-
"prepublishOnly": "npm run build"
|
|
25
|
-
},
|
|
26
20
|
"keywords": [
|
|
27
21
|
"azure-devops",
|
|
28
22
|
"ado",
|
|
@@ -38,18 +32,23 @@
|
|
|
38
32
|
"node": ">=18"
|
|
39
33
|
},
|
|
40
34
|
"dependencies": {
|
|
41
|
-
"@mate_tsaava/azure-devops-core": "workspace:*",
|
|
42
35
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
43
36
|
"openai": "^4.77.0",
|
|
44
37
|
"yargs": "^18.0.0",
|
|
45
38
|
"zod": "^3.25.63",
|
|
46
39
|
"chalk": "^5.4.1",
|
|
47
|
-
"dotenv": "^16.4.7"
|
|
40
|
+
"dotenv": "^16.4.7",
|
|
41
|
+
"@mate_tsaava/azure-devops-core": "1.0.1"
|
|
48
42
|
},
|
|
49
43
|
"devDependencies": {
|
|
50
44
|
"@types/node": "^22.19.1",
|
|
51
45
|
"@types/yargs": "^17.0.33",
|
|
52
46
|
"typescript": "^5.9.3",
|
|
53
47
|
"vitest": "^3.0.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc && cp -r src/rules dist/",
|
|
51
|
+
"clean": "rm -rf dist",
|
|
52
|
+
"test": "vitest run"
|
|
54
53
|
}
|
|
55
|
-
}
|
|
54
|
+
}
|