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.
- package/.claude/settings.local.json +11 -1
- package/README.md +33 -2
- package/commits.csv +2 -0
- package/eslint.config.mts +45 -0
- package/package.json +17 -9
- package/src/1.domain/analysis.ts +93 -0
- package/src/1.domain/analyzed-commit.ts +97 -0
- package/src/1.domain/application-error.ts +32 -0
- package/src/1.domain/category.ts +52 -0
- package/src/1.domain/commit-analysis-service.ts +92 -0
- package/src/1.domain/commit-hash.ts +40 -0
- package/src/1.domain/commit.ts +99 -0
- package/src/1.domain/date-formatting-service.ts +81 -0
- package/src/1.domain/date-range.ts +76 -0
- package/src/1.domain/report-generation-service.ts +292 -0
- package/src/2.application/analyze-commits.usecase.ts +307 -0
- package/src/2.application/generate-report.usecase.ts +204 -0
- package/src/2.application/llm-service.ts +54 -0
- package/src/2.application/resume-analysis.usecase.ts +123 -0
- package/src/3.presentation/analysis-repository.interface.ts +27 -0
- package/src/3.presentation/analyze-command.ts +128 -0
- package/src/3.presentation/cli-application.ts +255 -0
- package/src/3.presentation/command-handler.interface.ts +4 -0
- package/src/3.presentation/commit-analysis-controller.ts +101 -0
- package/src/3.presentation/commit-repository.interface.ts +47 -0
- package/src/3.presentation/console-formatter.ts +129 -0
- package/src/3.presentation/progress-repository.interface.ts +49 -0
- package/src/3.presentation/report-command.ts +50 -0
- package/src/3.presentation/resume-command.ts +59 -0
- package/src/3.presentation/storage-repository.interface.ts +33 -0
- package/src/3.presentation/storage-service.interface.ts +32 -0
- package/src/3.presentation/version-control-service.interface.ts +41 -0
- package/src/4.infrastructure/cache-service.ts +271 -0
- package/src/4.infrastructure/cached-analysis-repository.ts +46 -0
- package/src/4.infrastructure/claude-llm-adapter.ts +124 -0
- package/src/4.infrastructure/csv-service.ts +206 -0
- package/src/4.infrastructure/file-storage-repository.ts +108 -0
- package/src/4.infrastructure/file-system-storage-adapter.ts +87 -0
- package/src/4.infrastructure/gemini-llm-adapter.ts +46 -0
- package/src/4.infrastructure/git-adapter.ts +116 -0
- package/src/4.infrastructure/git-commit-repository.ts +85 -0
- package/src/4.infrastructure/json-progress-tracker.ts +182 -0
- package/src/4.infrastructure/llm-adapter-factory.ts +26 -0
- package/src/4.infrastructure/llm-adapter.ts +455 -0
- package/src/4.infrastructure/llm-analysis-repository.ts +38 -0
- package/src/4.infrastructure/openai-llm-adapter.ts +57 -0
- package/src/di.ts +108 -0
- package/src/main.ts +63 -0
- package/src/utils/app-paths.ts +36 -0
- package/src/utils/concurrency.ts +81 -0
- package/src/utils.ts +77 -0
- package/tsconfig.json +7 -1
- package/src/cli.ts +0 -170
- package/src/csv-reader.ts +0 -180
- package/src/csv.ts +0 -40
- package/src/errors.ts +0 -49
- package/src/git.ts +0 -112
- package/src/index.ts +0 -395
- package/src/llm.ts +0 -396
- package/src/progress.ts +0 -84
- package/src/report-generator.ts +0 -286
- 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
|
+
}
|