commit-analyzer 1.0.2 → 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.
Files changed (62) hide show
  1. package/.claude/settings.local.json +11 -1
  2. package/README.md +33 -2
  3. package/commits.csv +2 -0
  4. package/eslint.config.mts +45 -0
  5. package/package.json +17 -9
  6. package/src/1.domain/analysis.ts +93 -0
  7. package/src/1.domain/analyzed-commit.ts +97 -0
  8. package/src/1.domain/application-error.ts +32 -0
  9. package/src/1.domain/category.ts +52 -0
  10. package/src/1.domain/commit-analysis-service.ts +92 -0
  11. package/src/1.domain/commit-hash.ts +40 -0
  12. package/src/1.domain/commit.ts +99 -0
  13. package/src/1.domain/date-formatting-service.ts +81 -0
  14. package/src/1.domain/date-range.ts +76 -0
  15. package/src/1.domain/report-generation-service.ts +292 -0
  16. package/src/2.application/analyze-commits.usecase.ts +307 -0
  17. package/src/2.application/generate-report.usecase.ts +204 -0
  18. package/src/2.application/llm-service.ts +54 -0
  19. package/src/2.application/resume-analysis.usecase.ts +123 -0
  20. package/src/3.presentation/analysis-repository.interface.ts +27 -0
  21. package/src/3.presentation/analyze-command.ts +128 -0
  22. package/src/3.presentation/cli-application.ts +255 -0
  23. package/src/3.presentation/command-handler.interface.ts +4 -0
  24. package/src/3.presentation/commit-analysis-controller.ts +101 -0
  25. package/src/3.presentation/commit-repository.interface.ts +47 -0
  26. package/src/3.presentation/console-formatter.ts +129 -0
  27. package/src/3.presentation/progress-repository.interface.ts +49 -0
  28. package/src/3.presentation/report-command.ts +50 -0
  29. package/src/3.presentation/resume-command.ts +59 -0
  30. package/src/3.presentation/storage-repository.interface.ts +33 -0
  31. package/src/3.presentation/storage-service.interface.ts +32 -0
  32. package/src/3.presentation/version-control-service.interface.ts +41 -0
  33. package/src/4.infrastructure/cache-service.ts +271 -0
  34. package/src/4.infrastructure/cached-analysis-repository.ts +46 -0
  35. package/src/4.infrastructure/claude-llm-adapter.ts +124 -0
  36. package/src/4.infrastructure/csv-service.ts +206 -0
  37. package/src/4.infrastructure/file-storage-repository.ts +108 -0
  38. package/src/4.infrastructure/file-system-storage-adapter.ts +87 -0
  39. package/src/4.infrastructure/gemini-llm-adapter.ts +46 -0
  40. package/src/4.infrastructure/git-adapter.ts +116 -0
  41. package/src/4.infrastructure/git-commit-repository.ts +85 -0
  42. package/src/4.infrastructure/json-progress-tracker.ts +182 -0
  43. package/src/4.infrastructure/llm-adapter-factory.ts +26 -0
  44. package/src/4.infrastructure/llm-adapter.ts +455 -0
  45. package/src/4.infrastructure/llm-analysis-repository.ts +38 -0
  46. package/src/4.infrastructure/openai-llm-adapter.ts +57 -0
  47. package/src/di.ts +108 -0
  48. package/src/main.ts +63 -0
  49. package/src/utils/app-paths.ts +36 -0
  50. package/src/utils/concurrency.ts +81 -0
  51. package/src/utils.ts +77 -0
  52. package/tsconfig.json +7 -1
  53. package/src/cli.ts +0 -170
  54. package/src/csv-reader.ts +0 -180
  55. package/src/csv.ts +0 -40
  56. package/src/errors.ts +0 -49
  57. package/src/git.ts +0 -112
  58. package/src/index.ts +0 -395
  59. package/src/llm.ts +0 -396
  60. package/src/progress.ts +0 -84
  61. package/src/report-generator.ts +0 -286
  62. package/src/types.ts +0 -24
