commit-analyzer 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.claude/settings.local.json +11 -1
  2. package/README.md +33 -2
  3. package/commits.csv +2 -0
  4. package/eslint.config.mts +45 -0
  5. package/package.json +17 -9
  6. package/src/1.domain/analysis.ts +93 -0
  7. package/src/1.domain/analyzed-commit.ts +97 -0
  8. package/src/1.domain/application-error.ts +32 -0
  9. package/src/1.domain/category.ts +52 -0
  10. package/src/1.domain/commit-analysis-service.ts +92 -0
  11. package/src/1.domain/commit-hash.ts +40 -0
  12. package/src/1.domain/commit.ts +99 -0
  13. package/src/1.domain/date-formatting-service.ts +81 -0
  14. package/src/1.domain/date-range.ts +76 -0
  15. package/src/1.domain/report-generation-service.ts +292 -0
  16. package/src/2.application/analyze-commits.usecase.ts +307 -0
  17. package/src/2.application/generate-report.usecase.ts +204 -0
  18. package/src/2.application/llm-service.ts +54 -0
  19. package/src/2.application/resume-analysis.usecase.ts +123 -0
  20. package/src/3.presentation/analysis-repository.interface.ts +27 -0
  21. package/src/3.presentation/analyze-command.ts +128 -0
  22. package/src/3.presentation/cli-application.ts +255 -0
  23. package/src/3.presentation/command-handler.interface.ts +4 -0
  24. package/src/3.presentation/commit-analysis-controller.ts +101 -0
  25. package/src/3.presentation/commit-repository.interface.ts +47 -0
  26. package/src/3.presentation/console-formatter.ts +129 -0
  27. package/src/3.presentation/progress-repository.interface.ts +49 -0
  28. package/src/3.presentation/report-command.ts +50 -0
  29. package/src/3.presentation/resume-command.ts +59 -0
  30. package/src/3.presentation/storage-repository.interface.ts +33 -0
  31. package/src/3.presentation/storage-service.interface.ts +32 -0
  32. package/src/3.presentation/version-control-service.interface.ts +41 -0
  33. package/src/4.infrastructure/cache-service.ts +271 -0
  34. package/src/4.infrastructure/cached-analysis-repository.ts +46 -0
  35. package/src/4.infrastructure/claude-llm-adapter.ts +124 -0
  36. package/src/4.infrastructure/csv-service.ts +206 -0
  37. package/src/4.infrastructure/file-storage-repository.ts +108 -0
  38. package/src/4.infrastructure/file-system-storage-adapter.ts +87 -0
  39. package/src/4.infrastructure/gemini-llm-adapter.ts +46 -0
  40. package/src/4.infrastructure/git-adapter.ts +116 -0
  41. package/src/4.infrastructure/git-commit-repository.ts +85 -0
  42. package/src/4.infrastructure/json-progress-tracker.ts +182 -0
  43. package/src/4.infrastructure/llm-adapter-factory.ts +26 -0
  44. package/src/4.infrastructure/llm-adapter.ts +455 -0
  45. package/src/4.infrastructure/llm-analysis-repository.ts +38 -0
  46. package/src/4.infrastructure/openai-llm-adapter.ts +57 -0
  47. package/src/di.ts +108 -0
  48. package/src/main.ts +63 -0
  49. package/src/utils/app-paths.ts +36 -0
  50. package/src/utils/concurrency.ts +81 -0
  51. package/src/utils.ts +77 -0
  52. package/tsconfig.json +7 -1
  53. package/src/cli.ts +0 -170
  54. package/src/csv-reader.ts +0 -180
  55. package/src/csv.ts +0 -40
  56. package/src/errors.ts +0 -49
  57. package/src/git.ts +0 -112
  58. package/src/index.ts +0 -395
  59. package/src/llm.ts +0 -396
  60. package/src/progress.ts +0 -84
  61. package/src/report-generator.ts +0 -286
  62. package/src/types.ts +0 -24
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
-