git-diff-ai-reviewer 1.1.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.
@@ -0,0 +1,29 @@
1
+ name: Publish to NPM
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ id-token: write # required for provenance
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Setup Node.js
18
+ uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '20'
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - name: Install dependencies
24
+ run: npm install
25
+
26
+ - name: Publish to NPM
27
+ run: npm publish --provenance --access public
28
+ env:
29
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # git-diff-ai-reviewer
2
+
3
+ AI-powered code review tool using **Claude** or **Gemini** APIs. Reviews git branch diffs and generates structured, actionable feedback with severity levels.
4
+
5
+ ## Installation
6
+
7
+ > **`--save-dev` is correct** — this is a development tool (like ESLint or Prettier) used only during development and CI. It should never be in your production bundle.
8
+
9
+ ```bash
10
+ # From npm (recommended)
11
+ npm install --save-dev git-diff-ai-reviewer
12
+
13
+ # Or directly from GitHub
14
+ npm install --save-dev github:andrii-posia/git-diff-ai-reviewer
15
+ ```
16
+
17
+ Then install **only the SDK for the provider you want to use** (also as devDependencies):
18
+
19
+ ```bash
20
+ # If using Claude (Anthropic)
21
+ npm install --save-dev @anthropic-ai/sdk
22
+
23
+ # If using Gemini (Google)
24
+ npm install --save-dev @google/genai
25
+
26
+ # If you want both available
27
+ npm install --save-dev @anthropic-ai/sdk @google/genai
28
+
29
+ ```
30
+
31
+ > Neither SDK is installed automatically. You only pay for what you use.
32
+
33
+ ## Setup
34
+
35
+ Set the API key for your chosen provider:
36
+
37
+ ```bash
38
+ # For Claude (Anthropic)
39
+ export ANTHROPIC_API_KEY=your-key-here
40
+
41
+ # For Gemini (Google)
42
+ export GEMINI_API_KEY=your-key-here
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ### CLI
48
+
49
+ ```bash
50
+ # Review with auto-detected provider (based on which env var is set)
51
+ git-diff-ai-reviewer review
52
+
53
+ # Explicitly choose a provider
54
+ git-diff-ai-reviewer review --provider claude
55
+ git-diff-ai-reviewer review --provider gemini
56
+
57
+ # Review against a specific base branch
58
+ git-diff-ai-reviewer review --base develop
59
+
60
+ # Preview diff without calling API
61
+ git-diff-ai-reviewer review --dry-run
62
+
63
+ # Generate fix prompt from latest review
64
+ git-diff-ai-reviewer fix
65
+ ```
66
+
67
+ ### npm scripts
68
+
69
+ Add to your `package.json`:
70
+ ```json
71
+ {
72
+ "scripts": {
73
+ "review": "git-diff-ai-reviewer review",
74
+ "review:fix": "git-diff-ai-reviewer fix",
75
+ "review:dry": "git-diff-ai-reviewer review --dry-run"
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## Providers
81
+
82
+ | Provider | Env Variable | Default Model |
83
+ |----------|-------------|---------------|
84
+ | Claude | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` |
85
+ | Gemini | `GEMINI_API_KEY` | `gemini-2.0-flash` |
86
+
87
+ The provider is auto-detected from which API key is set. You can also set it explicitly in the config or via `--provider` flag.
88
+
89
+ ## Severity Levels
90
+
91
+ Each review finding is tagged with a severity:
92
+
93
+ - 🔴 **CRITICAL** — Bugs, security vulnerabilities, data loss risks. Must fix before merging.
94
+ - 🟡 **WARNING** — Performance issues, bad practices, potential bugs. Should be addressed.
95
+ - 🔵 **SUGGESTION** — Style improvements, readability, refactoring opportunities. Nice to have.
96
+
97
+ The CLI exits with code `1` if CRITICAL issues are found (useful for CI gates).
98
+
99
+ ## Review Rule Presets
100
+
101
+ Three built-in rule presets control what the AI checks for:
102
+
103
+ ### `basic` — Quick, lightweight review
104
+ - Syntax errors, typos, unused variables
105
+ - Debug statements left in code
106
+ - Hardcoded secrets
107
+ - Basic error handling
108
+
109
+ ### `standard` (default) — Balanced everyday review
110
+ - Everything in `basic`, plus:
111
+ - Input validation, null checks
112
+ - Code duplication, naming conventions
113
+ - Async/await correctness
114
+ - Resource cleanup
115
+
116
+ ### `comprehensive` — Full audit for critical code
117
+ - Everything in `standard`, plus:
118
+ - Security (SQL injection, XSS, auth)
119
+ - Race conditions, memory leaks
120
+ - API design, backward compatibility
121
+ - Accessibility, test coverage
122
+ - Performance analysis
123
+
124
+ Use a preset name or provide custom rules in your config.
125
+
126
+ ## Configuration
127
+
128
+ Create `.ai-review.config.json` in your project root:
129
+
130
+ ```json
131
+ {
132
+ "provider": "claude",
133
+ "baseBranch": "main",
134
+ "model": "",
135
+ "maxTokens": 4096,
136
+ "outputDir": "./reviews",
137
+ "reviewRules": "standard"
138
+ }
139
+ ```
140
+
141
+ ### Config Options
142
+
143
+ | Option | Type | Default | Description |
144
+ |--------|------|---------|-------------|
145
+ | `provider` | string | auto-detect | `"claude"` or `"gemini"` |
146
+ | `baseBranch` | string | `"main"` | Branch to compare against |
147
+ | `model` | string | per-provider | Override the AI model |
148
+ | `maxTokens` | number | `4096` | Max response tokens |
149
+ | `outputDir` | string | `"./reviews"` | Where to write output files |
150
+ | `reviewRules` | string\|array | `"standard"` | Preset name or custom rules array |
151
+
152
+ ### Custom Rules Example
153
+
154
+ ```json
155
+ {
156
+ "provider": "gemini",
157
+ "reviewRules": [
158
+ "Use camelCase for variable names",
159
+ "All functions must have JSDoc comments",
160
+ "No inline styles in React components",
161
+ "Database queries must use parameterized statements"
162
+ ]
163
+ }
164
+ ```
165
+
166
+ ## Programmatic API
167
+
168
+ ```javascript
169
+ const {
170
+ reviewCode,
171
+ getDiff,
172
+ getChangedFiles,
173
+ detectProvider,
174
+ STANDARD_RULES,
175
+ } = require('git-diff-ai-reviewer');
176
+
177
+ const diff = getDiff('main');
178
+ const files = getChangedFiles('main');
179
+ const provider = detectProvider({ provider: 'gemini' });
180
+ const review = await reviewCode(diff, {
181
+ provider,
182
+ changedFiles: files,
183
+ branchName: 'feature/xyz',
184
+ reviewRules: STANDARD_RULES,
185
+ });
186
+ ```
187
+
188
+ ## Output Files
189
+
190
+ Review outputs are saved to `./reviews/` (configurable):
191
+ - `review-output-<timestamp>.txt` — Full review with severity-tagged findings
192
+ - `fix-prompt-<timestamp>.txt` — AI agent-ready prompt to auto-fix issues
193
+
194
+ ## CI Integration
195
+
196
+ ```yaml
197
+ # GitHub Actions example
198
+ - name: AI Code Review
199
+ run: npx git-diff-ai-reviewer review --provider claude
200
+ env:
201
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
202
+ ```
203
+
204
+ The CLI exits with code `1` on CRITICAL findings, making it suitable as a CI gate.
package/bin/review.js ADDED
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { getDiff, getChangedFiles, getBranchName, isGitRepo } = require('../src/git');
6
+ const { reviewCode, parseSeverityCounts, detectProvider, PROVIDERS, DEFAULT_MODELS } = require('../src/provider');
7
+ const { buildFixPrompt } = require('../src/prompts');
8
+ const { loadConfig } = require('../src/config');
9
+ const { writeReviewOutput, writeFixPrompt, findLatestReview } = require('../src/output');
10
+
11
+ // ─── Argument Parsing ────────────────────────────────────────────────
12
+
13
+ function parseArgs(argv) {
14
+ const args = argv.slice(2);
15
+ const flags = {};
16
+ let command = null;
17
+
18
+ for (let i = 0; i < args.length; i++) {
19
+ switch (args[i]) {
20
+ case '--base':
21
+ case '-b':
22
+ flags.baseBranch = args[++i];
23
+ break;
24
+ case '--output':
25
+ case '-o':
26
+ flags.outputDir = args[++i];
27
+ break;
28
+ case '--config':
29
+ case '-c':
30
+ flags.configPath = args[++i];
31
+ break;
32
+ case '--model':
33
+ case '-m':
34
+ flags.model = args[++i];
35
+ break;
36
+ case '--provider':
37
+ case '-p':
38
+ flags.provider = args[++i];
39
+ break;
40
+ case '--dry-run':
41
+ flags.dryRun = true;
42
+ break;
43
+ case '--help':
44
+ case '-h':
45
+ flags.help = true;
46
+ break;
47
+ default:
48
+ if (!args[i].startsWith('-') && !command) {
49
+ command = args[i];
50
+ } else {
51
+ console.warn(`Unknown option: ${args[i]}`);
52
+ }
53
+ }
54
+ }
55
+
56
+ return { command: command || 'review', flags };
57
+ }
58
+
59
+ // ─── Help ────────────────────────────────────────────────────────────
60
+
61
+ function showHelp() {
62
+ console.log(`
63
+ ╔══════════════════════════════════════════════════════╗
64
+ ║ 🤖 AI Code Review CLI ║
65
+ ╚══════════════════════════════════════════════════════╝
66
+
67
+ Usage: ai-review <command> [options]
68
+
69
+ Commands:
70
+ review Review code changes using AI (default)
71
+ fix Generate a fix prompt from the latest review
72
+
73
+ Options:
74
+ -b, --base <branch> Base branch to compare against (default: main)
75
+ -p, --provider <name> AI provider: 'claude' or 'gemini' (auto-detected from env)
76
+ -m, --model <model> Model name (defaults per provider)
77
+ -o, --output <dir> Output directory for review files (default: ./reviews)
78
+ -c, --config <path> Path to config file
79
+ --dry-run Extract diff and show prompt without calling AI API
80
+ -h, --help Show this help message
81
+
82
+ Providers:
83
+ claude Uses ANTHROPIC_API_KEY (default model: claude-sonnet-4-20250514)
84
+ gemini Uses GEMINI_API_KEY (default model: gemini-2.0-flash)
85
+
86
+ Severity Levels:
87
+ 🔴 CRITICAL Bugs, security issues, data loss — must fix
88
+ 🟡 WARNING Performance, bad practices — should fix
89
+ 🔵 SUGGESTION Style, readability — nice to have
90
+
91
+ Review Rule Presets:
92
+ basic Quick lint-level checks
93
+ standard Balanced everyday review (default)
94
+ comprehensive Full audit for critical code
95
+
96
+ Examples:
97
+ ai-review review Review with auto-detected provider
98
+ ai-review review --provider gemini Review using Gemini
99
+ ai-review review --base develop Review against develop branch
100
+ ai-review review --dry-run Preview without calling API
101
+ ai-review fix Generate fix prompt from latest review
102
+
103
+ Config File (.ai-review.config.json):
104
+ {
105
+ "provider": "claude",
106
+ "baseBranch": "main",
107
+ "model": "",
108
+ "maxTokens": 4096,
109
+ "outputDir": "./reviews",
110
+ "reviewRules": "standard"
111
+ }
112
+ `);
113
+ }
114
+
115
+ // ─── Review Command ─────────────────────────────────────────────────
116
+
117
+ async function commandReview(config, flags) {
118
+ const baseBranch = flags.baseBranch || config.baseBranch;
119
+ const outputDir = path.resolve(flags.outputDir || config.outputDir);
120
+
121
+ // Detect provider
122
+ const provider = flags.provider || detectProvider(config);
123
+ const model = flags.model || config.model || DEFAULT_MODELS[provider];
124
+
125
+ console.log('');
126
+ console.log('🔍 AI Code Review');
127
+ console.log('─'.repeat(40));
128
+ console.log(` Provider: ${provider}`);
129
+ console.log(` Model: ${model}`);
130
+
131
+ // Get branch and diff info
132
+ const branchName = getBranchName();
133
+ console.log(` Branch: ${branchName}`);
134
+ console.log(` Base: ${baseBranch}`);
135
+
136
+ const changedFiles = getChangedFiles(baseBranch);
137
+ if (changedFiles.length === 0) {
138
+ console.log('\n✅ No changes detected. Nothing to review.');
139
+ process.exit(0);
140
+ }
141
+ console.log(` Changed: ${changedFiles.length} file(s)`);
142
+ changedFiles.forEach(f => console.log(` - ${f}`));
143
+
144
+ const diff = getDiff(baseBranch);
145
+ if (!diff.trim()) {
146
+ console.log('\n✅ Empty diff. Nothing to review.');
147
+ process.exit(0);
148
+ }
149
+ console.log(` Diff size: ${diff.length} characters`);
150
+ console.log('─'.repeat(40));
151
+
152
+ // Dry run mode — show what would be sent
153
+ if (flags.dryRun) {
154
+ console.log('\n📋 DRY RUN — Diff extracted, API not called.\n');
155
+ console.log(`Provider: ${provider} | Model: ${model}`);
156
+ console.log(`Rules: ${config.reviewRules.length} active`);
157
+ console.log('Changed files:');
158
+ changedFiles.forEach(f => console.log(` - ${f}`));
159
+ console.log(`\nDiff preview (first 500 chars):\n${diff.slice(0, 500)}...`);
160
+ console.log('\n💡 Remove --dry-run to send to AI for review.');
161
+ process.exit(0);
162
+ }
163
+
164
+ // Send to AI provider
165
+ console.log(`\n⏳ Sending to ${provider} (${model}) for review...\n`);
166
+
167
+ try {
168
+ const reviewText = await reviewCode(diff, {
169
+ provider,
170
+ changedFiles,
171
+ branchName,
172
+ model,
173
+ maxTokens: config.maxTokens,
174
+ reviewRules: config.reviewRules,
175
+ });
176
+
177
+ // Parse severity counts
178
+ const severityCounts = parseSeverityCounts(reviewText);
179
+
180
+ // Write output
181
+ const outputPath = writeReviewOutput(reviewText, outputDir, {
182
+ branchName,
183
+ changedFiles,
184
+ severityCounts,
185
+ provider,
186
+ model,
187
+ });
188
+
189
+ // Summary
190
+ console.log('═'.repeat(40));
191
+ console.log(' Review Complete!');
192
+ console.log('═'.repeat(40));
193
+ console.log(` 🔴 Critical: ${severityCounts.critical}`);
194
+ console.log(` 🟡 Warning: ${severityCounts.warning}`);
195
+ console.log(` 🔵 Suggestion: ${severityCounts.suggestion}`);
196
+ console.log('─'.repeat(40));
197
+ console.log(` 📄 Output: ${outputPath}`);
198
+ console.log('');
199
+ console.log('💡 Run "ai-review fix" to generate an AI fix prompt.');
200
+ console.log('');
201
+
202
+ // Exit with non-zero if critical issues found
203
+ if (severityCounts.critical > 0) {
204
+ process.exit(1);
205
+ }
206
+ } catch (error) {
207
+ console.error(`\n❌ Review failed: ${error.message}`);
208
+ process.exit(2);
209
+ }
210
+ }
211
+
212
+ // ─── Fix Command ────────────────────────────────────────────────────
213
+
214
+ async function commandFix(config, flags) {
215
+ const baseBranch = flags.baseBranch || config.baseBranch;
216
+ const outputDir = path.resolve(flags.outputDir || config.outputDir);
217
+
218
+ console.log('');
219
+ console.log('🔧 Generate Fix Prompt');
220
+ console.log('─'.repeat(40));
221
+
222
+ // Find latest review
223
+ const latestReview = findLatestReview(outputDir);
224
+ if (!latestReview) {
225
+ console.error('❌ No review output found. Run "ai-review review" first.');
226
+ process.exit(1);
227
+ }
228
+
229
+ console.log(` Using review: ${latestReview}`);
230
+
231
+ const reviewText = fs.readFileSync(latestReview, 'utf-8');
232
+ const diff = getDiff(baseBranch);
233
+
234
+ // Build fix prompt
235
+ const fixPromptText = buildFixPrompt(reviewText, diff);
236
+
237
+ // Write fix prompt
238
+ const fixPath = writeFixPrompt(fixPromptText, outputDir);
239
+
240
+ console.log('─'.repeat(40));
241
+ console.log(` 📄 Fix prompt: ${fixPath}`);
242
+ console.log('');
243
+ console.log('💡 Paste the contents of this file into your AI agent to auto-fix issues.');
244
+ console.log('');
245
+ }
246
+
247
+ // ─── Main ───────────────────────────────────────────────────────────
248
+
249
+ async function main() {
250
+ const { command, flags } = parseArgs(process.argv);
251
+
252
+ if (flags.help) {
253
+ showHelp();
254
+ process.exit(0);
255
+ }
256
+
257
+ // Verify git repo
258
+ if (!isGitRepo()) {
259
+ console.error('❌ Not a git repository. Run this command from within a git project.');
260
+ process.exit(1);
261
+ }
262
+
263
+ // Load config
264
+ const config = loadConfig(flags.configPath);
265
+
266
+ switch (command) {
267
+ case 'review':
268
+ await commandReview(config, flags);
269
+ break;
270
+ case 'fix':
271
+ await commandFix(config, flags);
272
+ break;
273
+ default:
274
+ console.error(`❌ Unknown command: ${command}`);
275
+ console.error(' Run "ai-review --help" for usage info.');
276
+ process.exit(1);
277
+ }
278
+ }
279
+
280
+ main().catch(err => {
281
+ console.error(`\n❌ Unexpected error: ${err.message}`);
282
+ process.exit(2);
283
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "git-diff-ai-reviewer",
3
+ "version": "1.1.0",
4
+ "description": "AI-powered code review using Claude or Gemini API. Reviews git branch diffs and generates actionable fix prompts.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "git-diff-ai-reviewer": "./bin/review.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node bin/review.js --help"
11
+ },
12
+ "keywords": [
13
+ "code-review",
14
+ "ai",
15
+ "claude",
16
+ "gemini",
17
+ "linter",
18
+ "git",
19
+ "diff"
20
+ ],
21
+ "author": "Andrii Posia",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/andrii-posia/git-diff-ai-reviewer.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/andrii-posia/git-diff-ai-reviewer/issues"
29
+ },
30
+ "homepage": "https://github.com/andrii-posia/git-diff-ai-reviewer#readme",
31
+ "peerDependencies": {
32
+ "@anthropic-ai/sdk": "^0.39.0",
33
+ "@google/genai": "^0.14.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@anthropic-ai/sdk": {
37
+ "optional": true
38
+ },
39
+ "@google/genai": {
40
+ "optional": true
41
+ }
42
+ }
43
+ }
package/src/claude.js ADDED
@@ -0,0 +1,95 @@
1
+ const { buildReviewSystemPrompt, buildReviewUserPrompt } = require('./prompts');
2
+
3
+ /**
4
+ * Dynamically load the Anthropic SDK.
5
+ * @returns {Object} The Anthropic class
6
+ */
7
+ function loadAnthropicSDK() {
8
+ try {
9
+ return require('@anthropic-ai/sdk');
10
+ } catch {
11
+ throw new Error(
12
+ 'Claude SDK not installed. Run: npm install @anthropic-ai/sdk'
13
+ );
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Create an Anthropic client instance.
19
+ * @returns {Anthropic}
20
+ */
21
+ function createClient() {
22
+ const apiKey = process.env.ANTHROPIC_API_KEY;
23
+ if (!apiKey) {
24
+ throw new Error(
25
+ 'ANTHROPIC_API_KEY environment variable is not set.\n' +
26
+ 'Get your API key at https://console.anthropic.com/settings/keys\n' +
27
+ 'Then set it: export ANTHROPIC_API_KEY=your-key-here'
28
+ );
29
+ }
30
+ const Anthropic = loadAnthropicSDK();
31
+ return new Anthropic({ apiKey });
32
+ }
33
+
34
+ /**
35
+ * Send code diff to Claude for review.
36
+ * @param {string} diff - The git diff content
37
+ * @param {Object} options
38
+ * @param {string[]} options.changedFiles - List of changed file paths
39
+ * @param {string} options.branchName - Current branch name
40
+ * @param {string} [options.model] - Claude model to use
41
+ * @param {number} [options.maxTokens] - Max response tokens
42
+ * @param {string[]} [options.reviewRules] - Custom review rules
43
+ * @returns {Promise<string>} The review text
44
+ */
45
+ async function reviewCode(diff, options = {}) {
46
+ const {
47
+ changedFiles = [],
48
+ branchName = 'unknown',
49
+ model = 'claude-sonnet-4-20250514',
50
+ maxTokens = 4096,
51
+ reviewRules = [],
52
+ } = options;
53
+
54
+ const client = createClient();
55
+
56
+ const systemPrompt = buildReviewSystemPrompt({ reviewRules });
57
+ const userMessage = buildReviewUserPrompt(diff, changedFiles, branchName);
58
+
59
+ console.log(`🤖 Sending ${diff.length} chars of diff to Claude (${model})...`);
60
+
61
+ const response = await client.messages.create({
62
+ model,
63
+ max_tokens: maxTokens,
64
+ system: systemPrompt,
65
+ messages: [
66
+ { role: 'user', content: userMessage },
67
+ ],
68
+ });
69
+
70
+ const reviewText = response.content
71
+ .filter(block => block.type === 'text')
72
+ .map(block => block.text)
73
+ .join('\n');
74
+
75
+ return reviewText;
76
+ }
77
+
78
+ /**
79
+ * Parse review text to extract severity counts.
80
+ * @param {string} reviewText - Raw review text from Claude
81
+ * @returns {{ critical: number, warning: number, suggestion: number }}
82
+ */
83
+ function parseSeverityCounts(reviewText) {
84
+ const critical = (reviewText.match(/\[SEVERITY\]\s*CRITICAL/gi) || []).length;
85
+ const warning = (reviewText.match(/\[SEVERITY\]\s*WARNING/gi) || []).length;
86
+ const suggestion = (reviewText.match(/\[SEVERITY\]\s*SUGGESTION/gi) || []).length;
87
+
88
+ return { critical, warning, suggestion };
89
+ }
90
+
91
+ module.exports = {
92
+ createClient,
93
+ reviewCode,
94
+ parseSeverityCounts,
95
+ };
package/src/config.js ADDED
@@ -0,0 +1,52 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { resolveRules } = require('./rules');
4
+
5
+ const CONFIG_FILENAME = '.ai-review.config.json';
6
+
7
+ const DEFAULT_CONFIG = {
8
+ provider: '', // 'claude' or 'gemini' — auto-detected from env vars if empty
9
+ baseBranch: 'main',
10
+ model: '', // per-provider default applied at runtime
11
+ maxTokens: 4096,
12
+ outputDir: './reviews',
13
+ reviewRules: 'standard', // preset name ('basic', 'standard', 'comprehensive') or custom array
14
+ };
15
+
16
+ /**
17
+ * Load configuration from .ai-review.config.json in the project root.
18
+ * Falls back to defaults for missing values.
19
+ * @param {string} [configPath] - Explicit path to config file
20
+ * @param {string} [cwd] - Working directory to search for config
21
+ * @returns {Object} Merged configuration
22
+ */
23
+ function loadConfig(configPath, cwd = process.cwd()) {
24
+ const resolvedPath = configPath
25
+ ? path.resolve(cwd, configPath)
26
+ : path.join(cwd, CONFIG_FILENAME);
27
+
28
+ let fileConfig = {};
29
+
30
+ if (fs.existsSync(resolvedPath)) {
31
+ try {
32
+ const raw = fs.readFileSync(resolvedPath, 'utf-8');
33
+ fileConfig = JSON.parse(raw);
34
+ console.log(`📋 Loaded config from ${resolvedPath}`);
35
+ } catch (error) {
36
+ console.warn(`⚠️ Failed to parse config file ${resolvedPath}: ${error.message}`);
37
+ }
38
+ }
39
+
40
+ const merged = { ...DEFAULT_CONFIG, ...fileConfig };
41
+
42
+ // Resolve rule presets to actual rule arrays
43
+ merged.reviewRules = resolveRules(merged.reviewRules);
44
+
45
+ return merged;
46
+ }
47
+
48
+ module.exports = {
49
+ loadConfig,
50
+ DEFAULT_CONFIG,
51
+ CONFIG_FILENAME,
52
+ };
package/src/gemini.js ADDED
@@ -0,0 +1,76 @@
1
+ const { buildReviewSystemPrompt, buildReviewUserPrompt } = require('./prompts');
2
+
3
+ /**
4
+ * Dynamically load the Gemini SDK.
5
+ * @returns {Object} The GoogleGenAI class
6
+ */
7
+ function loadGeminiSDK() {
8
+ try {
9
+ return require('@google/genai');
10
+ } catch {
11
+ throw new Error(
12
+ 'Gemini SDK not installed. Run: npm install @google/genai'
13
+ );
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Create a Gemini client instance.
19
+ * @returns {Object} GoogleGenAI instance
20
+ */
21
+ function createGeminiClient() {
22
+ const apiKey = process.env.GEMINI_API_KEY;
23
+ if (!apiKey) {
24
+ throw new Error(
25
+ 'GEMINI_API_KEY environment variable is not set.\n' +
26
+ 'Get your API key at https://aistudio.google.com/apikey\n' +
27
+ 'Then set it: export GEMINI_API_KEY=your-key-here'
28
+ );
29
+ }
30
+ const { GoogleGenAI } = loadGeminiSDK();
31
+ return new GoogleGenAI({ apiKey });
32
+ }
33
+
34
+ /**
35
+ * Send code diff to Gemini for review.
36
+ * @param {string} diff - The git diff content
37
+ * @param {Object} options
38
+ * @param {string[]} options.changedFiles - List of changed file paths
39
+ * @param {string} options.branchName - Current branch name
40
+ * @param {string} [options.model] - Gemini model to use
41
+ * @param {number} [options.maxTokens] - Max response tokens
42
+ * @param {string[]} [options.reviewRules] - Custom review rules
43
+ * @returns {Promise<string>} The review text
44
+ */
45
+ async function reviewCodeWithGemini(diff, options = {}) {
46
+ const {
47
+ changedFiles = [],
48
+ branchName = 'unknown',
49
+ model = 'gemini-2.0-flash',
50
+ maxTokens = 4096,
51
+ reviewRules = [],
52
+ } = options;
53
+
54
+ const client = createGeminiClient();
55
+
56
+ const systemPrompt = buildReviewSystemPrompt({ reviewRules });
57
+ const userMessage = buildReviewUserPrompt(diff, changedFiles, branchName);
58
+
59
+ console.log(`🤖 Sending ${diff.length} chars of diff to Gemini (${model})...`);
60
+
61
+ const response = await client.models.generateContent({
62
+ model,
63
+ contents: userMessage,
64
+ config: {
65
+ systemInstruction: systemPrompt,
66
+ maxOutputTokens: maxTokens,
67
+ },
68
+ });
69
+
70
+ return response.text;
71
+ }
72
+
73
+ module.exports = {
74
+ createGeminiClient,
75
+ reviewCodeWithGemini,
76
+ };
package/src/git.js ADDED
@@ -0,0 +1,113 @@
1
+ const { execSync } = require('child_process');
2
+
3
+ /**
4
+ * Get the diff between the current branch and a base branch.
5
+ * @param {string} baseBranch - The base branch to compare against (default: 'main')
6
+ * @param {string} cwd - Working directory (default: process.cwd())
7
+ * @returns {string} The git diff output
8
+ */
9
+ function getDiff(baseBranch = 'main', cwd = process.cwd()) {
10
+ try {
11
+ // First try three-dot diff (for branch comparison)
12
+ const diff = execSync(`git diff ${baseBranch}...HEAD`, {
13
+ cwd,
14
+ encoding: 'utf-8',
15
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large diffs
16
+ });
17
+
18
+ if (!diff.trim()) {
19
+ // Fallback: try two-dot diff (for uncommitted changes)
20
+ const uncommitted = execSync('git diff HEAD', {
21
+ cwd,
22
+ encoding: 'utf-8',
23
+ maxBuffer: 10 * 1024 * 1024,
24
+ });
25
+
26
+ if (!uncommitted.trim()) {
27
+ // Also check staged changes
28
+ const staged = execSync('git diff --cached', {
29
+ cwd,
30
+ encoding: 'utf-8',
31
+ maxBuffer: 10 * 1024 * 1024,
32
+ });
33
+ return staged;
34
+ }
35
+ return uncommitted;
36
+ }
37
+
38
+ return diff;
39
+ } catch (error) {
40
+ throw new Error(`Failed to get git diff: ${error.message}`);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get list of changed files between current branch and base branch.
46
+ * @param {string} baseBranch - The base branch to compare against
47
+ * @param {string} cwd - Working directory
48
+ * @returns {string[]} Array of changed file paths
49
+ */
50
+ function getChangedFiles(baseBranch = 'main', cwd = process.cwd()) {
51
+ try {
52
+ const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, {
53
+ cwd,
54
+ encoding: 'utf-8',
55
+ });
56
+
57
+ const files = output.trim().split('\n').filter(Boolean);
58
+
59
+ if (files.length === 0) {
60
+ // Fallback to uncommitted changes
61
+ const uncommitted = execSync('git diff --name-only HEAD', {
62
+ cwd,
63
+ encoding: 'utf-8',
64
+ });
65
+ return uncommitted.trim().split('\n').filter(Boolean);
66
+ }
67
+
68
+ return files;
69
+ } catch (error) {
70
+ throw new Error(`Failed to get changed files: ${error.message}`);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Get the current branch name.
76
+ * @param {string} cwd - Working directory
77
+ * @returns {string} Current branch name
78
+ */
79
+ function getBranchName(cwd = process.cwd()) {
80
+ try {
81
+ return execSync('git rev-parse --abbrev-ref HEAD', {
82
+ cwd,
83
+ encoding: 'utf-8',
84
+ }).trim();
85
+ } catch (error) {
86
+ throw new Error(`Failed to get branch name: ${error.message}`);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Check if we are inside a git repository.
92
+ * @param {string} cwd - Working directory
93
+ * @returns {boolean}
94
+ */
95
+ function isGitRepo(cwd = process.cwd()) {
96
+ try {
97
+ execSync('git rev-parse --is-inside-work-tree', {
98
+ cwd,
99
+ encoding: 'utf-8',
100
+ stdio: 'pipe',
101
+ });
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ module.exports = {
109
+ getDiff,
110
+ getChangedFiles,
111
+ getBranchName,
112
+ isGitRepo,
113
+ };
package/src/index.js ADDED
@@ -0,0 +1,41 @@
1
+ const { getDiff, getChangedFiles, getBranchName, isGitRepo } = require('./git');
2
+ const { reviewCode, parseSeverityCounts, detectProvider, PROVIDERS, DEFAULT_MODELS } = require('./provider');
3
+ const { buildFixPrompt, buildReviewSystemPrompt, buildReviewUserPrompt } = require('./prompts');
4
+ const { loadConfig } = require('./config');
5
+ const { writeReviewOutput, writeFixPrompt, findLatestReview } = require('./output');
6
+ const { BASIC_RULES, STANDARD_RULES, COMPREHENSIVE_RULES, RULE_PRESETS, resolveRules } = require('./rules');
7
+
8
+ module.exports = {
9
+ // Git utilities
10
+ getDiff,
11
+ getChangedFiles,
12
+ getBranchName,
13
+ isGitRepo,
14
+
15
+ // AI providers
16
+ reviewCode,
17
+ parseSeverityCounts,
18
+ detectProvider,
19
+ PROVIDERS,
20
+ DEFAULT_MODELS,
21
+
22
+ // Prompts
23
+ buildFixPrompt,
24
+ buildReviewSystemPrompt,
25
+ buildReviewUserPrompt,
26
+
27
+ // Config
28
+ loadConfig,
29
+
30
+ // Output
31
+ writeReviewOutput,
32
+ writeFixPrompt,
33
+ findLatestReview,
34
+
35
+ // Rules
36
+ BASIC_RULES,
37
+ STANDARD_RULES,
38
+ COMPREHENSIVE_RULES,
39
+ RULE_PRESETS,
40
+ resolveRules,
41
+ };
package/src/output.js ADDED
@@ -0,0 +1,119 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Ensure the output directory exists.
6
+ * @param {string} dir - Directory path
7
+ */
8
+ function ensureDir(dir) {
9
+ if (!fs.existsSync(dir)) {
10
+ fs.mkdirSync(dir, { recursive: true });
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Generate a timestamped filename.
16
+ * @param {string} prefix - File prefix (e.g., 'review-output')
17
+ * @param {string} ext - File extension (e.g., '.txt')
18
+ * @returns {string}
19
+ */
20
+ function timestampedFilename(prefix, ext = '.txt') {
21
+ const now = new Date();
22
+ const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
23
+ return `${prefix}-${ts}${ext}`;
24
+ }
25
+
26
+ /**
27
+ * Write the review output to a text file.
28
+ * @param {string} reviewText - The review content
29
+ * @param {string} outputDir - Output directory path
30
+ * @param {Object} meta - Metadata to include in the header
31
+ * @param {string} meta.branchName - Branch name
32
+ * @param {string[]} meta.changedFiles - List of changed files
33
+ * @param {{ critical: number, warning: number, suggestion: number }} meta.severityCounts
34
+ * @returns {string} Path to the written file
35
+ */
36
+ function writeReviewOutput(reviewText, outputDir, meta = {}) {
37
+ ensureDir(outputDir);
38
+
39
+ const filename = timestampedFilename('review-output');
40
+ const filePath = path.join(outputDir, filename);
41
+
42
+ const header = [
43
+ '═'.repeat(60),
44
+ ' AI CODE REVIEW',
45
+ '═'.repeat(60),
46
+ ` Provider: ${meta.provider || 'unknown'}`,
47
+ ` Model: ${meta.model || 'unknown'}`,
48
+ ` Branch: ${meta.branchName || 'unknown'}`,
49
+ ` Date: ${new Date().toISOString()}`,
50
+ ` Files: ${(meta.changedFiles || []).length} changed`,
51
+ '',
52
+ ];
53
+
54
+ if (meta.severityCounts) {
55
+ const sc = meta.severityCounts;
56
+ header.push(
57
+ ' Severity Summary:',
58
+ ` 🔴 CRITICAL: ${sc.critical}`,
59
+ ` 🟡 WARNING: ${sc.warning}`,
60
+ ` 🔵 SUGGESTION: ${sc.suggestion}`,
61
+ '',
62
+ );
63
+ }
64
+
65
+ header.push('═'.repeat(60), '');
66
+
67
+ const content = header.join('\n') + '\n' + reviewText + '\n';
68
+
69
+ fs.writeFileSync(filePath, content, 'utf-8');
70
+ return filePath;
71
+ }
72
+
73
+ /**
74
+ * Write the fix prompt to a text file.
75
+ * @param {string} promptText - The fix prompt content
76
+ * @param {string} outputDir - Output directory path
77
+ * @returns {string} Path to the written file
78
+ */
79
+ function writeFixPrompt(promptText, outputDir) {
80
+ ensureDir(outputDir);
81
+
82
+ const filename = timestampedFilename('fix-prompt');
83
+ const filePath = path.join(outputDir, filename);
84
+
85
+ const header = [
86
+ '═'.repeat(60),
87
+ ' AI FIX PROMPT — paste this into your AI agent',
88
+ '═'.repeat(60),
89
+ ` Generated: ${new Date().toISOString()}`,
90
+ '═'.repeat(60),
91
+ '',
92
+ ].join('\n');
93
+
94
+ fs.writeFileSync(filePath, header + '\n' + promptText + '\n', 'utf-8');
95
+ return filePath;
96
+ }
97
+
98
+ /**
99
+ * Find the most recent review output file in the output directory.
100
+ * @param {string} outputDir - Output directory path
101
+ * @returns {string|null} Path to the latest review file, or null
102
+ */
103
+ function findLatestReview(outputDir) {
104
+ if (!fs.existsSync(outputDir)) return null;
105
+
106
+ const files = fs.readdirSync(outputDir)
107
+ .filter(f => f.startsWith('review-output-') && f.endsWith('.txt'))
108
+ .sort()
109
+ .reverse();
110
+
111
+ return files.length > 0 ? path.join(outputDir, files[0]) : null;
112
+ }
113
+
114
+ module.exports = {
115
+ writeReviewOutput,
116
+ writeFixPrompt,
117
+ findLatestReview,
118
+ ensureDir,
119
+ };
package/src/prompts.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Severity levels for review findings.
3
+ */
4
+ const SEVERITY_LEVELS = {
5
+ CRITICAL: 'CRITICAL', // Bugs, security issues, data loss risks
6
+ WARNING: 'WARNING', // Performance issues, bad practices, potential bugs
7
+ SUGGESTION: 'SUGGESTION', // Style, readability, minor improvements
8
+ };
9
+
10
+ /**
11
+ * Build the system prompt for code review.
12
+ * @param {Object} options
13
+ * @param {string[]} [options.reviewRules] - Additional custom review rules
14
+ * @returns {string}
15
+ */
16
+ function buildReviewSystemPrompt(options = {}) {
17
+ const customRules = options.reviewRules
18
+ ? `\n\nAdditional review rules provided by the project:\n${options.reviewRules.map((r, i) => `${i + 1}. ${r}`).join('\n')}`
19
+ : '';
20
+
21
+ return `You are an expert code reviewer. You review git diffs and provide actionable, specific feedback.
22
+
23
+ Your review MUST follow this exact output format for each finding:
24
+
25
+ ---
26
+ [SEVERITY] CRITICAL | WARNING | SUGGESTION
27
+ [FILE] <filename>
28
+ [LINE] <line number or range, e.g., 42 or 42-50>
29
+ [TITLE] <short summary of the issue>
30
+ [DESCRIPTION]
31
+ <detailed explanation of the issue>
32
+ [FIX]
33
+ <specific code or instructions to fix the issue>
34
+ ---
35
+
36
+ Severity definitions:
37
+ - CRITICAL: Bugs, security vulnerabilities, data loss risks, crashes, broken functionality. These MUST be fixed before merging.
38
+ - WARNING: Performance issues, bad practices, potential future bugs, missing error handling. Should be addressed.
39
+ - SUGGESTION: Code style, readability improvements, refactoring opportunities, documentation. Nice to have.
40
+
41
+ Rules:
42
+ 1. Be specific — reference exact file names and line numbers from the diff.
43
+ 2. Provide concrete fix suggestions, not vague advice.
44
+ 3. Focus on what changed in the diff, not preexisting code.
45
+ 4. Group related issues together if they share the same root cause.
46
+ 5. At the end, provide a SUMMARY section with counts by severity level.
47
+ 6. If the code looks good, say so — don't invent issues.${customRules}`;
48
+ }
49
+
50
+ /**
51
+ * Build the user message for code review.
52
+ * @param {string} diff - The git diff content
53
+ * @param {string[]} changedFiles - List of changed files
54
+ * @param {string} branchName - Current branch name
55
+ * @returns {string}
56
+ */
57
+ function buildReviewUserPrompt(diff, changedFiles, branchName) {
58
+ return `Please review the following code changes on branch "${branchName}".
59
+
60
+ Changed files:
61
+ ${changedFiles.map(f => `- ${f}`).join('\n')}
62
+
63
+ Git diff:
64
+ \`\`\`diff
65
+ ${diff}
66
+ \`\`\`
67
+
68
+ Provide your review following the structured format specified in your instructions.`;
69
+ }
70
+
71
+ /**
72
+ * Build a prompt that an AI agent can use to automatically fix issues found in the review.
73
+ * @param {string} reviewText - The review output text
74
+ * @param {string} diff - The original git diff
75
+ * @returns {string}
76
+ */
77
+ function buildFixPrompt(reviewText, diff) {
78
+ return `You are an AI coding agent. Below is a code review of recent changes, followed by the original diff.
79
+ Your task is to fix ALL issues marked as CRITICAL and WARNING. For SUGGESTION items, fix them only if they are trivial.
80
+
81
+ Apply the fixes directly to the source files. For each fix:
82
+ 1. State which file and line you are modifying.
83
+ 2. Show the exact change (before → after).
84
+ 3. Explain briefly why the change is needed.
85
+
86
+ === CODE REVIEW ===
87
+ ${reviewText}
88
+
89
+ === ORIGINAL DIFF ===
90
+ \`\`\`diff
91
+ ${diff}
92
+ \`\`\`
93
+
94
+ === INSTRUCTIONS ===
95
+ - Fix CRITICAL issues first, then WARNING, then SUGGESTION.
96
+ - Do not introduce new bugs or change unrelated code.
97
+ - If a fix requires refactoring, explain the approach before applying.
98
+ - Output each fix in a clear, copy-pasteable format.
99
+
100
+ Begin fixing the issues now.`;
101
+ }
102
+
103
+ module.exports = {
104
+ SEVERITY_LEVELS,
105
+ buildReviewSystemPrompt,
106
+ buildReviewUserPrompt,
107
+ buildFixPrompt,
108
+ };
@@ -0,0 +1,84 @@
1
+ const { reviewCode: reviewWithClaude, parseSeverityCounts } = require('./claude');
2
+ const { reviewCodeWithGemini } = require('./gemini');
3
+
4
+ /**
5
+ * Supported AI providers.
6
+ */
7
+ const PROVIDERS = {
8
+ CLAUDE: 'claude',
9
+ GEMINI: 'gemini',
10
+ };
11
+
12
+ /**
13
+ * Default models per provider.
14
+ */
15
+ const DEFAULT_MODELS = {
16
+ [PROVIDERS.CLAUDE]: 'claude-sonnet-4-20250514',
17
+ [PROVIDERS.GEMINI]: 'gemini-2.0-flash',
18
+ };
19
+
20
+ /**
21
+ * Detect which provider to use based on config and available env vars.
22
+ * Priority: explicit config > available API key.
23
+ * @param {Object} config - Loaded config
24
+ * @returns {string} provider name
25
+ */
26
+ function detectProvider(config) {
27
+ // Explicit config takes priority
28
+ if (config.provider) {
29
+ return config.provider.toLowerCase();
30
+ }
31
+
32
+ // Auto-detect from available API keys
33
+ if (process.env.ANTHROPIC_API_KEY) return PROVIDERS.CLAUDE;
34
+ if (process.env.GEMINI_API_KEY) return PROVIDERS.GEMINI;
35
+
36
+ throw new Error(
37
+ 'No AI provider configured.\n' +
38
+ 'Either set "provider" in .ai-review.config.json, or set one of:\n' +
39
+ ' - ANTHROPIC_API_KEY for Claude\n' +
40
+ ' - GEMINI_API_KEY for Gemini'
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Review code using the configured AI provider.
46
+ * @param {string} diff - The git diff content
47
+ * @param {Object} options - Review options
48
+ * @param {string} options.provider - AI provider ('claude' or 'gemini')
49
+ * @param {string[]} options.changedFiles
50
+ * @param {string} options.branchName
51
+ * @param {string} [options.model]
52
+ * @param {number} [options.maxTokens]
53
+ * @param {string[]} [options.reviewRules]
54
+ * @returns {Promise<string>} Review text
55
+ */
56
+ async function reviewCode(diff, options = {}) {
57
+ const provider = options.provider || PROVIDERS.CLAUDE;
58
+
59
+ // Use provider-specific default model if not explicitly set
60
+ const model = options.model || DEFAULT_MODELS[provider];
61
+
62
+ const reviewOptions = { ...options, model };
63
+
64
+ switch (provider) {
65
+ case PROVIDERS.CLAUDE:
66
+ return reviewWithClaude(diff, reviewOptions);
67
+
68
+ case PROVIDERS.GEMINI:
69
+ return reviewCodeWithGemini(diff, reviewOptions);
70
+
71
+ default:
72
+ throw new Error(
73
+ `Unknown provider: "${provider}". Supported: ${Object.values(PROVIDERS).join(', ')}`
74
+ );
75
+ }
76
+ }
77
+
78
+ module.exports = {
79
+ PROVIDERS,
80
+ DEFAULT_MODELS,
81
+ detectProvider,
82
+ reviewCode,
83
+ parseSeverityCounts,
84
+ };
package/src/rules.js ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Default review rule sets that can be used out of the box.
3
+ * Users can reference these by name in .ai-review.config.json
4
+ * or customize their own rules.
5
+ */
6
+
7
+ /**
8
+ * Basic rules — lightweight, fast review for quick PRs.
9
+ */
10
+ const BASIC_RULES = [
11
+ 'Check for syntax errors and typos',
12
+ 'Flag unused variables and dead code',
13
+ 'Check for console.log or debug statements left in production code',
14
+ 'Verify error handling exists for async operations',
15
+ 'Check for hardcoded secrets, API keys, or passwords',
16
+ ];
17
+
18
+ /**
19
+ * Standard rules — balanced review covering common issues.
20
+ */
21
+ const STANDARD_RULES = [
22
+ ...BASIC_RULES,
23
+ 'Check for proper input validation and sanitization',
24
+ 'Verify functions have clear, descriptive names',
25
+ 'Flag overly complex functions (too many parameters, deep nesting, long functions)',
26
+ 'Check for missing or incorrect error messages',
27
+ 'Verify proper use of const/let (no unnecessary var usage)',
28
+ 'Check for potential null/undefined reference errors',
29
+ 'Flag duplicated code that could be extracted into reusable functions',
30
+ 'Verify that async/await and Promises are used correctly',
31
+ 'Check for proper resource cleanup (event listeners, timers, connections)',
32
+ ];
33
+
34
+ /**
35
+ * Comprehensive rules — thorough review for critical code.
36
+ */
37
+ const COMPREHENSIVE_RULES = [
38
+ ...STANDARD_RULES,
39
+ 'Check for SQL injection, XSS, and other security vulnerabilities',
40
+ 'Verify proper authentication and authorization checks',
41
+ 'Review error handling strategy — are errors logged, reported, and recoverable?',
42
+ 'Check for race conditions in concurrent or async code',
43
+ 'Verify proper memory management (no memory leaks, large object retention)',
44
+ 'Check for proper HTTP status codes and API response formats',
45
+ 'Review database queries for N+1 problems and missing indexes',
46
+ 'Verify backward compatibility — will this break existing clients or APIs?',
47
+ 'Check for accessibility issues in UI code (ARIA labels, keyboard navigation)',
48
+ 'Review test coverage — are edge cases and error paths tested?',
49
+ 'Check for proper logging with appropriate log levels',
50
+ 'Verify that environment-specific config is not hardcoded',
51
+ 'Check for proper handling of edge cases (empty arrays, zero values, boundary conditions)',
52
+ 'Review for performance issues (unnecessary re-renders, expensive computations in loops)',
53
+ ];
54
+
55
+ /**
56
+ * Named rule presets that can be referenced in config.
57
+ */
58
+ const RULE_PRESETS = {
59
+ basic: BASIC_RULES,
60
+ standard: STANDARD_RULES,
61
+ comprehensive: COMPREHENSIVE_RULES,
62
+ };
63
+
64
+ /**
65
+ * Resolve review rules from config.
66
+ * Accepts a preset name (string) or custom rules array.
67
+ * @param {string|string[]} rulesConfig - Preset name or custom rules
68
+ * @returns {string[]} Resolved rules
69
+ */
70
+ function resolveRules(rulesConfig) {
71
+ if (!rulesConfig || (Array.isArray(rulesConfig) && rulesConfig.length === 0)) {
72
+ return STANDARD_RULES; // Default to standard if nothing specified
73
+ }
74
+
75
+ if (typeof rulesConfig === 'string') {
76
+ const preset = RULE_PRESETS[rulesConfig.toLowerCase()];
77
+ if (!preset) {
78
+ console.warn(`⚠️ Unknown rule preset: "${rulesConfig}". Using "standard".`);
79
+ return STANDARD_RULES;
80
+ }
81
+ return preset;
82
+ }
83
+
84
+ if (Array.isArray(rulesConfig)) {
85
+ return rulesConfig;
86
+ }
87
+
88
+ return STANDARD_RULES;
89
+ }
90
+
91
+ module.exports = {
92
+ BASIC_RULES,
93
+ STANDARD_RULES,
94
+ COMPREHENSIVE_RULES,
95
+ RULE_PRESETS,
96
+ resolveRules,
97
+ };