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
package/src/csv-reader.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "fs"
|
|
2
|
-
|
|
3
|
-
export interface ParsedCSVRow {
|
|
4
|
-
year: number
|
|
5
|
-
category: "tweak" | "feature" | "process"
|
|
6
|
-
summary: string
|
|
7
|
-
description: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class CSVReaderService {
|
|
11
|
-
static readCSV(filename: string): ParsedCSVRow[] {
|
|
12
|
-
try {
|
|
13
|
-
const content = readFileSync(filename, "utf8")
|
|
14
|
-
return this.parseCSV(content)
|
|
15
|
-
} catch (error) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
`Failed to read CSV file ${filename}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
private static parseCSV(content: string): ParsedCSVRow[] {
|
|
23
|
-
const lines = content.trim().split("\n")
|
|
24
|
-
|
|
25
|
-
if (lines.length === 0) {
|
|
26
|
-
throw new Error("CSV file is empty")
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Validate header
|
|
30
|
-
const header = lines[0].toLowerCase()
|
|
31
|
-
const expectedHeader = "year,category,summary,description"
|
|
32
|
-
if (header !== expectedHeader) {
|
|
33
|
-
throw new Error(
|
|
34
|
-
`Invalid CSV format. Expected header: "${expectedHeader}", got: "${header}"`,
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const rows: ParsedCSVRow[] = []
|
|
39
|
-
|
|
40
|
-
for (let i = 1; i < lines.length; i++) {
|
|
41
|
-
const line = lines[i].trim()
|
|
42
|
-
if (line === "") continue // Skip empty lines
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const row = this.parseCSVLine(line, i + 1)
|
|
46
|
-
rows.push(row)
|
|
47
|
-
} catch (error) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`Error parsing CSV line ${i + 1}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return rows
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private static parseCSVLine(line: string, lineNumber: number): ParsedCSVRow {
|
|
58
|
-
const fields = this.parseCSVFields(line)
|
|
59
|
-
|
|
60
|
-
if (fields.length !== 4) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`Expected 4 fields (year,category,summary,description), got ${fields.length}`,
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const [yearStr, category, summary, description] = fields
|
|
67
|
-
|
|
68
|
-
// Validate year
|
|
69
|
-
const year = parseInt(yearStr, 10)
|
|
70
|
-
if (isNaN(year) || year < 1900 || year > 2100) {
|
|
71
|
-
throw new Error(`Invalid year: ${yearStr}`)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Validate category
|
|
75
|
-
if (!this.isValidCategory(category)) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
`Invalid category: ${category}. Must be one of: tweak, feature, process`,
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Validate required fields
|
|
82
|
-
if (!summary.trim()) {
|
|
83
|
-
throw new Error("Summary field cannot be empty")
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!description.trim()) {
|
|
87
|
-
throw new Error("Description field cannot be empty")
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
year,
|
|
92
|
-
category: category as "tweak" | "feature" | "process",
|
|
93
|
-
summary: summary.trim(),
|
|
94
|
-
description: description.trim(),
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Parse CSV fields handling quoted fields with commas and escaped quotes
|
|
100
|
-
*/
|
|
101
|
-
private static parseCSVFields(line: string): string[] {
|
|
102
|
-
const fields: string[] = []
|
|
103
|
-
let currentField = ""
|
|
104
|
-
let inQuotes = false
|
|
105
|
-
let i = 0
|
|
106
|
-
|
|
107
|
-
while (i < line.length) {
|
|
108
|
-
const char = line[i]
|
|
109
|
-
const nextChar = line[i + 1]
|
|
110
|
-
|
|
111
|
-
if (char === '"') {
|
|
112
|
-
if (inQuotes && nextChar === '"') {
|
|
113
|
-
// Escaped quote inside quoted field
|
|
114
|
-
currentField += '"'
|
|
115
|
-
i += 2
|
|
116
|
-
} else {
|
|
117
|
-
// Start or end of quoted field
|
|
118
|
-
inQuotes = !inQuotes
|
|
119
|
-
i++
|
|
120
|
-
}
|
|
121
|
-
} else if (char === "," && !inQuotes) {
|
|
122
|
-
// Field separator outside quotes
|
|
123
|
-
fields.push(currentField)
|
|
124
|
-
currentField = ""
|
|
125
|
-
i++
|
|
126
|
-
} else {
|
|
127
|
-
// Regular character
|
|
128
|
-
currentField += char
|
|
129
|
-
i++
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Add the last field
|
|
134
|
-
fields.push(currentField)
|
|
135
|
-
|
|
136
|
-
return fields
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private static isValidCategory(
|
|
140
|
-
category: string,
|
|
141
|
-
): category is "tweak" | "feature" | "process" {
|
|
142
|
-
return ["tweak", "feature", "process"].includes(category)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Get summary statistics about the CSV data
|
|
147
|
-
*/
|
|
148
|
-
static getStatistics(rows: ParsedCSVRow[]): {
|
|
149
|
-
totalRows: number
|
|
150
|
-
yearRange: { min: number; max: number }
|
|
151
|
-
categoryBreakdown: Record<string, number>
|
|
152
|
-
} {
|
|
153
|
-
if (rows.length === 0) {
|
|
154
|
-
return {
|
|
155
|
-
totalRows: 0,
|
|
156
|
-
yearRange: { min: 0, max: 0 },
|
|
157
|
-
categoryBreakdown: { tweak: 0, feature: 0, process: 0 },
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const years = rows.map((row) => row.year)
|
|
162
|
-
const categoryBreakdown = rows.reduce(
|
|
163
|
-
(acc, row) => {
|
|
164
|
-
acc[row.category]++
|
|
165
|
-
return acc
|
|
166
|
-
},
|
|
167
|
-
{ tweak: 0, feature: 0, process: 0 } as Record<string, number>,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
totalRows: rows.length,
|
|
172
|
-
yearRange: {
|
|
173
|
-
min: Math.min(...years),
|
|
174
|
-
max: Math.max(...years),
|
|
175
|
-
},
|
|
176
|
-
categoryBreakdown,
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
package/src/csv.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from "fs"
|
|
2
|
-
import { AnalyzedCommit, CSVRow } from "./types"
|
|
3
|
-
|
|
4
|
-
export class CSVService {
|
|
5
|
-
static generateCSV(commits: AnalyzedCommit[]): string {
|
|
6
|
-
const headers = "year,category,summary,description"
|
|
7
|
-
const rows = commits.map((commit) => this.formatRow(commit))
|
|
8
|
-
|
|
9
|
-
return [headers, ...rows].join("\n")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
static exportToFile(commits: AnalyzedCommit[], filename: string): void {
|
|
13
|
-
const csvContent = this.generateCSV(commits)
|
|
14
|
-
writeFileSync(filename, csvContent, "utf8")
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
private static formatRow(commit: AnalyzedCommit): string {
|
|
18
|
-
const row: CSVRow = {
|
|
19
|
-
year: commit.year,
|
|
20
|
-
category: commit.analysis.category,
|
|
21
|
-
summary: commit.analysis.summary,
|
|
22
|
-
description: commit.analysis.description,
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return [
|
|
26
|
-
row.year,
|
|
27
|
-
this.escapeCsvField(row.category),
|
|
28
|
-
this.escapeCsvField(row.summary),
|
|
29
|
-
this.escapeCsvField(row.description),
|
|
30
|
-
].join(",")
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private static escapeCsvField(field: string): string {
|
|
34
|
-
if (field.includes(",") || field.includes('"') || field.includes("\n")) {
|
|
35
|
-
return `"${field.replace(/"/g, '""')}"`
|
|
36
|
-
}
|
|
37
|
-
return field
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
package/src/errors.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export class CommitAnalyzerError extends Error {
|
|
2
|
-
constructor(
|
|
3
|
-
message: string,
|
|
4
|
-
public readonly code: string,
|
|
5
|
-
) {
|
|
6
|
-
super(message)
|
|
7
|
-
this.name = "CommitAnalyzerError"
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class GitError extends CommitAnalyzerError {
|
|
12
|
-
constructor(message: string) {
|
|
13
|
-
super(message, "GIT_ERROR")
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class LLMError extends CommitAnalyzerError {
|
|
18
|
-
constructor(message: string) {
|
|
19
|
-
super(message, "LLM_ERROR")
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class ValidationError extends CommitAnalyzerError {
|
|
24
|
-
constructor(message: string) {
|
|
25
|
-
super(message, "VALIDATION_ERROR")
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class FileError extends CommitAnalyzerError {
|
|
30
|
-
constructor(message: string) {
|
|
31
|
-
super(message, "FILE_ERROR")
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function handleError(error: unknown): never {
|
|
36
|
-
if (error instanceof CommitAnalyzerError) {
|
|
37
|
-
console.error(`Error [${error.code}]: ${error.message}`)
|
|
38
|
-
process.exit(1)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (error instanceof Error) {
|
|
42
|
-
console.error(`Unexpected error: ${error.message}`)
|
|
43
|
-
process.exit(1)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
console.error("Unknown error occurred")
|
|
47
|
-
process.exit(1)
|
|
48
|
-
}
|
|
49
|
-
|
package/src/git.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { execSync } from "child_process"
|
|
2
|
-
import { CommitInfo } from "./types"
|
|
3
|
-
|
|
4
|
-
export class GitService {
|
|
5
|
-
static async getCommitInfo(hash: string): Promise<CommitInfo> {
|
|
6
|
-
try {
|
|
7
|
-
const showOutput = execSync(
|
|
8
|
-
`git show --format="%H|%s|%ci" --no-patch "${hash}"`,
|
|
9
|
-
{
|
|
10
|
-
encoding: "utf8",
|
|
11
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
12
|
-
},
|
|
13
|
-
).trim()
|
|
14
|
-
|
|
15
|
-
const [fullHash, message, dateStr] = showOutput.split("|")
|
|
16
|
-
const date = new Date(dateStr)
|
|
17
|
-
const year = date.getFullYear()
|
|
18
|
-
|
|
19
|
-
const diff = execSync(`git show "${hash}"`, {
|
|
20
|
-
encoding: "utf8",
|
|
21
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
22
|
-
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large diffs
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
hash: fullHash,
|
|
27
|
-
message,
|
|
28
|
-
date,
|
|
29
|
-
diff,
|
|
30
|
-
year,
|
|
31
|
-
}
|
|
32
|
-
} catch (error) {
|
|
33
|
-
throw new Error(
|
|
34
|
-
`Failed to get commit info for ${hash}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
static validateCommitHash(hash: string): boolean {
|
|
40
|
-
try {
|
|
41
|
-
execSync(`git rev-parse --verify "${hash}"`, {
|
|
42
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
43
|
-
})
|
|
44
|
-
return true
|
|
45
|
-
} catch {
|
|
46
|
-
return false
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
static isGitRepository(): boolean {
|
|
51
|
-
try {
|
|
52
|
-
execSync("git rev-parse --git-dir", {
|
|
53
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
54
|
-
})
|
|
55
|
-
return true
|
|
56
|
-
} catch {
|
|
57
|
-
return false
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
static getCurrentUserEmail(): string {
|
|
62
|
-
try {
|
|
63
|
-
return execSync("git config user.email", {
|
|
64
|
-
encoding: "utf8",
|
|
65
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
66
|
-
}).trim()
|
|
67
|
-
} catch (error) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
`Failed to get current user email: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
static getCurrentUserName(): string {
|
|
75
|
-
try {
|
|
76
|
-
return execSync("git config user.name", {
|
|
77
|
-
encoding: "utf8",
|
|
78
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
79
|
-
}).trim()
|
|
80
|
-
} catch (error) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
`Failed to get current user name: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
static getUserAuthoredCommits(author?: string, limit?: number): string[] {
|
|
88
|
-
try {
|
|
89
|
-
const authorFilter = author || this.getCurrentUserEmail()
|
|
90
|
-
const limitFlag = limit ? `--max-count=${limit}` : ""
|
|
91
|
-
|
|
92
|
-
const output = execSync(
|
|
93
|
-
`git log --author="${authorFilter}" --format="%H" --no-merges ${limitFlag}`,
|
|
94
|
-
{
|
|
95
|
-
encoding: "utf8",
|
|
96
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
97
|
-
},
|
|
98
|
-
).trim()
|
|
99
|
-
|
|
100
|
-
if (!output) {
|
|
101
|
-
return []
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return output.split("\n").filter((hash) => hash.length > 0)
|
|
105
|
-
} catch (error) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
`Failed to get user authored commits: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|