commit-analyzer 1.0.3 → 1.1.1
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 +18 -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 +209 -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 +46 -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 +252 -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 +143 -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 +109 -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 -411
- package/src/progress.ts +0 -84
- package/src/report-generator.ts +0 -286
- package/src/types.ts +0 -24
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execSync } from "child_process"
|
|
2
|
+
|
|
3
|
+
import { LLMAdapter } from "./llm-adapter"
|
|
4
|
+
|
|
5
|
+
export class OpenAILLMAdapter extends LLMAdapter {
|
|
6
|
+
private static readonly MAX_PROMPT_LENGTH = parseInt(
|
|
7
|
+
process.env.OPENAI_MAX_PROMPT_LENGTH || "100000",
|
|
8
|
+
10,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
protected getMaxPromptLength(): number {
|
|
12
|
+
return OpenAILLMAdapter.MAX_PROMPT_LENGTH
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async detectAvailableModels(): Promise<string[]> {
|
|
16
|
+
try {
|
|
17
|
+
execSync("command -v codex", { stdio: "ignore" })
|
|
18
|
+
return ["codex -q", "openai"]
|
|
19
|
+
} catch {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async isAvailable(): Promise<boolean> {
|
|
25
|
+
const available = await this.detectAvailableModels()
|
|
26
|
+
return available.length > 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected async executeModelCommand(prompt: string): Promise<string> {
|
|
30
|
+
const truncatedPrompt = this.truncatePrompt(prompt)
|
|
31
|
+
|
|
32
|
+
if (this.verbose) {
|
|
33
|
+
console.log(` - Prompt length: ${truncatedPrompt.length} characters`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const modelCommand = this.model || "codex -q"
|
|
37
|
+
|
|
38
|
+
// Handle codex -q quirk: requires prompt as command line argument
|
|
39
|
+
if (modelCommand.includes("-q")) {
|
|
40
|
+
const escapedPrompt = truncatedPrompt.replace(/"/g, '\\"')
|
|
41
|
+
return execSync(`${modelCommand} "${escapedPrompt}"`, {
|
|
42
|
+
encoding: "utf8",
|
|
43
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
44
|
+
timeout: LLMAdapter.DEFAULT_TIMEOUT,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fallback for non-quiet mode commands
|
|
49
|
+
return execSync(modelCommand, {
|
|
50
|
+
input: truncatedPrompt,
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
53
|
+
timeout: LLMAdapter.DEFAULT_TIMEOUT,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
package/src/di.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { CommitAnalysisService } from "@domain/commit-analysis-service"
|
|
2
|
+
import { DateFormattingService } from "@domain/date-formatting-service"
|
|
3
|
+
import { ReportGenerationService } from "@domain/report-generation-service"
|
|
4
|
+
|
|
5
|
+
import { AnalyzeCommitsUseCase } from "@app/analyze-commits.usecase"
|
|
6
|
+
import { GenerateReportUseCase } from "@app/generate-report.usecase"
|
|
7
|
+
import { ResumeAnalysisUseCase } from "@app/resume-analysis.usecase"
|
|
8
|
+
|
|
9
|
+
import { AnalyzeCommand } from "@presentation/analyze-command"
|
|
10
|
+
import { CLIApplication } from "@presentation/cli-application"
|
|
11
|
+
import { CommitAnalysisController } from "@presentation/commit-analysis-controller"
|
|
12
|
+
import { ReportCommand } from "@presentation/report-command"
|
|
13
|
+
import { ResumeCommand } from "@presentation/resume-command"
|
|
14
|
+
|
|
15
|
+
import { CacheService } from "@infra/cache-service"
|
|
16
|
+
import { CachedAnalysisRepository } from "@infra/cached-analysis-repository"
|
|
17
|
+
import { FileStorageRepository } from "@infra/file-storage-repository"
|
|
18
|
+
import { FileSystemStorageAdapter } from "@infra/file-system-storage-adapter"
|
|
19
|
+
import { GitAdapter } from "@infra/git-adapter"
|
|
20
|
+
import { GitCommitRepository } from "@infra/git-commit-repository"
|
|
21
|
+
import { JSONProgressTracker } from "@infra/json-progress-tracker"
|
|
22
|
+
import { LLMAdapterFactory } from "@infra/llm-adapter-factory"
|
|
23
|
+
import { LLMAnalysisRepository } from "@infra/llm-analysis-repository"
|
|
24
|
+
|
|
25
|
+
export interface DIContainerOptions {
|
|
26
|
+
llm?: string
|
|
27
|
+
noCache?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class DIContainer {
|
|
31
|
+
private readonly gitAdapter = new GitAdapter()
|
|
32
|
+
private readonly llmAdapter = LLMAdapterFactory.create(this.options?.llm)
|
|
33
|
+
private readonly storageAdapter = new FileSystemStorageAdapter()
|
|
34
|
+
private readonly cacheService = (() => {
|
|
35
|
+
const service = new CacheService()
|
|
36
|
+
if (this.options?.noCache) {
|
|
37
|
+
service.setCacheEnabled(false)
|
|
38
|
+
}
|
|
39
|
+
return service
|
|
40
|
+
})()
|
|
41
|
+
private readonly progressTracker = new JSONProgressTracker(
|
|
42
|
+
this.storageAdapter,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
private readonly commitRepository = new GitCommitRepository(this.gitAdapter)
|
|
46
|
+
private readonly llmAnalysisRepository = new LLMAnalysisRepository(
|
|
47
|
+
this.llmAdapter,
|
|
48
|
+
)
|
|
49
|
+
private readonly analysisRepository = this.options?.noCache
|
|
50
|
+
? this.llmAnalysisRepository
|
|
51
|
+
: new CachedAnalysisRepository(
|
|
52
|
+
this.llmAnalysisRepository,
|
|
53
|
+
this.cacheService,
|
|
54
|
+
)
|
|
55
|
+
private readonly storageRepository = new FileStorageRepository(
|
|
56
|
+
this.storageAdapter,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
private readonly commitAnalysisService = new CommitAnalysisService(
|
|
60
|
+
this.commitRepository,
|
|
61
|
+
this.analysisRepository,
|
|
62
|
+
)
|
|
63
|
+
private readonly reportGenerationService = new ReportGenerationService()
|
|
64
|
+
private readonly dateFormattingService = new DateFormattingService()
|
|
65
|
+
|
|
66
|
+
private readonly analyzeCommitsUseCase = new AnalyzeCommitsUseCase(
|
|
67
|
+
this.commitAnalysisService,
|
|
68
|
+
this.progressTracker,
|
|
69
|
+
this.storageRepository,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
private readonly generateReportUseCase = new GenerateReportUseCase(
|
|
73
|
+
this.reportGenerationService,
|
|
74
|
+
this.storageRepository,
|
|
75
|
+
this.llmAdapter,
|
|
76
|
+
this.dateFormattingService,
|
|
77
|
+
this.gitAdapter,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
private readonly resumeAnalysisUseCase = new ResumeAnalysisUseCase(
|
|
81
|
+
this.progressTracker,
|
|
82
|
+
this.analyzeCommitsUseCase,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
private readonly analyzeCommand = new AnalyzeCommand(
|
|
86
|
+
this.analyzeCommitsUseCase,
|
|
87
|
+
this.commitAnalysisService,
|
|
88
|
+
this.commitRepository,
|
|
89
|
+
)
|
|
90
|
+
private readonly reportCommand = new ReportCommand(this.generateReportUseCase)
|
|
91
|
+
private readonly resumeCommand = new ResumeCommand(this.resumeAnalysisUseCase)
|
|
92
|
+
|
|
93
|
+
private readonly controller = new CommitAnalysisController(
|
|
94
|
+
this.analyzeCommand,
|
|
95
|
+
this.reportCommand,
|
|
96
|
+
this.resumeCommand,
|
|
97
|
+
this.progressTracker,
|
|
98
|
+
this.cacheService,
|
|
99
|
+
this.commitRepository,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
private readonly application = new CLIApplication(this.controller)
|
|
103
|
+
|
|
104
|
+
constructor(private readonly options?: DIContainerOptions) {}
|
|
105
|
+
|
|
106
|
+
getApplication(): CLIApplication {
|
|
107
|
+
return this.application
|
|
108
|
+
}
|
|
109
|
+
}
|