@@ -0,0 +1,307 @@
1
+ import { AnalyzedCommit } from "@domain/analyzed-commit"
2
+ import { CommitAnalysisService } from "@domain/commit-analysis-service"
3
+ import { CommitHash } from "@domain/commit-hash"
4
+
5
+ import { ICommandHandler } from "@presentation/command-handler.interface"
6
+ import { ConsoleFormatter } from "@presentation/console-formatter"
7
+ import { IProgressRepository } from "@presentation/progress-repository.interface"
8
+ import { IStorageRepository } from "@presentation/storage-repository.interface"
9
+
10
+ import { ConcurrencyManager } from "../utils/concurrency"
11
+
12
+ export interface AnalyzeCommitsCommand {
13
+ commitHashes: string[]
14
+ outputFile: string
15
+ verbose?: boolean
16
+ saveProgressInterval?: number
17
+ batchSize?: number
18
+ }
19
+
20
+ export interface AnalyzeCommitsResult {
21
+ analyzedCommits: AnalyzedCommit[]
22
+ failedCommits: number
23
+ totalProcessed: number
24
+ }
25
+
26
+ export class AnalyzeCommitsUseCase
27
+ implements ICommandHandler<AnalyzeCommitsCommand, AnalyzeCommitsResult>
28
+ {
29
+ private static readonly DEFAULT_PROGRESS_INTERVAL = 10
30
+ private static readonly DEFAULT_BATCH_SIZE = 1
31
+ private static readonly DEFAULT_MAX_CONCURRENCY = 3
32
+
33
+ constructor(
34
+ private readonly commitAnalysisService: CommitAnalysisService,
35
+ private readonly progressRepository: IProgressRepository,
36
+ private readonly storageRepository: IStorageRepository,
37
+ ) {}
38
+
39
+ async handle(command: AnalyzeCommitsCommand): Promise<AnalyzeCommitsResult> {
40
+ const {
41
+ commitHashes,
42
+ outputFile,
43
+ verbose = false,
44
+ saveProgressInterval = AnalyzeCommitsUseCase.DEFAULT_PROGRESS_INTERVAL,
45
+ batchSize = AnalyzeCommitsUseCase.DEFAULT_BATCH_SIZE,
46
+ } = command
47
+
48
+ if (commitHashes.length === 0) {
49
+ throw new Error("No commits provided for analysis")
50
+ }
51
+
52
+ // Convert string hashes to domain objects
53
+ const hashes = commitHashes.map((hash) => CommitHash.create(hash))
54
+
55
+ // Validate commits exist
56
+ const { valid, invalid } =
57
+ await this.commitAnalysisService.validateCommits(hashes)
58
+
59
+ if (invalid.length > 0) {
60
+ ConsoleFormatter.logWarning(
61
+ `Warning: ${invalid.length} invalid commit hashes found`,
62
+ )
63
+ for (const invalidHash of invalid) {
64
+ ConsoleFormatter.logWarning(` - ${invalidHash.getShortHash()}`)
65
+ }
66
+ }
67
+
68
+ if (valid.length === 0) {
69
+ throw new Error("No valid commits found for analysis")
70
+ }
71
+
72
+ const analyzedCommits: AnalyzedCommit[] = []
73
+ const processedCommits: CommitHash[] = []
74
+ let failedCommits = 0
75
+
76
+ ConsoleFormatter.logInfo(`Analyzing ${valid.length} commits...`)
77
+
78
+ if (batchSize === 1) {
79
+ // Sequential processing for batch size 1
80
+ for (let i = 0; i < valid.length; i++) {
81
+ const hash = valid[i]
82
+ const currentIndex = i + 1
83
+
84
+ if (verbose) {
85
+ ConsoleFormatter.logInfo(
86
+ `[${currentIndex}/${valid.length}] Processing: ${hash.getShortHash()}`,
87
+ )
88
+ } else {
89
+ ConsoleFormatter.updateProgress(currentIndex, valid.length, `Processing commits`)
90
+ }
91
+
92
+ try {
93
+ const analyzedCommit =
94
+ await this.commitAnalysisService.analyzeCommit(hash)
95
+ analyzedCommits.push(analyzedCommit)
96
+ processedCommits.push(hash)
97
+
98
+ const analysis = analyzedCommit.getAnalysis()
99
+
100
+ if (verbose) {
101
+ ConsoleFormatter.logSuccess(
102
+ `✓ [${currentIndex}/${valid.length}] Analyzed as "${analysis.getCategory().getValue()}": ${analysis.getSummary()}`,
103
+ )
104
+ }
105
+
106
+ // Save progress periodically
107
+ if (
108
+ currentIndex % saveProgressInterval === 0 ||
109
+ currentIndex === valid.length
110
+ ) {
111
+ await this.saveProgress(
112
+ hashes,
113
+ processedCommits,
114
+ analyzedCommits,
115
+ outputFile,
116
+ )
117
+ if (verbose) {
118
+ ConsoleFormatter.logInfo(
119
+ `Progress saved (${currentIndex}/${valid.length})`,
120
+ )
121
+ }
122
+ }
123
+ } catch (error) {
124
+ const errorMessage =
125
+ error instanceof Error ? error.message : "Unknown error"
126
+
127
+ if (verbose) {
128
+ ConsoleFormatter.logError(`❌ [${currentIndex}/${valid.length}] Failed: ${errorMessage}`)
129
+ }
130
+
131
+ failedCommits++
132
+ processedCommits.push(hash)
133
+
134
+ // Save progress on failure
135
+ await this.saveProgress(
136
+ hashes,
137
+ processedCommits,
138
+ analyzedCommits,
139
+ outputFile,
140
+ )
141
+
142
+ if (verbose) {
143
+ ConsoleFormatter.logError(` Detailed error: ${errorMessage}`)
144
+ }
145
+ }
146
+ }
147
+
148
+ if (!verbose) {
149
+ ConsoleFormatter.completeProgress()
150
+ }
151
+ } else {
152
+ // Batch processing for batch size > 1
153
+ const results = await this.processBatches(
154
+ valid,
155
+ batchSize,
156
+ saveProgressInterval,
157
+ hashes,
158
+ outputFile,
159
+ verbose
160
+ )
161
+
162
+ analyzedCommits.push(...results.analyzedCommits)
163
+ processedCommits.push(...results.processedCommits)
164
+ failedCommits = results.failedCommits
165
+ }
166
+
167
+ // Export results
168
+ if (analyzedCommits.length > 0) {
169
+ await this.storageRepository.exportToCSV(analyzedCommits, outputFile)
170
+ }
171
+
172
+ return {
173
+ analyzedCommits,
174
+ failedCommits,
175
+ totalProcessed: processedCommits.length,
176
+ }
177
+ }
178
+
179
+ private async saveProgress(
180
+ totalCommits: CommitHash[],
181
+ processedCommits: CommitHash[],
182
+ analyzedCommits: AnalyzedCommit[],
183
+ outputFile: string,
184
+ ): Promise<void> {
185
+ const progressState = {
186
+ totalCommits,
187
+ processedCommits,
188
+ analyzedCommits,
189
+ lastProcessedIndex: processedCommits.length - 1,
190
+ startTime: new Date(),
191
+ outputFile,
192
+ }
193
+
194
+ await this.progressRepository.saveProgress(progressState)
195
+ }
196
+
197
+ private async processBatches(
198
+ commitHashes: CommitHash[],
199
+ batchSize: number,
200
+ saveProgressInterval: number,
201
+ allHashes: CommitHash[],
202
+ outputFile: string,
203
+ verbose: boolean
204
+ ): Promise<{
205
+ analyzedCommits: AnalyzedCommit[]
206
+ processedCommits: CommitHash[]
207
+ failedCommits: number
208
+ }> {
209
+ const analyzedCommits: AnalyzedCommit[] = []
210
+ const processedCommits: CommitHash[] = []
211
+ let failedCommits = 0
212
+
213
+ // Process commits in batches
214
+ for (let i = 0; i < commitHashes.length; i += batchSize) {
215
+ const batch = commitHashes.slice(i, i + batchSize)
216
+ const batchStart = i + 1
217
+ const batchEnd = Math.min(i + batchSize, commitHashes.length)
218
+
219
+ if (verbose) {
220
+ ConsoleFormatter.logInfo(
221
+ `Processing batch ${batchStart}-${batchEnd}/${commitHashes.length} (${batch.length} commits)`,
222
+ )
223
+ } else {
224
+ ConsoleFormatter.updateProgress(batchStart, commitHashes.length, `Processing batch ${batchStart}-${batchEnd}`)
225
+ }
226
+
227
+ // Process all commits in this batch with controlled concurrency
228
+ const concurrencyManager = new ConcurrencyManager(AnalyzeCommitsUseCase.DEFAULT_MAX_CONCURRENCY)
229
+
230
+ const batchPromises = batch.map(async (hash, index) => {
231
+ const globalIndex = i + index + 1
232
+
233
+ return concurrencyManager.execute(async () => {
234
+ try {
235
+ if (verbose) {
236
+ ConsoleFormatter.logInfo(
237
+ ` [${globalIndex}/${commitHashes.length}] Processing: ${hash.getShortHash()}`,
238
+ )
239
+ }
240
+
241
+ const analyzedCommit = await this.commitAnalysisService.analyzeCommit(hash)
242
+ const analysis = analyzedCommit.getAnalysis()
243
+
244
+ if (verbose) {
245
+ ConsoleFormatter.logSuccess(
246
+ `✓ [${globalIndex}/${commitHashes.length}] Analyzed as "${analysis.getCategory().getValue()}": ${analysis.getSummary()}`,
247
+ )
248
+ }
249
+
250
+ return { success: true, commit: analyzedCommit, hash }
251
+ } catch (error) {
252
+ const errorMessage = error instanceof Error ? error.message : "Unknown error"
253
+
254
+ if (verbose) {
255
+ ConsoleFormatter.logError(`❌ [${globalIndex}/${commitHashes.length}] Failed: ${errorMessage}`)
256
+ ConsoleFormatter.logError(` Detailed error: ${errorMessage}`)
257
+ }
258
+
259
+ return { success: false, hash, error: errorMessage }
260
+ }
261
+ })
262
+ })
263
+
264
+ // Wait for all commits in this batch to complete
265
+ const batchResults = await Promise.all(batchPromises)
266
+
267
+ // Process results
268
+ for (const result of batchResults) {
269
+ processedCommits.push(result.hash)
270
+
271
+ if (result.success && 'commit' in result && result.commit) {
272
+ analyzedCommits.push(result.commit)
273
+ } else {
274
+ failedCommits++
275
+ }
276
+ }
277
+
278
+ // Save progress periodically or after each batch
279
+ if (
280
+ (i + batchSize) % (saveProgressInterval * batchSize) === 0 ||
281
+ (i + batchSize) >= commitHashes.length
282
+ ) {
283
+ await this.saveProgress(
284
+ allHashes,
285
+ processedCommits,
286
+ analyzedCommits,
287
+ outputFile,
288
+ )
289
+ if (verbose) {
290
+ ConsoleFormatter.logInfo(
291
+ `Progress saved (${processedCommits.length}/${commitHashes.length})`,
292
+ )
293
+ }
294
+ }
295
+ }
296
+
297
+ if (!verbose) {
298
+ ConsoleFormatter.completeProgress()
299
+ }
300
+
301
+ return {
302
+ analyzedCommits,
303
+ processedCommits,
304
+ failedCommits
305
+ }
306
+ }
307
+ }
@@ -0,0 +1,204 @@
1
+ import { AnalyzedCommit } from "@domain/analyzed-commit"
2
+ import { DateFormattingService } from "@domain/date-formatting-service"
3
+ import {
4
+ CommitStatistics,
5
+ ReportGenerationService,
6
+ } from "@domain/report-generation-service"
7
+
8
+ import { ICommandHandler } from "@presentation/command-handler.interface"
9
+ import { IStorageRepository } from "@presentation/storage-repository.interface"
10
+
11
+ import { ILLMService } from "./llm-service"
12
+
13
+ export interface GenerateReportCommand {
14
+ inputCsvPath?: string
15
+ analyzedCommits?: AnalyzedCommit[]
16
+ outputPath: string
17
+ includeStatistics?: boolean
18
+ sourceInfo?: {
19
+ type: 'author' | 'commits' | 'csv'
20
+ value: string // email address, commit hashes, or csv file path
21
+ }
22
+ }
23
+
24
+ export interface GenerateReportResult {
25
+ reportPath: string
26
+ statistics: CommitStatistics
27
+ commitsProcessed: number
28
+ }
29
+
30
+ export class GenerateReportUseCase
31
+ implements ICommandHandler<GenerateReportCommand, GenerateReportResult>
32
+ {
33
+ constructor(
34
+ private readonly reportGenerationService: ReportGenerationService,
35
+ private readonly storageRepository: IStorageRepository,
36
+ private readonly llmService: ILLMService,
37
+ private readonly dateFormattingService: DateFormattingService,
38
+ ) {}
39
+
40
+ async handle(command: GenerateReportCommand): Promise<GenerateReportResult> {
41
+ const {
42
+ inputCsvPath,
43
+ analyzedCommits,
44
+ outputPath,
45
+ includeStatistics = true,
46
+ } = command
47
+
48
+ let commits: AnalyzedCommit[]
49
+
50
+ // Get commits from either CSV file or provided commits
51
+ if (inputCsvPath) {
52
+ console.log(`Reading CSV data from ${inputCsvPath}...`)
53
+ commits = await this.storageRepository.importFromCSV(inputCsvPath)
54
+ } else if (analyzedCommits) {
55
+ commits = analyzedCommits
56
+ } else {
57
+ throw new Error("Either inputCsvPath or analyzedCommits must be provided")
58
+ }
59
+
60
+ if (commits.length === 0) {
61
+ throw new Error("No commits found for report generation")
62
+ }
63
+
64
+ // Generate statistics
65
+ const statistics = this.reportGenerationService.generateStatistics(commits)
66
+
67
+ // Format date range based on commit span
68
+ const dateRange = this.dateFormattingService.formatDateRange(commits)
69
+
70
+ console.log(
71
+ `Found ${statistics.totalCommits} commits spanning ${dateRange}`,
72
+ )
73
+ console.log(
74
+ `Categories: ${statistics.categoryBreakdown.feature} features, ${statistics.categoryBreakdown.process} process, ${statistics.categoryBreakdown.tweak} tweaks`,
75
+ )
76
+
77
+ // Generate and save the report
78
+ console.log("Generating condensed report...")
79
+ await this.generateMarkdownReportWithLLM({
80
+ commits,
81
+ statistics,
82
+ outputPath,
83
+ includeStatistics,
84
+ sourceInfo: command.sourceInfo,
85
+ })
86
+
87
+ return {
88
+ reportPath: outputPath,
89
+ statistics,
90
+ commitsProcessed: commits.length,
91
+ }
92
+ }
93
+
94
+ private async generateMarkdownReportWithLLM(params: {
95
+ commits: AnalyzedCommit[]
96
+ statistics: CommitStatistics
97
+ outputPath: string
98
+ includeStatistics: boolean
99
+ sourceInfo?: { type: 'author' | 'commits' | 'csv'; value: string }
100
+ }): Promise<void> {
101
+ const { commits, statistics, outputPath, includeStatistics, sourceInfo } = params
102
+ let reportContent = "# Development Summary Report\n\n"
103
+
104
+ if (includeStatistics) {
105
+ reportContent += this.generateAnalysisSection(commits, statistics, sourceInfo)
106
+ reportContent += "\n\n"
107
+ }
108
+
109
+ // Generate sophisticated yearly summaries using LLM
110
+ reportContent += await this.generateYearlySummariesWithLLM(commits)
111
+
112
+ // Write the final report using the storage repository
113
+ await this.storageRepository.writeFile(outputPath, reportContent)
114
+ }
115
+
116
+ /**
117
+ * Generate commit analysis section with accurate counts (like the original)
118
+ */
119
+ private generateAnalysisSection(
120
+ commits: AnalyzedCommit[],
121
+ statistics: CommitStatistics,
122
+ sourceInfo?: { type: 'author' | 'commits' | 'csv'; value: string }
123
+ ): string {
124
+ // Get formatted date range using the date formatting service
125
+ const dateRange = this.dateFormattingService.formatDateRange(commits)
126
+ // Group data by year for detailed breakdown
127
+ const yearlyStats = commits.reduce(
128
+ (acc, commit) => {
129
+ const year = commit.getYear()
130
+ const category = commit.getAnalysis().getCategory().getValue()
131
+
132
+ if (!acc[year]) {
133
+ acc[year] = { tweak: 0, feature: 0, process: 0, total: 0 }
134
+ }
135
+ acc[year][category]++
136
+ acc[year].total++
137
+ return acc
138
+ },
139
+ {} as Record<
140
+ number,
141
+ { tweak: number; feature: number; process: number; total: number }
142
+ >,
143
+ )
144
+
145
+ // Sort years in descending order
146
+ const sortedYears = Object.keys(yearlyStats)
147
+ .map(Number)
148
+ .sort((a, b) => b - a)
149
+
150
+ let analysisContent = `## Commit Analysis\n`
151
+ analysisContent += `- **Total Commits**: ${statistics.totalCommits} commits spanning ${dateRange}\n`
152
+
153
+ // Add source information
154
+ if (sourceInfo) {
155
+ switch (sourceInfo.type) {
156
+ case 'author':
157
+ analysisContent += `- **Source**: Commits by author ${sourceInfo.value}\n`
158
+ break
159
+ case 'commits': {
160
+ const commitCount = sourceInfo.value.split(',').length
161
+ analysisContent += `- **Source**: ${commitCount} specific commit${commitCount > 1 ? 's' : ''}\n`
162
+ break
163
+ }
164
+ case 'csv':
165
+ analysisContent += `- **Source**: Imported from ${sourceInfo.value}\n`
166
+ break
167
+ }
168
+ }
169
+
170
+ // Add year-by-year breakdown
171
+ for (const year of sortedYears) {
172
+ const yearData = yearlyStats[year]
173
+ analysisContent += `- **${year}**: ${yearData.total} commits (${yearData.feature} features, ${yearData.process} process, ${yearData.tweak} tweaks)\n`
174
+ }
175
+
176
+ return analysisContent
177
+ }
178
+
179
+ /**
180
+ * Generate sophisticated time-period-based summaries using LLM service
181
+ */
182
+ private async generateYearlySummariesWithLLM(
183
+ commits: AnalyzedCommit[],
184
+ ): Promise<string> {
185
+ // Determine the appropriate time period for the report
186
+ const timePeriod = this.reportGenerationService.determineTimePeriod(commits)
187
+
188
+ // Group commits by the determined time period
189
+ const groupedCommits = this.reportGenerationService.groupByTimePeriod(commits, timePeriod)
190
+
191
+ // Convert grouped commits to CSV format for LLM consumption
192
+ const csvContent = this.reportGenerationService.convertGroupedToCSV(groupedCommits, timePeriod)
193
+
194
+ try {
195
+ // Use the LLM service to generate sophisticated time-period-based summaries
196
+ const summaryContent = await this.llmService.generateTimePeriodSummariesFromCSV(csvContent, timePeriod)
197
+ return summaryContent
198
+ } catch (error) {
199
+ throw new Error(
200
+ `Failed to generate ${timePeriod} summaries: ${error instanceof Error ? error.message : "Unknown error"}`,
201
+ )
202
+ }
203
+ }
204
+ }
@@ -0,0 +1,54 @@
1
+ import { CategoryType } from "@domain/category"
2
+
3
+ /**
4
+ * Output port for LLM analysis operations
5
+ */
6
+ export interface ILLMService {
7
+ /**
8
+ * Detects available LLM models on the system
9
+ */
10
+ detectAvailableModels(): Promise<string[]>
11
+
12
+ /**
13
+ * Sets the LLM model to use
14
+ */
15
+ setModel(model: string): void
16
+
17
+ /**
18
+ * Sets verbose mode for detailed logging
19
+ */
20
+ setVerbose(verbose: boolean): void
21
+
22
+ /**
23
+ * Analyzes commit content and returns structured analysis
24
+ */
25
+ analyzeCommit(
26
+ message: string,
27
+ diff: string,
28
+ ): Promise<{
29
+ category: CategoryType
30
+ summary: string
31
+ description: string
32
+ }>
33
+
34
+ /**
35
+ * Gets the maximum number of retry attempts
36
+ */
37
+ getMaxRetries(): number
38
+
39
+ /**
40
+ * Generates sophisticated yearly summaries from CSV data using LLM
41
+ * with consolidation, categorization, and stakeholder-friendly language
42
+ */
43
+ generateYearlySummariesFromCSV(csvContent: string): Promise<string>
44
+
45
+ /**
46
+ * Generates time-period-based summaries from CSV data using LLM
47
+ */
48
+ generateTimePeriodSummariesFromCSV(csvContent: string, period: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'): Promise<string>
49
+
50
+ /**
51
+ * Checks if the service is available and configured
52
+ */
53
+ isAvailable(): Promise<boolean>
54
+ }
@@ -0,0 +1,123 @@
1
+ import { ICommandHandler } from "@presentation/command-handler.interface"
2
+ import { ConsoleFormatter } from "@presentation/console-formatter"
3
+ import { IProgressRepository } from "@presentation/progress-repository.interface"
4
+
5
+ import { createPromiseReadline } from "../utils"
6
+
7
+ import {
8
+ AnalyzeCommitsResult,
9
+ AnalyzeCommitsUseCase,
10
+ } from "./analyze-commits.usecase"
11
+
12
+ export interface ResumeAnalysisCommand {
13
+ verbose?: boolean
14
+ }
15
+
16
+ export class ResumeAnalysisUseCase
17
+ implements ICommandHandler<ResumeAnalysisCommand, AnalyzeCommitsResult | null>
18
+ {
19
+ constructor(
20
+ private readonly progressRepository: IProgressRepository,
21
+ private readonly analyzeCommitsUseCase: AnalyzeCommitsUseCase,
22
+ ) {}
23
+
24
+ async handle(
25
+ command: ResumeAnalysisCommand,
26
+ ): Promise<AnalyzeCommitsResult | null> {
27
+ const { verbose = false } = command
28
+
29
+ // Check if there's saved progress
30
+ const hasProgress = await this.progressRepository.hasProgress()
31
+ if (!hasProgress) {
32
+ ConsoleFormatter.logInfo(
33
+ "No previous checkpoint found. Starting fresh...",
34
+ )
35
+ return null
36
+ }
37
+
38
+ // Load progress state
39
+ const progressState = await this.progressRepository.loadProgress()
40
+ if (!progressState) {
41
+ ConsoleFormatter.logError("Failed to load progress state.")
42
+ return null
43
+ }
44
+
45
+ ConsoleFormatter.logInfo("Found previous session checkpoint")
46
+ ConsoleFormatter.logInfo(
47
+ this.progressRepository.formatProgressSummary(progressState),
48
+ )
49
+
50
+ // Ask user if they want to resume
51
+ const shouldResume = await this.promptUserForResume()
52
+ if (!shouldResume) {
53
+ await this.progressRepository.clearProgress()
54
+ ConsoleFormatter.logInfo("Starting fresh analysis...")
55
+ return null
56
+ }
57
+
58
+ // Get remaining commits
59
+ const remainingCommits =
60
+ this.progressRepository.getRemainingCommits(progressState)
61
+
62
+ if (remainingCommits.length === 0) {
63
+ ConsoleFormatter.logSuccess("All commits have already been processed!")
64
+ await this.progressRepository.clearProgress()
65
+ return {
66
+ analyzedCommits: progressState.analyzedCommits,
67
+ failedCommits: 0,
68
+ totalProcessed: progressState.processedCommits.length,
69
+ }
70
+ }
71
+
72
+ ConsoleFormatter.logInfo(
73
+ `Resuming with ${remainingCommits.length} remaining commits...`,
74
+ )
75
+ ConsoleFormatter.logInfo(
76
+ `Previous progress: ${progressState.processedCommits.length}/${progressState.totalCommits.length} commits processed`,
77
+ )
78
+
79
+ if (verbose) {
80
+ ConsoleFormatter.logDebug(
81
+ `analyzedCommits.length = ${progressState.analyzedCommits.length}`,
82
+ )
83
+ ConsoleFormatter.logDebug(
84
+ `processedCommits.length = ${progressState.processedCommits.length}`,
85
+ )
86
+ ConsoleFormatter.logDebug(
87
+ `remainingCommits.length = ${remainingCommits.length}`,
88
+ )
89
+ }
90
+
91
+ // Continue analysis with remaining commits
92
+ const remainingHashStrings = remainingCommits.map((hash) => hash.getValue())
93
+ const result = await this.analyzeCommitsUseCase.handle({
94
+ commitHashes: remainingHashStrings,
95
+ outputFile: progressState.outputFile,
96
+ verbose,
97
+ })
98
+
99
+ // Combine with previous results
100
+ const totalAnalyzedCommits = [
101
+ ...progressState.analyzedCommits,
102
+ ...result.analyzedCommits,
103
+ ]
104
+
105
+ return {
106
+ analyzedCommits: totalAnalyzedCommits,
107
+ failedCommits: result.failedCommits,
108
+ totalProcessed:
109
+ progressState.processedCommits.length + result.totalProcessed,
110
+ }
111
+ }
112
+
113
+ private async promptUserForResume(): Promise<boolean> {
114
+ const { question, close } = createPromiseReadline()
115
+ try {
116
+ const answer = await question("Resume from previous session? (y/n): ")
117
+ const normalizedAnswer = answer.toLowerCase().trim()
118
+ return normalizedAnswer === "y" || normalizedAnswer === "yes"
119
+ } finally {
120
+ close()
121
+ }
122
+ }
123
+ }