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,182 @@
1
+ import { Analysis } from "@domain/analysis"
2
+ import { AnalyzedCommit } from "@domain/analyzed-commit"
3
+ import { Category } from "@domain/category"
4
+ import { Commit } from "@domain/commit"
5
+ import { CommitHash } from "@domain/commit-hash"
6
+
7
+ import { ConsoleFormatter } from "@presentation/console-formatter"
8
+ import {
9
+ IProgressRepository,
10
+ ProgressState,
11
+ } from "@presentation/progress-repository.interface"
12
+ import { IStorageService } from "@presentation/storage-service.interface"
13
+
14
+ import { calculatePercentage } from "../utils"
15
+ import { AppPaths } from "../utils/app-paths"
16
+
17
+ export class JSONProgressTracker implements IProgressRepository {
18
+ private static readonly JSON_INDENT = 2
19
+
20
+ constructor(private readonly storageService: IStorageService) {}
21
+
22
+ private getProgressFilePath(): string {
23
+ return AppPaths.getProgressFilePath()
24
+ }
25
+
26
+ async saveProgress(state: ProgressState): Promise<void> {
27
+ const serializedState = this.serializeProgressState(state)
28
+ const content = JSON.stringify(
29
+ serializedState,
30
+ null,
31
+ JSONProgressTracker.JSON_INDENT,
32
+ )
33
+ await this.storageService.writeFile(
34
+ this.getProgressFilePath(),
35
+ content,
36
+ )
37
+ }
38
+
39
+ async loadProgress(): Promise<ProgressState | null> {
40
+ if (!(await this.hasProgress())) {
41
+ return null
42
+ }
43
+
44
+ try {
45
+ const content = await this.storageService.readFile(
46
+ this.getProgressFilePath(),
47
+ )
48
+ const serializedState = JSON.parse(content)
49
+ return this.deserializeProgressState(serializedState)
50
+ } catch (error) {
51
+ ConsoleFormatter.logError(`Failed to load progress file: ${error}`)
52
+ return null
53
+ }
54
+ }
55
+
56
+ async hasProgress(): Promise<boolean> {
57
+ return this.storageService.fileExists(this.getProgressFilePath())
58
+ }
59
+
60
+ async clearProgress(): Promise<void> {
61
+ await this.storageService.deleteFile(this.getProgressFilePath())
62
+ }
63
+
64
+ getRemainingCommits(state: ProgressState): CommitHash[] {
65
+ const processedHashes = new Set(
66
+ state.processedCommits.map((hash) => hash.getValue()),
67
+ )
68
+ return state.totalCommits.filter(
69
+ (hash) => !processedHashes.has(hash.getValue()),
70
+ )
71
+ }
72
+
73
+ formatProgressSummary(state: ProgressState): string {
74
+ const processed = state.processedCommits.length
75
+ const total = state.totalCommits.length
76
+ const remaining = total - processed
77
+ const percentComplete = calculatePercentage(processed, total)
78
+
79
+ return `
80
+ Previous session:
81
+ - Started: ${state.startTime.toLocaleString()}
82
+ - Progress: ${processed}/${total} commits (${percentComplete}%)
83
+ - Remaining: ${remaining} commits
84
+ - Output file: ${state.outputFile}
85
+ `.trim()
86
+ }
87
+
88
+ private serializeProgressState(
89
+ state: ProgressState,
90
+ ): Record<string, unknown> {
91
+ return {
92
+ totalCommits: state.totalCommits.map((hash) => hash.getValue()),
93
+ processedCommits: state.processedCommits.map((hash) => hash.getValue()),
94
+ analyzedCommits: state.analyzedCommits.map((commit) => ({
95
+ hash: commit.getHash().getValue(),
96
+ message: commit.getMessage(),
97
+ date: commit.getDate().toISOString(),
98
+ year: commit.getYear(),
99
+ category: commit.getAnalysis().getCategory().getValue(),
100
+ summary: commit.getAnalysis().getSummary(),
101
+ description: commit.getAnalysis().getDescription(),
102
+ })),
103
+ lastProcessedIndex: state.lastProcessedIndex,
104
+ startTime: state.startTime.toISOString(),
105
+ outputFile: state.outputFile,
106
+ }
107
+ }
108
+
109
+ private deserializeProgressState(
110
+ data: Record<string, unknown>,
111
+ ): ProgressState {
112
+ this.validateProgressData(data)
113
+
114
+ return {
115
+ totalCommits: (data.totalCommits as string[]).map((hash: string) =>
116
+ CommitHash.create(hash),
117
+ ),
118
+ processedCommits: (data.processedCommits as string[]).map(
119
+ (hash: string) => CommitHash.create(hash),
120
+ ),
121
+ analyzedCommits: this.deserializeAnalyzedCommits(
122
+ data.analyzedCommits as Record<string, unknown>[],
123
+ ),
124
+ lastProcessedIndex: data.lastProcessedIndex as number,
125
+ startTime: new Date(data.startTime as string),
126
+ outputFile: data.outputFile as string,
127
+ }
128
+ }
129
+
130
+ private validateProgressData(data: Record<string, unknown>): void {
131
+ if (!Array.isArray(data.totalCommits)) {
132
+ throw new Error("Invalid progress data: totalCommits must be an array")
133
+ }
134
+
135
+ if (!Array.isArray(data.processedCommits)) {
136
+ throw new Error(
137
+ "Invalid progress data: processedCommits must be an array",
138
+ )
139
+ }
140
+
141
+ if (typeof data.lastProcessedIndex !== "number") {
142
+ throw new Error(
143
+ "Invalid progress data: lastProcessedIndex must be a number",
144
+ )
145
+ }
146
+
147
+ if (typeof data.startTime !== "string") {
148
+ throw new Error("Invalid progress data: startTime must be a string")
149
+ }
150
+
151
+ if (typeof data.outputFile !== "string") {
152
+ throw new Error("Invalid progress data: outputFile must be a string")
153
+ }
154
+ }
155
+
156
+ private deserializeAnalyzedCommits(
157
+ data: Record<string, unknown>[],
158
+ ): AnalyzedCommit[] {
159
+ if (!Array.isArray(data)) {
160
+ return []
161
+ }
162
+
163
+ return data.map((item: Record<string, unknown>) => {
164
+ const hash = CommitHash.create(item.hash as string)
165
+ const commit = new Commit({
166
+ hash,
167
+ message: item.message as string,
168
+ date: new Date(item.date as string),
169
+ diff: "", // We don't store diff in progress, so use empty string
170
+ })
171
+
172
+ const category = Category.create(item.category as string)
173
+ const analysis = new Analysis({
174
+ category,
175
+ summary: item.summary as string,
176
+ description: item.description as string,
177
+ })
178
+
179
+ return new AnalyzedCommit(commit, analysis)
180
+ })
181
+ }
182
+ }
@@ -0,0 +1,26 @@
1
+ import { ClaudeLLMAdapter } from "./claude-llm-adapter"
2
+ import { GeminiLLMAdapter } from "./gemini-llm-adapter"
3
+ import { LLMAdapter } from "./llm-adapter"
4
+ import { OpenAILLMAdapter } from "./openai-llm-adapter"
5
+
6
+ export class LLMAdapterFactory {
7
+ static create(llm?: string): LLMAdapter {
8
+ const normalizedLLM = llm?.toLowerCase()
9
+
10
+ switch (normalizedLLM) {
11
+ case "gemini":
12
+ return new GeminiLLMAdapter()
13
+ case "openai":
14
+ case "gpt":
15
+ return new OpenAILLMAdapter()
16
+ case "claude":
17
+ default:
18
+ return new ClaudeLLMAdapter()
19
+ }
20
+ }
21
+
22
+ static getSupportedLLMs(): string[] {
23
+ return ["claude", "gemini", "openai"]
24
+ }
25
+ }
26
+
@@ -0,0 +1,455 @@
1
+ import { CategoryType } from "@domain/category"
2
+
3
+ import { ILLMService } from "@app/llm-service"
4
+
5
+ import { sleep } from "../utils"
6
+
7
+ export abstract class LLMAdapter implements ILLMService {
8
+ protected static readonly DEFAULT_TIMEOUT = 60000
9
+ protected static readonly MAX_RETRIES = parseInt(
10
+ process.env.LLM_MAX_RETRIES || "3",
11
+ 10,
12
+ )
13
+ protected static readonly INITIAL_RETRY_DELAY = parseInt(
14
+ process.env.LLM_INITIAL_RETRY_DELAY || "5000",
15
+ 10,
16
+ )
17
+ protected static readonly MAX_RETRY_DELAY = parseInt(
18
+ process.env.LLM_MAX_RETRY_DELAY || "30000",
19
+ 10,
20
+ )
21
+ protected static readonly RETRY_MULTIPLIER = parseFloat(
22
+ process.env.LLM_RETRY_MULTIPLIER || "2",
23
+ )
24
+
25
+ protected model: string = ""
26
+ protected verbose: boolean = false
27
+ protected retryEnabled: boolean = true
28
+
29
+ protected abstract getMaxPromptLength(): number
30
+
31
+ abstract detectAvailableModels(): Promise<string[]>
32
+ abstract isAvailable(): Promise<boolean>
33
+ protected abstract executeModelCommand(prompt: string): Promise<string>
34
+
35
+ setLLM(llm: string): void {
36
+ this.model = llm
37
+ }
38
+
39
+ // Deprecated: Use setLLM instead
40
+ setModel(model: string): void {
41
+ this.setLLM(model)
42
+ }
43
+
44
+ setVerbose(verbose: boolean): void {
45
+ this.verbose = verbose
46
+ }
47
+
48
+ setRetryEnabled(enabled: boolean): void {
49
+ this.retryEnabled = enabled
50
+ }
51
+
52
+ getMaxRetries(): number {
53
+ return LLMAdapter.MAX_RETRIES
54
+ }
55
+
56
+ async analyzeCommit(
57
+ message: string,
58
+ diff: string,
59
+ ): Promise<{
60
+ category: CategoryType
61
+ summary: string
62
+ description: string
63
+ }> {
64
+ const prompt = this.buildPrompt(message, diff)
65
+ let lastError: Error | null = null
66
+
67
+ if (!this.retryEnabled) {
68
+ try {
69
+ const output = await this.executeModelCommand(prompt)
70
+ return this.parseResponse(output)
71
+ } catch (error) {
72
+ throw new Error(
73
+ `Failed to analyze commit: ${error instanceof Error ? error.message : "Unknown error"}`,
74
+ )
75
+ }
76
+ }
77
+
78
+ for (let attempt = 1; attempt <= LLMAdapter.MAX_RETRIES; attempt++) {
79
+ try {
80
+ const output = await this.executeModelCommand(prompt)
81
+ return this.parseResponse(output)
82
+ } catch (error) {
83
+ lastError = error instanceof Error ? error : new Error("Unknown error")
84
+
85
+ if (attempt < LLMAdapter.MAX_RETRIES) {
86
+ const delay = Math.min(
87
+ LLMAdapter.INITIAL_RETRY_DELAY *
88
+ Math.pow(LLMAdapter.RETRY_MULTIPLIER, attempt - 1),
89
+ LLMAdapter.MAX_RETRY_DELAY,
90
+ )
91
+
92
+ if (this.verbose) {
93
+ console.log(
94
+ ` - Attempt ${attempt}/${LLMAdapter.MAX_RETRIES} failed. Retrying in ${delay / 1000}s...`,
95
+ )
96
+ }
97
+
98
+ await sleep(delay)
99
+ }
100
+ }
101
+ }
102
+
103
+ throw new Error(
104
+ `Failed to analyze commit after ${LLMAdapter.MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`,
105
+ )
106
+ }
107
+
108
+ protected buildPrompt(commitMessage: string, diff: string): string {
109
+ return `Analyze this git commit and provide a categorization.
110
+
111
+ COMMIT MESSAGE:
112
+ ${commitMessage}
113
+
114
+ COMMIT DIFF:
115
+ ${diff}
116
+
117
+ Based on the commit message and code changes, categorize this commit as one of:
118
+ - "tweak": Minor adjustments, bug fixes, small improvements
119
+ - "feature": New functionality, major additions
120
+ - "process": Build system, CI/CD, tooling, configuration changes
121
+
122
+ IMPORTANT: You must respond with ONLY a valid JSON object in this exact format:
123
+
124
+ \`\`\`json
125
+ {
126
+ "category": "tweak|feature|process",
127
+ "summary": "One-line description (max 80 characters)",
128
+ "description": "Detailed explanation in 2-3 sentences"
129
+ }
130
+ \`\`\`
131
+
132
+ Do not include any other text outside the JSON code block.`
133
+ }
134
+
135
+ protected parseResponse(response: string): {
136
+ category: CategoryType
137
+ summary: string
138
+ description: string
139
+ } {
140
+ try {
141
+ const jsonMatch = response.match(/```json\s*([\s\S]*?)\s*```/)
142
+ if (!jsonMatch) {
143
+ throw new Error("No JSON block found in response")
144
+ }
145
+
146
+ const jsonString = jsonMatch[1].trim()
147
+ const parsed = JSON.parse(jsonString)
148
+
149
+ const { category, summary, description } = parsed
150
+
151
+ if (!this.isValidCategory(category)) {
152
+ throw new Error(`Invalid category: ${category}`)
153
+ }
154
+
155
+ if (!summary || !description) {
156
+ throw new Error("Missing required fields in response")
157
+ }
158
+
159
+ return {
160
+ category: category as CategoryType,
161
+ summary: summary.substring(0, 80), // Ensure max length
162
+ description: description,
163
+ }
164
+ } catch (error) {
165
+ if (this.verbose) {
166
+ console.log(
167
+ ` - Raw LLM response (first 2000 chars): ${response.substring(0, 2000)}`,
168
+ )
169
+ if (response.length > 2000) {
170
+ console.log(
171
+ ` - Response truncated (total length: ${response.length} chars)`,
172
+ )
173
+ }
174
+ }
175
+
176
+ throw new Error(
177
+ `Failed to parse LLM response: ${error instanceof Error ? error.message : "Unknown error"}`,
178
+ )
179
+ }
180
+ }
181
+
182
+ protected isValidCategory(category: string): category is CategoryType {
183
+ return ["tweak", "feature", "process"].includes(category)
184
+ }
185
+
186
+ async generateYearlySummariesFromCSV(csvContent: string): Promise<string> {
187
+ return this.generateTimePeriodSummariesFromCSV(csvContent, 'yearly')
188
+ }
189
+
190
+ async generateTimePeriodSummariesFromCSV(csvContent: string, period: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'): Promise<string> {
191
+ const prompt = this.buildTimePeriodReportPrompt(csvContent, period)
192
+ let lastError: Error | null = null
193
+
194
+ if (!this.retryEnabled) {
195
+ try {
196
+ const output = await this.executeModelCommand(prompt)
197
+ return this.parseReportResponse(output)
198
+ } catch (error) {
199
+ throw new Error(
200
+ `Failed to generate report: ${error instanceof Error ? error.message : "Unknown error"}`,
201
+ )
202
+ }
203
+ }
204
+
205
+ for (let attempt = 1; attempt <= LLMAdapter.MAX_RETRIES; attempt++) {
206
+ try {
207
+ const output = await this.executeModelCommand(prompt)
208
+ return this.parseReportResponse(output)
209
+ } catch (error) {
210
+ lastError = error instanceof Error ? error : new Error("Unknown error")
211
+
212
+ if (this.verbose) {
213
+ console.log(` - Error generating report:`)
214
+ console.log(` Attempt: ${attempt}/${LLMAdapter.MAX_RETRIES}`)
215
+ console.log(` Error: ${lastError.message}`)
216
+ }
217
+
218
+ if (attempt < LLMAdapter.MAX_RETRIES) {
219
+ const delay = Math.min(
220
+ LLMAdapter.INITIAL_RETRY_DELAY *
221
+ Math.pow(LLMAdapter.RETRY_MULTIPLIER, attempt - 1),
222
+ LLMAdapter.MAX_RETRY_DELAY,
223
+ )
224
+ if (this.verbose) {
225
+ console.log(` Retrying in ${delay / 1000}s...`)
226
+ }
227
+ await sleep(delay)
228
+ }
229
+ }
230
+ }
231
+
232
+ throw new Error(
233
+ `Failed to generate report after ${LLMAdapter.MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`,
234
+ )
235
+ }
236
+
237
+ private buildReportPrompt(csvContent: string): string {
238
+ return `Analyze the following CSV data containing git commit analysis results and generate a condensed markdown development summary report.
239
+
240
+ CSV DATA:
241
+ ${csvContent}
242
+
243
+ INSTRUCTIONS:
244
+ 1. Group the data by year (descending order, most recent first)
245
+ 2. Within each year, group by category: Features, Process Improvements, and Tweaks & Bug Fixes
246
+ 3. Consolidate similar items within each category to create readable summaries
247
+ 4. Focus on what was accomplished rather than individual commit details
248
+ 5. Use clear, professional language appropriate for stakeholders
249
+
250
+ CATEGORY MAPPING:
251
+ - "feature" → "Features" section
252
+ - "process" → "Processes" section
253
+ - "tweak" → "Tweaks & Bug Fixes" section
254
+
255
+ CONSOLIDATION GUIDELINES:
256
+ - Group similar features together (e.g., "authentication system improvements")
257
+ - Combine related bug fixes (e.g., "resolved 8 authentication issues")
258
+ - Summarize process changes by theme (e.g., "CI/CD pipeline enhancements")
259
+ - Use bullet points for individual items within categories
260
+ - Aim for 3-7 bullet points per category per year
261
+ - Include specific numbers when relevant (e.g., "15 bug fixes", "3 new features")
262
+
263
+ OUTPUT FORMAT:
264
+ Generate yearly summary sections with this exact structure (DO NOT include the main title or commit analysis section):
265
+
266
+ \`\`\`markdown
267
+ ## [YEAR]
268
+ ### Features
269
+ - [Consolidated feature summary 1]
270
+ - [Consolidated feature summary 2]
271
+ - [Additional features as needed]
272
+
273
+ ### Processes
274
+ - [Consolidated process improvement 1]
275
+ - [Consolidated process improvement 2]
276
+ - [Additional process items as needed]
277
+
278
+ ### Tweaks & Bug Fixes
279
+ - [Consolidated tweak/fix summary 1]
280
+ - [Consolidated tweak/fix summary 2]
281
+ - [Additional tweaks/fixes as needed]
282
+
283
+ ## [PREVIOUS YEAR]
284
+ [Repeat structure for each year in the data]
285
+ \`\`\`
286
+
287
+ QUALITY REQUIREMENTS:
288
+ - Keep summaries concise but informative
289
+ - Use active voice and clear language
290
+ - Avoid technical jargon where possible
291
+ - Ensure each bullet point represents meaningful work
292
+ - Make the report valuable for both technical and non-technical readers
293
+
294
+ Generate the markdown report now:`
295
+ }
296
+
297
+ private buildTimePeriodReportPrompt(csvContent: string, period: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'): string {
298
+ const periodDisplayName = this.getPeriodDisplayName(period)
299
+ const sectionHeader = this.getSectionHeader(period)
300
+
301
+ return `Analyze the following CSV data containing git commit analysis results and generate a condensed markdown development summary report.
302
+
303
+ CSV DATA:
304
+ ${csvContent}
305
+
306
+ INSTRUCTIONS:
307
+ 1. Group the data by ${periodDisplayName} (descending order, most recent first)
308
+ 2. Within each ${periodDisplayName.toLowerCase()}, group by category: Features, Process Improvements, and Tweaks & Bug Fixes
309
+ 3. Consolidate similar items within each category to create readable summaries
310
+ 4. Focus on what was accomplished rather than individual commit details
311
+ 5. Use clear, professional language appropriate for stakeholders
312
+ 6. Only include sections for time periods that have commits
313
+
314
+ CATEGORY MAPPING:
315
+ - "feature" → "Features" section
316
+ - "process" → "Processes" section
317
+ - "tweak" → "Tweaks & Bug Fixes" section
318
+
319
+ CONSOLIDATION GUIDELINES:
320
+ - Group similar features together (e.g., "authentication system improvements")
321
+ - Combine related bug fixes (e.g., "resolved 8 authentication issues")
322
+ - Summarize process changes by theme (e.g., "CI/CD pipeline enhancements")
323
+ - Use bullet points for individual items within categories
324
+ - Aim for 3-7 bullet points per category per ${periodDisplayName.toLowerCase()}
325
+ - Include specific numbers when relevant (e.g., "15 bug fixes", "3 new features")
326
+
327
+ OUTPUT FORMAT:
328
+ Generate ${periodDisplayName.toLowerCase()} summary sections with this exact structure (DO NOT include the main title or commit analysis section):
329
+
330
+ \`\`\`markdown
331
+ ${sectionHeader}
332
+ ### Features
333
+ - [Consolidated feature summary 1]
334
+ - [Consolidated feature summary 2]
335
+ - [Additional features as needed]
336
+
337
+ ### Processes
338
+ - [Consolidated process improvement 1]
339
+ - [Consolidated process improvement 2]
340
+ - [Additional process items as needed]
341
+
342
+ ### Tweaks & Bug Fixes
343
+ - [Consolidated tweak/fix summary 1]
344
+ - [Consolidated tweak/fix summary 2]
345
+ - [Additional tweaks/fixes as needed]
346
+
347
+ ${this.getPreviousPeriodExample(period)}
348
+ [Repeat structure for each ${periodDisplayName.toLowerCase()} in the data]
349
+ \`\`\`
350
+
351
+ QUALITY REQUIREMENTS:
352
+ - Keep summaries concise but informative
353
+ - Use active voice and clear language
354
+ - Avoid technical jargon where possible
355
+ - Ensure each bullet point represents meaningful work
356
+ - Make the report valuable for both technical and non-technical readers
357
+
358
+ Generate the markdown report now:`
359
+ }
360
+
361
+ private getPeriodDisplayName(period: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'): string {
362
+ switch (period) {
363
+ case 'daily': return 'Daily Period'
364
+ case 'weekly': return 'Week'
365
+ case 'monthly': return 'Month'
366
+ case 'quarterly': return 'Quarter'
367
+ case 'yearly': return 'Year'
368
+ }
369
+ }
370
+
371
+ private getSectionHeader(period: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'): string {
372
+ switch (period) {
373
+ case 'daily': return '## [DATE] [TIME_OF_DAY]'
374
+ case 'weekly': return '## [WEEK_RANGE]'
375
+ case 'monthly': return '## [MONTH] [YEAR]'
376
+ case 'quarterly': return '## [QUARTER] [YEAR]'
377
+ case 'yearly': return '## [YEAR]'
378
+ }
379
+ }
380
+
381
+ private getPreviousPeriodExample(period: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'): string {
382
+ switch (period) {
383
+ case 'daily': return '## [PREVIOUS_DATE] [TIME_OF_DAY]'
384
+ case 'weekly': return '## [PREVIOUS_WEEK_RANGE]'
385
+ case 'monthly': return '## [PREVIOUS_MONTH] [YEAR]'
386
+ case 'quarterly': return '## [PREVIOUS_QUARTER] [YEAR]'
387
+ case 'yearly': return '## [PREVIOUS_YEAR]'
388
+ }
389
+ }
390
+
391
+ private parseReportResponse(response: string): string {
392
+ // Look for markdown block first
393
+ const markdownMatch = response.match(/```markdown\s*([\s\S]*?)\s*```/)
394
+ if (markdownMatch) {
395
+ return markdownMatch[1].trim()
396
+ }
397
+
398
+ // If no markdown block, look for content starting with "##" (yearly sections)
399
+ const yearSectionMatch = response.match(/^(##\s+\d{4}[\s\S]*)/m)
400
+ if (yearSectionMatch) {
401
+ return yearSectionMatch[1].trim()
402
+ }
403
+
404
+ // If no clear structure found, return the entire response but log a warning
405
+ if (this.verbose) {
406
+ console.log(
407
+ " - Warning: Could not find structured yearly sections in LLM response",
408
+ )
409
+ console.log(` - Response preview: ${response.substring(0, 200)}...`)
410
+ }
411
+
412
+ return response.trim()
413
+ }
414
+
415
+ protected truncatePrompt(prompt: string): string {
416
+ const maxLength = this.getMaxPromptLength()
417
+ if (prompt.length <= maxLength) {
418
+ return prompt
419
+ }
420
+
421
+ // Find the diff section and truncate it
422
+ const diffStartIndex = prompt.indexOf("COMMIT DIFF:")
423
+ if (diffStartIndex === -1) {
424
+ return prompt
425
+ }
426
+
427
+ const beforeDiff = prompt.substring(0, diffStartIndex)
428
+ const afterDiffHeader = prompt.substring(diffStartIndex)
429
+ const diffHeaderEnd = afterDiffHeader.indexOf("\n") + 1
430
+ const diffHeader = afterDiffHeader.substring(0, diffHeaderEnd)
431
+ const diffContent = afterDiffHeader.substring(diffHeaderEnd)
432
+
433
+ // Calculate how much space we have for the diff
434
+ const overhead = beforeDiff.length + diffHeader.length + 500 // Leave some buffer
435
+ const maxDiffLength = Math.max(1000, maxLength - overhead)
436
+
437
+ if (diffContent.length > maxDiffLength) {
438
+ const truncatedDiff = diffContent.substring(0, maxDiffLength)
439
+ const truncationNotice =
440
+ "\n\n[DIFF TRUNCATED - Original length: " +
441
+ diffContent.length +
442
+ " characters]"
443
+
444
+ return (
445
+ beforeDiff +
446
+ diffHeader +
447
+ truncatedDiff +
448
+ truncationNotice +
449
+ "\n\nBased on the commit message and code changes, categorize this commit..."
450
+ )
451
+ }
452
+
453
+ return prompt
454
+ }
455
+ }
@@ -0,0 +1,38 @@
1
+ import { Analysis } from "@domain/analysis"
2
+ import { Category } from "@domain/category"
3
+ import { Commit } from "@domain/commit"
4
+
5
+ import { ILLMService } from "@app/llm-service"
6
+
7
+ import { IAnalysisRepository } from "@presentation/analysis-repository.interface"
8
+
9
+ export class LLMAnalysisRepository implements IAnalysisRepository {
10
+ constructor(private readonly llmService: ILLMService) {}
11
+
12
+ async analyze(commit: Commit): Promise<Analysis> {
13
+ const result = await this.llmService.analyzeCommit(
14
+ commit.getMessage(),
15
+ commit.getDiff(),
16
+ )
17
+
18
+ const category = Category.fromType(result.category)
19
+
20
+ return new Analysis({
21
+ category,
22
+ summary: result.summary,
23
+ description: result.description,
24
+ })
25
+ }
26
+
27
+ async isAvailable(): Promise<boolean> {
28
+ return this.llmService.isAvailable()
29
+ }
30
+
31
+ getMaxRetries(): number {
32
+ return this.llmService.getMaxRetries()
33
+ }
34
+
35
+ setVerbose(verbose: boolean): void {
36
+ this.llmService.setVerbose(verbose)
37
+ }
38
+ }