@aaronshaf/ger 2.0.10 → 3.0.2

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.
@@ -1,486 +0,0 @@
1
- import { Effect, pipe, Schema } from 'effect'
2
- import { ReviewStrategyService, ReviewStrategyError } from '@/services/review-strategy'
3
- import { commentCommandWithInput } from './comment'
4
- import { Console } from 'effect'
5
- import { GerritApiService } from '@/api/gerrit'
6
- import { buildEnhancedPrompt } from '@/utils/review-prompt-builder'
7
- import * as fs from 'node:fs/promises'
8
- import * as fsSync from 'node:fs'
9
- import * as os from 'node:os'
10
- import * as path from 'node:path'
11
- import { fileURLToPath } from 'node:url'
12
- import { dirname } from 'node:path'
13
- import * as readline from 'node:readline'
14
- import { GitWorktreeService } from '@/services/git-worktree'
15
-
16
- // Get the directory of this module
17
- const __filename = fileURLToPath(import.meta.url)
18
- const __dirname = dirname(__filename)
19
-
20
- // Effect-based file reading helper
21
- const readFileEffect = (filePath: string): Effect.Effect<string, Error, never> =>
22
- Effect.tryPromise({
23
- try: () => fs.readFile(filePath, 'utf8'),
24
- catch: (error) => new Error(`Failed to read file ${filePath}: ${error}`),
25
- })
26
-
27
- // Load default prompts from .md files using Effect
28
- const loadDefaultPrompts = Effect.gen(function* () {
29
- const defaultReviewPrompt = yield* readFileEffect(
30
- path.join(__dirname, '../../prompts/default-review.md'),
31
- )
32
- const inlineReviewSystemPrompt = yield* readFileEffect(
33
- path.join(__dirname, '../../prompts/system-inline-review.md'),
34
- )
35
- const overallReviewSystemPrompt = yield* readFileEffect(
36
- path.join(__dirname, '../../prompts/system-overall-review.md'),
37
- )
38
-
39
- return {
40
- defaultReviewPrompt,
41
- inlineReviewSystemPrompt,
42
- overallReviewSystemPrompt,
43
- }
44
- })
45
-
46
- // Helper to expand tilde in file paths
47
- const expandTilde = (filePath: string): string => {
48
- if (filePath.startsWith('~/')) {
49
- return path.join(os.homedir(), filePath.slice(2))
50
- }
51
- return filePath
52
- }
53
-
54
- // Helper to read prompt file using Effect
55
- const readPromptFileEffect = (filePath: string): Effect.Effect<string | null, never, never> =>
56
- Effect.gen(function* () {
57
- const expanded = expandTilde(filePath)
58
-
59
- // Check if file exists using sync method since Effect doesn't have a convenient async exists check
60
- const exists = yield* Effect.try(() => fsSync.existsSync(expanded)).pipe(
61
- Effect.catchAll(() => Effect.succeed(false)),
62
- )
63
-
64
- if (!exists) {
65
- return null
66
- }
67
-
68
- // Read file using async Effect
69
- const content = yield* readFileEffect(expanded).pipe(
70
- Effect.catchAll(() => Effect.succeed(null)),
71
- )
72
-
73
- return content
74
- })
75
-
76
- interface ReviewOptions {
77
- debug?: boolean
78
- dryRun?: boolean
79
- comment?: boolean
80
- yes?: boolean
81
- prompt?: string
82
- tool?: string
83
- systemPrompt?: string
84
- }
85
-
86
- // Schema for validating AI-generated inline comments
87
- const InlineCommentSchema = Schema.Struct({
88
- file: Schema.String,
89
- message: Schema.String,
90
- side: Schema.optional(Schema.String),
91
- line: Schema.optional(Schema.Number),
92
- range: Schema.optional(
93
- Schema.Struct({
94
- start_line: Schema.Number,
95
- end_line: Schema.Number,
96
- start_character: Schema.optional(Schema.Number),
97
- end_character: Schema.optional(Schema.Number),
98
- }),
99
- ),
100
- })
101
-
102
- interface InlineComment extends Schema.Schema.Type<typeof InlineCommentSchema> {}
103
-
104
- // Helper to validate and fix AI-generated inline comments
105
- const validateAndFixInlineComments = (
106
- rawComments: unknown[],
107
- availableFiles: string[],
108
- ): Effect.Effect<InlineComment[], never, never> =>
109
- Effect.gen(function* () {
110
- const validComments: InlineComment[] = []
111
-
112
- for (const rawComment of rawComments) {
113
- // Validate comment structure using Effect Schema
114
- const parseResult = yield* Schema.decodeUnknown(InlineCommentSchema)(rawComment).pipe(
115
- Effect.catchTag('ParseError', (_parseError) =>
116
- Effect.gen(function* () {
117
- yield* Console.warn('Skipping comment with invalid structure')
118
- return yield* Effect.succeed(null)
119
- }),
120
- ),
121
- )
122
-
123
- if (!parseResult) {
124
- continue
125
- }
126
-
127
- const comment = parseResult
128
-
129
- // Skip comments with invalid line formats (like ":range")
130
- if (!comment.line && !comment.range) {
131
- yield* Console.warn('Skipping comment with invalid line format')
132
- continue
133
- }
134
-
135
- // Try to find the correct file path
136
- let correctFilePath = comment.file
137
-
138
- // If the file path doesn't exist exactly, try to find a match
139
- if (!availableFiles.includes(comment.file)) {
140
- // Look for files that end with the provided path (secure path matching)
141
- const matchingFiles = availableFiles.filter((file) => {
142
- const normalizedFile = file.replace(/\\/g, '/')
143
- const normalizedComment = comment.file.replace(/\\/g, '/')
144
-
145
- // Only match if the comment path is a suffix of the file path with proper boundaries
146
- return (
147
- normalizedFile.endsWith(normalizedComment) &&
148
- (normalizedFile === normalizedComment ||
149
- normalizedFile.endsWith(`/${normalizedComment}`))
150
- )
151
- })
152
-
153
- if (matchingFiles.length === 1) {
154
- correctFilePath = matchingFiles[0]
155
- yield* Console.log(`Fixed file path: ${comment.file} -> ${correctFilePath}`)
156
- } else if (matchingFiles.length > 1) {
157
- // Multiple matches, try to pick the most likely one (exact suffix match)
158
- const exactMatch = matchingFiles.find((file) => file.endsWith(`/${comment.file}`))
159
- if (exactMatch) {
160
- correctFilePath = exactMatch
161
- yield* Console.log(
162
- `Fixed file path (exact match): ${comment.file} -> ${correctFilePath}`,
163
- )
164
- } else {
165
- yield* Console.warn(`Multiple file matches for ${comment.file}. Skipping comment.`)
166
- continue
167
- }
168
- } else {
169
- yield* Console.warn(`File not found in change: ${comment.file}. Skipping comment.`)
170
- continue
171
- }
172
- }
173
-
174
- // Update the comment with the correct file path and add to valid comments
175
- validComments.push({ ...comment, file: correctFilePath })
176
- }
177
-
178
- return validComments
179
- })
180
-
181
- // Helper function to prompt user for confirmation
182
- const promptUser = (message: string): Effect.Effect<boolean, never> =>
183
- Effect.async<boolean, never>((resume) => {
184
- const rl = readline.createInterface({
185
- input: process.stdin,
186
- output: process.stdout,
187
- })
188
-
189
- rl.question(`${message} [y/N]: `, (answer: string) => {
190
- rl.close()
191
- resume(Effect.succeed(answer.toLowerCase() === 'y'))
192
- })
193
- })
194
-
195
- export const reviewCommand = (
196
- changeId: string,
197
- options: ReviewOptions = {},
198
- ): Effect.Effect<
199
- void,
200
- Error | ReviewStrategyError,
201
- GerritApiService | ReviewStrategyService | GitWorktreeService
202
- > =>
203
- Effect.gen(function* () {
204
- const reviewStrategy = yield* ReviewStrategyService
205
- const gitService = yield* GitWorktreeService
206
-
207
- // Load default prompts
208
- const prompts = yield* loadDefaultPrompts
209
-
210
- // Validate preconditions
211
- yield* gitService.validatePreconditions()
212
-
213
- // Check for available AI strategies
214
- yield* Console.log('→ Checking AI tool availability...')
215
- const availableStrategies = yield* reviewStrategy.getAvailableStrategies()
216
-
217
- if (availableStrategies.length === 0) {
218
- return yield* Effect.fail(
219
- new Error('No AI tools available. Please install claude, gemini, or opencode CLI.'),
220
- )
221
- }
222
-
223
- // Select strategy based on user preference
224
- const selectedStrategy = yield* reviewStrategy.selectStrategy(options.tool)
225
- yield* Console.log(`✓ Using AI tool: ${selectedStrategy.name}`)
226
-
227
- // Load custom review prompt if provided
228
- let userReviewPrompt = prompts.defaultReviewPrompt
229
-
230
- if (options.prompt) {
231
- const customPrompt = yield* readPromptFileEffect(options.prompt)
232
- if (customPrompt) {
233
- userReviewPrompt = customPrompt
234
- yield* Console.log(`✓ Using custom review prompt from ${options.prompt}`)
235
- } else {
236
- yield* Console.log(`⚠ Could not read custom prompt file: ${options.prompt}`)
237
- yield* Console.log('→ Using default review prompt')
238
- }
239
- }
240
-
241
- // Use Effect's resource management for worktree lifecycle
242
- yield* Effect.acquireUseRelease(
243
- // Acquire: Create worktree and setup
244
- Effect.gen(function* () {
245
- const worktreeInfo = yield* gitService.createWorktree(changeId)
246
- yield* gitService.fetchAndCheckoutPatchset(worktreeInfo)
247
- return worktreeInfo
248
- }),
249
-
250
- // Use: Run the enhanced review process
251
- (worktreeInfo) =>
252
- Effect.gen(function* () {
253
- // Switch to worktree directory
254
- const originalCwd = process.cwd()
255
- process.chdir(worktreeInfo.path)
256
-
257
- try {
258
- // Get changed files from git
259
- const changedFiles = yield* gitService.getChangedFiles()
260
-
261
- yield* Console.log(`→ Found ${changedFiles.length} changed files`)
262
- if (options.debug) {
263
- yield* Console.log(`[DEBUG] Changed files: ${changedFiles.join(', ')}`)
264
- }
265
-
266
- // Stage 1: Generate inline comments
267
- yield* Console.log(`→ Generating inline comments for change ${changeId}...`)
268
-
269
- const inlinePrompt = yield* buildEnhancedPrompt(
270
- userReviewPrompt,
271
- prompts.inlineReviewSystemPrompt,
272
- changeId,
273
- changedFiles,
274
- )
275
-
276
- // Run inline review using selected strategy
277
- if (options.debug) {
278
- yield* Console.log(`[DEBUG] Running inline review with ${selectedStrategy.name}`)
279
- yield* Console.log(`[DEBUG] Working directory: ${worktreeInfo.path}`)
280
- }
281
-
282
- const inlineResponse = yield* reviewStrategy
283
- .executeWithStrategy(selectedStrategy, inlinePrompt, {
284
- cwd: worktreeInfo.path,
285
- systemPrompt: options.systemPrompt || prompts.inlineReviewSystemPrompt,
286
- })
287
- .pipe(
288
- Effect.catchTag('ReviewStrategyError', (error) =>
289
- Effect.gen(function* () {
290
- yield* Console.error(`✗ Inline review failed: ${error.message}`)
291
- return yield* Effect.fail(new Error(error.message))
292
- }),
293
- ),
294
- )
295
-
296
- if (options.debug) {
297
- yield* Console.log(`[DEBUG] Inline review completed`)
298
- yield* Console.log(`[DEBUG] Response length: ${inlineResponse.length} chars`)
299
- }
300
-
301
- // Response content is ready for parsing
302
- const extractedInlineResponse = inlineResponse.trim()
303
-
304
- if (options.debug) {
305
- yield* Console.log(
306
- `[DEBUG] Extracted response for parsing:\n${extractedInlineResponse}`,
307
- )
308
- }
309
-
310
- // Parse JSON array from response
311
- const inlineCommentsArray = yield* Effect.tryPromise({
312
- try: () => Promise.resolve(JSON.parse(extractedInlineResponse)),
313
- catch: (error) => new Error(`Invalid JSON response: ${error}`),
314
- }).pipe(
315
- Effect.catchAll((error) =>
316
- Effect.gen(function* () {
317
- yield* Console.error(`✗ Failed to parse inline comments JSON: ${error}`)
318
- yield* Console.error(`Raw extracted response: "${extractedInlineResponse}"`)
319
- if (!options.debug) {
320
- yield* Console.error('Run with --debug to see full AI output')
321
- }
322
- return yield* Effect.fail(error)
323
- }),
324
- ),
325
- )
326
-
327
- // Validate that the response is an array
328
- if (!Array.isArray(inlineCommentsArray)) {
329
- yield* Console.error('✗ AI response is not an array of comments')
330
- return yield* Effect.fail(new Error('Invalid inline comments format'))
331
- }
332
-
333
- // Validate and fix inline comments
334
- const originalCount = inlineCommentsArray.length
335
- const inlineComments = yield* validateAndFixInlineComments(
336
- inlineCommentsArray,
337
- changedFiles,
338
- )
339
- const validCount = inlineComments.length
340
-
341
- if (originalCount > validCount) {
342
- yield* Console.log(
343
- `→ Filtered ${originalCount - validCount} invalid comments, ${validCount} remain`,
344
- )
345
- }
346
-
347
- // Handle inline comments output/posting
348
- yield* handleInlineComments(inlineComments, changeId, options)
349
-
350
- // Stage 2: Generate overall review comment
351
- yield* Console.log(`→ Generating overall review comment for change ${changeId}...`)
352
-
353
- const overallPrompt = yield* buildEnhancedPrompt(
354
- userReviewPrompt,
355
- prompts.overallReviewSystemPrompt,
356
- changeId,
357
- changedFiles,
358
- )
359
-
360
- // Run overall review using selected strategy
361
- if (options.debug) {
362
- yield* Console.log(`[DEBUG] Running overall review with ${selectedStrategy.name}`)
363
- }
364
-
365
- const overallResponse = yield* reviewStrategy
366
- .executeWithStrategy(selectedStrategy, overallPrompt, {
367
- cwd: worktreeInfo.path,
368
- systemPrompt: options.systemPrompt || prompts.overallReviewSystemPrompt,
369
- })
370
- .pipe(
371
- Effect.catchTag('ReviewStrategyError', (error) =>
372
- Effect.gen(function* () {
373
- yield* Console.error(`✗ Overall review failed: ${error.message}`)
374
- return yield* Effect.fail(new Error(error.message))
375
- }),
376
- ),
377
- )
378
-
379
- if (options.debug) {
380
- yield* Console.log(`[DEBUG] Overall review completed`)
381
- yield* Console.log(`[DEBUG] Response length: ${overallResponse.length} chars`)
382
- }
383
-
384
- // Response content is ready for use
385
- const extractedOverallResponse = overallResponse.trim()
386
-
387
- // Handle overall review output/posting
388
- yield* handleOverallReview(extractedOverallResponse, changeId, options)
389
- } finally {
390
- // Always restore original working directory
391
- process.chdir(originalCwd)
392
- }
393
-
394
- yield* Console.log(`✓ Review complete for ${changeId}`)
395
- }),
396
-
397
- // Release: Always cleanup worktree
398
- (worktreeInfo) => gitService.cleanup(worktreeInfo),
399
- )
400
- })
401
-
402
- // Helper function to handle inline comments output/posting
403
- const handleInlineComments = (
404
- inlineComments: InlineComment[],
405
- changeId: string,
406
- options: ReviewOptions,
407
- ): Effect.Effect<void, Error, GerritApiService> =>
408
- Effect.gen(function* () {
409
- if (!options.comment) {
410
- // Display mode
411
- if (inlineComments.length > 0) {
412
- yield* Console.log('\n━━━━━━ INLINE COMMENTS ━━━━━━')
413
- for (const comment of inlineComments) {
414
- yield* Console.log(`\n📍 ${comment.file}${comment.line ? `:${comment.line}` : ''}`)
415
- yield* Console.log(comment.message)
416
- }
417
- } else {
418
- yield* Console.log('\n→ No inline comments')
419
- }
420
- } else {
421
- // Comment posting mode
422
- if (inlineComments.length > 0) {
423
- yield* Console.log('\n━━━━━━ INLINE COMMENTS TO POST ━━━━━━')
424
- for (const comment of inlineComments) {
425
- yield* Console.log(`\n📍 ${comment.file}${comment.line ? `:${comment.line}` : ''}`)
426
- yield* Console.log(comment.message)
427
- }
428
- yield* Console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
429
-
430
- const shouldPost =
431
- options.yes || (yield* promptUser('\nPost these inline comments to Gerrit?'))
432
-
433
- if (shouldPost) {
434
- yield* pipe(
435
- commentCommandWithInput(changeId, JSON.stringify(inlineComments), { batch: true }),
436
- Effect.catchAll((error) =>
437
- Effect.gen(function* () {
438
- yield* Console.error(`✗ Failed to post inline comments: ${error}`)
439
- return yield* Effect.fail(error)
440
- }),
441
- ),
442
- )
443
- yield* Console.log(`✓ Inline comments posted for ${changeId}`)
444
- } else {
445
- yield* Console.log('→ Inline comments not posted')
446
- }
447
- }
448
- }
449
- })
450
-
451
- // Helper function to handle overall review output/posting
452
- const handleOverallReview = (
453
- overallResponse: string,
454
- changeId: string,
455
- options: ReviewOptions,
456
- ): Effect.Effect<void, Error, GerritApiService> =>
457
- Effect.gen(function* () {
458
- if (!options.comment) {
459
- // Display mode
460
- yield* Console.log('\n━━━━━━ OVERALL REVIEW ━━━━━━')
461
- yield* Console.log(overallResponse)
462
- yield* Console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
463
- } else {
464
- // Comment posting mode
465
- yield* Console.log('\n━━━━━━ OVERALL REVIEW TO POST ━━━━━━')
466
- yield* Console.log(overallResponse)
467
- yield* Console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
468
-
469
- const shouldPost = options.yes || (yield* promptUser('\nPost this overall review to Gerrit?'))
470
-
471
- if (shouldPost) {
472
- yield* pipe(
473
- commentCommandWithInput(changeId, overallResponse, {}),
474
- Effect.catchAll((error) =>
475
- Effect.gen(function* () {
476
- yield* Console.error(`✗ Failed to post review comment: ${error}`)
477
- return yield* Effect.fail(error)
478
- }),
479
- ),
480
- )
481
- yield* Console.log(`✓ Overall review posted for ${changeId}`)
482
- } else {
483
- yield* Console.log('→ Overall review not posted')
484
- }
485
- }
486
- })
@@ -1,86 +0,0 @@
1
- # Engineering Code Review - Signal Over Noise
2
-
3
- You are conducting a technical code review for experienced engineers. **PRIORITY: Find actual problems, not generate busy work.**
4
-
5
- ## Core Principles
6
-
7
- **SIGNAL > NOISE**: Only comment on issues that materially impact correctness, security, performance, or maintainability. Silence is better than noise.
8
-
9
- **NO PRAISE NEEDED**: Don't compliment good code. Engineers expect competent code by default.
10
-
11
- **EMPTY RESPONSES ARE VALID**: Small changes without issues should result in empty inline comments. The overall review can simply note "No significant issues found."
12
-
13
- **FOCUS ON REAL PROBLEMS**:
14
- - Bugs that will cause runtime failures
15
- - Security vulnerabilities
16
- - Performance bottlenecks
17
- - Architectural mistakes
18
- - Missing error handling
19
-
20
- **ASSUME COMPETENCE**: The author is an experienced engineer who made intentional decisions. Question only when you see genuine problems.
21
-
22
- ## Review Categories (Priority Order)
23
-
24
- ### 1. CRITICAL ISSUES (Must Fix)
25
- - **Correctness**: Logic errors, race conditions, data corruption risks
26
- - **Security**: Authentication bypasses, injection vulnerabilities, data exposure
27
- - **Data Loss**: Operations that could destroy or corrupt user data
28
- - **Breaking Changes**: Incompatible API/schema changes without migration
29
- - **Production Impact**: Issues that would cause outages or severe degradation
30
-
31
- ### 2. SIGNIFICANT CONCERNS (Should Fix)
32
- - **Performance**: Memory leaks, N+1 queries, inefficient algorithms
33
- - **Error Handling**: Missing error cases, silent failures, poor recovery
34
- - **Resource Management**: Unclosed connections, file handles, cleanup issues
35
- - **Type Safety**: Unsafe casts, missing validation, schema mismatches
36
- - **Concurrency**: Deadlock risks, thread safety issues, synchronization problems
37
-
38
- ### 3. CODE QUALITY (Consider Fixing)
39
- - **Architecture**: Design pattern violations, coupling issues, abstraction leaks
40
- - **Maintainability**: Complex logic without justification, unclear naming
41
- - **Testing**: Missing test coverage for critical paths, brittle test design
42
- - **Documentation**: Misleading comments, missing API documentation
43
- - **Best Practices**: Framework misuse, anti-patterns, deprecated APIs
44
-
45
- ### 4. MINOR IMPROVEMENTS (Optional)
46
- - **Consistency**: Deviations from established patterns without reason
47
- - **Efficiency**: Minor optimization opportunities
48
- - **Clarity**: Code that works but could be more readable
49
- - **Future-Proofing**: Anticipating likely future requirements
50
-
51
- ## What NOT to Review (Common Time Wasters)
52
-
53
- - **Code style/formatting**: Handled by automated tools
54
- - **Personal preferences**: Different != wrong
55
- - **Compliments**: "Looks good!" wastes everyone's time
56
- - **Nitpicks**: Minor wording, variable names, spacing
57
- - **Micro-optimizations**: Unless there's a proven performance problem
58
- - **Already working code**: If it works and isn't broken, don't fix it
59
- - **Suggestions for "better" approaches**: Only if current approach has concrete problems
60
-
61
- ## Before Commenting, Ask Yourself
62
-
63
- 1. **Will this cause a runtime failure?** → Critical issue, comment required
64
- 2. **Will this create a security vulnerability?** → Critical issue, comment required
65
- 3. **Will this significantly harm performance?** → Important issue, comment required
66
- 4. **Will this make the code unmaintainable?** → Consider commenting
67
- 5. **Is this just a different way to solve the same problem?** → Skip it
68
-
69
- ## Output Guidelines
70
-
71
- **INLINE COMMENTS**: Only for specific line-level issues. Empty array is perfectly valid.
72
- - Start with "🤖 "
73
- - Be direct: "This will cause X bug" not "Consider maybe perhaps changing this"
74
- - Provide specific fixes when possible
75
-
76
- **OVERALL REVIEW**: Required even if no inline comments.
77
- - For clean code: "No significant issues found. Change is ready."
78
- - For problematic code: Focus on the most important issues only
79
- - Skip the pleasantries, get to the point
80
-
81
- ## Success Metrics
82
-
83
- - **Good review**: Finds 1-3 real issues that would cause problems
84
- - **Great review**: Catches a critical bug before production
85
- - **Bad review**: 10+ nitpicky comments about style preferences
86
- - **Terrible review**: "Great job! LGTM!" with zero value added