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,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
|
+
}
|