commit-analyzer 1.0.3 → 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 -411
- package/src/progress.ts +0 -84
- package/src/report-generator.ts +0 -286
- package/src/types.ts +0 -24
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AnalyzedCommit } from "./analyzed-commit"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Domain service for formatting date ranges based on time periods
|
|
5
|
+
*/
|
|
6
|
+
export class DateFormattingService {
|
|
7
|
+
/**
|
|
8
|
+
* Format date range based on the span of commits
|
|
9
|
+
*/
|
|
10
|
+
formatDateRange(commits: AnalyzedCommit[]): string {
|
|
11
|
+
const dates = commits.map(c => c.getDate())
|
|
12
|
+
const minDate = new Date(Math.min(...dates.map(d => d.getTime())))
|
|
13
|
+
const maxDate = new Date(Math.max(...dates.map(d => d.getTime())))
|
|
14
|
+
|
|
15
|
+
const diffInMilliseconds = maxDate.getTime() - minDate.getTime()
|
|
16
|
+
const diffInDays = diffInMilliseconds / (1000 * 60 * 60 * 24)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
// For very small differences, use a more conservative approach
|
|
20
|
+
if (diffInDays < 0.05) { // Less than ~1 hour
|
|
21
|
+
return this.formatHourlyRange(minDate, maxDate)
|
|
22
|
+
} else if (diffInDays <= 1) {
|
|
23
|
+
return this.formatDailyRange(minDate, maxDate)
|
|
24
|
+
} else if (diffInDays <= 7) {
|
|
25
|
+
return this.formatWeeklyRange(minDate, maxDate)
|
|
26
|
+
} else if (diffInDays <= 31) {
|
|
27
|
+
return this.formatMonthlyRange(minDate, maxDate)
|
|
28
|
+
} else if (diffInDays <= 365) {
|
|
29
|
+
return this.formatMonthlyRange(minDate, maxDate)
|
|
30
|
+
} else {
|
|
31
|
+
return this.formatYearlyRange(minDate, maxDate)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
private formatHourlyRange(minDate: Date, maxDate: Date): string {
|
|
37
|
+
const formatTime = (date: Date) => {
|
|
38
|
+
const hour = date.getHours()
|
|
39
|
+
const ampm = hour < 12 ? 'am' : 'pm'
|
|
40
|
+
const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour
|
|
41
|
+
return `${displayHour}${ampm}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If same time, just show the single time
|
|
45
|
+
if (minDate.getTime() === maxDate.getTime()) {
|
|
46
|
+
return formatTime(minDate)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return `${formatTime(minDate)} to ${formatTime(maxDate)}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private formatDailyRange(minDate: Date, maxDate: Date): string {
|
|
53
|
+
const minDay = minDate.toLocaleDateString('en-US', { weekday: 'long' })
|
|
54
|
+
const maxDay = maxDate.toLocaleDateString('en-US', { weekday: 'long' })
|
|
55
|
+
return `${minDay} to ${maxDay}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private formatWeeklyRange(minDate: Date, maxDate: Date): string {
|
|
59
|
+
const formatWeekly = (date: Date) => `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`
|
|
60
|
+
return `${formatWeekly(minDate)} - ${formatWeekly(maxDate)}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private formatMonthlyRange(minDate: Date, maxDate: Date): string {
|
|
64
|
+
const minMonth = minDate.toLocaleDateString('en-US', { month: 'short' })
|
|
65
|
+
const maxMonth = maxDate.toLocaleDateString('en-US', { month: 'short' })
|
|
66
|
+
const minYear = minDate.getFullYear()
|
|
67
|
+
const maxYear = maxDate.getFullYear()
|
|
68
|
+
|
|
69
|
+
if (minYear === maxYear) {
|
|
70
|
+
return `${minMonth} - ${maxMonth}`
|
|
71
|
+
} else {
|
|
72
|
+
return `${minMonth} ${minYear} - ${maxMonth} ${maxYear}`
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private formatYearlyRange(minDate: Date, maxDate: Date): string {
|
|
77
|
+
const minYear = minDate.getFullYear()
|
|
78
|
+
const maxYear = maxDate.getFullYear()
|
|
79
|
+
return minYear === maxYear ? minYear.toString() : `${minYear} - ${maxYear}`
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value object for date ranges in commit analysis
|
|
3
|
+
*/
|
|
4
|
+
export class DateRange {
|
|
5
|
+
private constructor(
|
|
6
|
+
private readonly startDate: Date,
|
|
7
|
+
private readonly endDate: Date,
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
static create(startDate: Date, endDate: Date): DateRange {
|
|
11
|
+
if (!startDate || !endDate) {
|
|
12
|
+
throw new Error('Start date and end date are required')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (startDate > endDate) {
|
|
16
|
+
throw new Error('Start date cannot be after end date')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return new DateRange(new Date(startDate), new Date(endDate))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static fromYear(year: number): DateRange {
|
|
23
|
+
if (!Number.isInteger(year) || year < 1970) {
|
|
24
|
+
throw new Error('Invalid year provided')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const startDate = new Date(year, 0, 1) // January 1st
|
|
28
|
+
const endDate = new Date(year, 11, 31, 23, 59, 59, 999) // December 31st
|
|
29
|
+
|
|
30
|
+
return new DateRange(startDate, endDate)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static fromYearRange(startYear: number, endYear: number): DateRange {
|
|
34
|
+
if (startYear > endYear) {
|
|
35
|
+
throw new Error('Start year cannot be after end year')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const startDate = new Date(startYear, 0, 1)
|
|
39
|
+
const endDate = new Date(endYear, 11, 31, 23, 59, 59, 999)
|
|
40
|
+
|
|
41
|
+
return new DateRange(startDate, endDate)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getStartDate(): Date {
|
|
45
|
+
return new Date(this.startDate)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getEndDate(): Date {
|
|
49
|
+
return new Date(this.endDate)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
contains(date: Date): boolean {
|
|
53
|
+
return date >= this.startDate && date <= this.endDate
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getYearSpan(): number {
|
|
57
|
+
return this.endDate.getFullYear() - this.startDate.getFullYear() + 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getYears(): number[] {
|
|
61
|
+
const years: number[] = []
|
|
62
|
+
for (let year = this.startDate.getFullYear(); year <= this.endDate.getFullYear(); year++) {
|
|
63
|
+
years.push(year)
|
|
64
|
+
}
|
|
65
|
+
return years
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
equals(other: DateRange): boolean {
|
|
69
|
+
return this.startDate.getTime() === other.startDate.getTime() &&
|
|
70
|
+
this.endDate.getTime() === other.endDate.getTime()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
toString(): string {
|
|
74
|
+
return `${this.startDate.getFullYear()}-${this.endDate.getFullYear()}`
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { AnalyzedCommit } from "./analyzed-commit"
|
|
2
|
+
import { Category, CategoryType } from "./category"
|
|
3
|
+
import { DateRange } from "./date-range"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Statistics for analyzed commits
|
|
7
|
+
*/
|
|
8
|
+
export interface CommitStatistics {
|
|
9
|
+
totalCommits: number
|
|
10
|
+
yearRange: {
|
|
11
|
+
min: number
|
|
12
|
+
max: number
|
|
13
|
+
}
|
|
14
|
+
categoryBreakdown: Record<CategoryType, number>
|
|
15
|
+
yearlyBreakdown: Record<number, number>
|
|
16
|
+
largeChanges: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Domain service for report generation operations
|
|
21
|
+
*/
|
|
22
|
+
export class ReportGenerationService {
|
|
23
|
+
/**
|
|
24
|
+
* Generates statistics from analyzed commits
|
|
25
|
+
*/
|
|
26
|
+
generateStatistics(commits: AnalyzedCommit[]): CommitStatistics {
|
|
27
|
+
if (commits.length === 0) {
|
|
28
|
+
throw new Error("Cannot generate statistics from empty commit list")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const years = commits.map((c) => c.getYear())
|
|
32
|
+
|
|
33
|
+
const categoryBreakdown: Record<CategoryType, number> = {
|
|
34
|
+
tweak: 0,
|
|
35
|
+
feature: 0,
|
|
36
|
+
process: 0,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const yearlyBreakdown: Record<number, number> = {}
|
|
40
|
+
let largeChanges = 0
|
|
41
|
+
|
|
42
|
+
for (const commit of commits) {
|
|
43
|
+
// Category breakdown
|
|
44
|
+
const category = commit.getAnalysis().getCategory().getValue()
|
|
45
|
+
categoryBreakdown[category]++
|
|
46
|
+
|
|
47
|
+
// Yearly breakdown
|
|
48
|
+
const year = commit.getYear()
|
|
49
|
+
yearlyBreakdown[year] = (yearlyBreakdown[year] || 0) + 1
|
|
50
|
+
|
|
51
|
+
// Large changes count
|
|
52
|
+
if (commit.isLargeChange()) {
|
|
53
|
+
largeChanges++
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
totalCommits: commits.length,
|
|
59
|
+
yearRange: {
|
|
60
|
+
min: Math.min(...years),
|
|
61
|
+
max: Math.max(...years),
|
|
62
|
+
},
|
|
63
|
+
categoryBreakdown,
|
|
64
|
+
yearlyBreakdown,
|
|
65
|
+
largeChanges,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Filters commits by date range
|
|
71
|
+
*/
|
|
72
|
+
filterByDateRange(
|
|
73
|
+
commits: AnalyzedCommit[],
|
|
74
|
+
dateRange: DateRange,
|
|
75
|
+
): AnalyzedCommit[] {
|
|
76
|
+
return commits.filter((commit) => dateRange.contains(commit.getDate()))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Filters commits by category
|
|
81
|
+
*/
|
|
82
|
+
filterByCategory(
|
|
83
|
+
commits: AnalyzedCommit[],
|
|
84
|
+
category: Category,
|
|
85
|
+
): AnalyzedCommit[] {
|
|
86
|
+
return commits.filter((commit) =>
|
|
87
|
+
commit.getAnalysis().getCategory().equals(category),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Groups commits by year
|
|
93
|
+
*/
|
|
94
|
+
groupByYear(commits: AnalyzedCommit[]): Map<number, AnalyzedCommit[]> {
|
|
95
|
+
const grouped = new Map<number, AnalyzedCommit[]>()
|
|
96
|
+
|
|
97
|
+
for (const commit of commits) {
|
|
98
|
+
const year = commit.getYear()
|
|
99
|
+
if (!grouped.has(year)) {
|
|
100
|
+
grouped.set(year, [])
|
|
101
|
+
}
|
|
102
|
+
grouped.get(year)!.push(commit)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return grouped
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Groups commits by category
|
|
110
|
+
*/
|
|
111
|
+
groupByCategory(
|
|
112
|
+
commits: AnalyzedCommit[],
|
|
113
|
+
): Map<CategoryType, AnalyzedCommit[]> {
|
|
114
|
+
const grouped = new Map<CategoryType, AnalyzedCommit[]>()
|
|
115
|
+
|
|
116
|
+
for (const commit of commits) {
|
|
117
|
+
const category = commit.getAnalysis().getCategory().getValue()
|
|
118
|
+
if (!grouped.has(category)) {
|
|
119
|
+
grouped.set(category, [])
|
|
120
|
+
}
|
|
121
|
+
grouped.get(category)!.push(commit)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return grouped
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Sorts commits by date (newest first by default)
|
|
129
|
+
*/
|
|
130
|
+
sortByDate(
|
|
131
|
+
commits: AnalyzedCommit[],
|
|
132
|
+
ascending: boolean = false,
|
|
133
|
+
): AnalyzedCommit[] {
|
|
134
|
+
return commits.slice().sort((a, b) => {
|
|
135
|
+
const aDate = a.getDate().getTime()
|
|
136
|
+
const bDate = b.getDate().getTime()
|
|
137
|
+
return ascending ? aDate - bDate : bDate - aDate
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Gets the most significant commits (features and large changes)
|
|
143
|
+
*/
|
|
144
|
+
getSignificantCommits(commits: AnalyzedCommit[]): AnalyzedCommit[] {
|
|
145
|
+
return commits.filter(
|
|
146
|
+
(commit) =>
|
|
147
|
+
commit.getAnalysis().isFeatureAnalysis() || commit.isLargeChange(),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Determines the appropriate time period for summaries based on date range
|
|
153
|
+
*/
|
|
154
|
+
determineTimePeriod(commits: AnalyzedCommit[]): 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly' {
|
|
155
|
+
if (commits.length === 0) return 'yearly'
|
|
156
|
+
|
|
157
|
+
const dates = commits.map(c => c.getDate())
|
|
158
|
+
const minDate = new Date(Math.min(...dates.map(d => d.getTime())))
|
|
159
|
+
const maxDate = new Date(Math.max(...dates.map(d => d.getTime())))
|
|
160
|
+
|
|
161
|
+
const diffInMilliseconds = maxDate.getTime() - minDate.getTime()
|
|
162
|
+
const diffInDays = diffInMilliseconds / (1000 * 60 * 60 * 24)
|
|
163
|
+
|
|
164
|
+
if (diffInDays <= 1) return 'daily'
|
|
165
|
+
if (diffInDays <= 7) return 'weekly'
|
|
166
|
+
if (diffInDays <= 31) return 'monthly'
|
|
167
|
+
if (diffInDays <= 93) return 'quarterly' // ~3 months
|
|
168
|
+
return 'yearly'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Groups commits by the appropriate time period
|
|
173
|
+
*/
|
|
174
|
+
groupByTimePeriod(commits: AnalyzedCommit[], period: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'): Map<string, AnalyzedCommit[]> {
|
|
175
|
+
const grouped = new Map<string, AnalyzedCommit[]>()
|
|
176
|
+
|
|
177
|
+
for (const commit of commits) {
|
|
178
|
+
const date = commit.getDate()
|
|
179
|
+
let key: string
|
|
180
|
+
|
|
181
|
+
switch (period) {
|
|
182
|
+
case 'daily':
|
|
183
|
+
key = this.formatDailyKey(date)
|
|
184
|
+
break
|
|
185
|
+
case 'weekly':
|
|
186
|
+
key = this.formatWeeklyKey(date)
|
|
187
|
+
break
|
|
188
|
+
case 'monthly':
|
|
189
|
+
key = this.formatMonthlyKey(date)
|
|
190
|
+
break
|
|
191
|
+
case 'quarterly':
|
|
192
|
+
key = this.formatQuarterlyKey(date)
|
|
193
|
+
break
|
|
194
|
+
case 'yearly':
|
|
195
|
+
default:
|
|
196
|
+
key = date.getFullYear().toString()
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!grouped.has(key)) {
|
|
201
|
+
grouped.set(key, [])
|
|
202
|
+
}
|
|
203
|
+
grouped.get(key)!.push(commit)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return grouped
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private formatDailyKey(date: Date): string {
|
|
210
|
+
const year = date.getFullYear()
|
|
211
|
+
const month = date.getMonth() + 1
|
|
212
|
+
const day = date.getDate()
|
|
213
|
+
const hour = date.getHours()
|
|
214
|
+
|
|
215
|
+
if (hour < 12) return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} Morning`
|
|
216
|
+
if (hour < 17) return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} Afternoon`
|
|
217
|
+
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} Evening`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private formatWeeklyKey(date: Date): string {
|
|
221
|
+
const startOfWeek = new Date(date)
|
|
222
|
+
startOfWeek.setDate(date.getDate() - date.getDay())
|
|
223
|
+
const endOfWeek = new Date(startOfWeek)
|
|
224
|
+
endOfWeek.setDate(startOfWeek.getDate() + 6)
|
|
225
|
+
|
|
226
|
+
const formatDate = (d: Date) =>
|
|
227
|
+
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`
|
|
228
|
+
|
|
229
|
+
return `Week of ${formatDate(startOfWeek)} to ${formatDate(endOfWeek)}`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private formatMonthlyKey(date: Date): string {
|
|
233
|
+
const months = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
234
|
+
'July', 'August', 'September', 'October', 'November', 'December']
|
|
235
|
+
return `${months[date.getMonth()]} ${date.getFullYear()}`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private formatQuarterlyKey(date: Date): string {
|
|
239
|
+
const quarter = Math.floor(date.getMonth() / 3) + 1
|
|
240
|
+
return `Q${quarter} ${date.getFullYear()}`
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Converts analyzed commits to CSV string format for LLM consumption
|
|
245
|
+
*/
|
|
246
|
+
convertToCSVString(commits: AnalyzedCommit[]): string {
|
|
247
|
+
const header = "year,category,summary,description"
|
|
248
|
+
const rows = commits.map((commit) => {
|
|
249
|
+
const analysis = commit.getAnalysis()
|
|
250
|
+
return [
|
|
251
|
+
commit.getYear().toString(),
|
|
252
|
+
this.escapeCsvField(analysis.getCategory().getValue()),
|
|
253
|
+
this.escapeCsvField(analysis.getSummary()),
|
|
254
|
+
this.escapeCsvField(analysis.getDescription()),
|
|
255
|
+
].join(",")
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
return [header, ...rows].join("\n")
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Converts grouped commits to CSV with time period information
|
|
263
|
+
*/
|
|
264
|
+
convertGroupedToCSV(groupedCommits: Map<string, AnalyzedCommit[]>, period: string): string {
|
|
265
|
+
const header = `${period},category,summary,description`
|
|
266
|
+
const rows: string[] = []
|
|
267
|
+
|
|
268
|
+
for (const [timePeriod, commits] of groupedCommits) {
|
|
269
|
+
for (const commit of commits) {
|
|
270
|
+
const analysis = commit.getAnalysis()
|
|
271
|
+
rows.push([
|
|
272
|
+
this.escapeCsvField(timePeriod),
|
|
273
|
+
this.escapeCsvField(analysis.getCategory().getValue()),
|
|
274
|
+
this.escapeCsvField(analysis.getSummary()),
|
|
275
|
+
this.escapeCsvField(analysis.getDescription()),
|
|
276
|
+
].join(","))
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return [header, ...rows].join("\n")
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Escape CSV fields that contain commas, quotes, or newlines
|
|
285
|
+
*/
|
|
286
|
+
private escapeCsvField(field: string): string {
|
|
287
|
+
if (field.includes(",") || field.includes('"') || field.includes("\n")) {
|
|
288
|
+
return `"${field.replace(/"/g, '""')}"`
|
|
289
|
+
}
|
|
290
|
+
return field
|
|
291
|
+
}
|
|
292
|
+
}
|