commit-analyzer 1.1.4 → 1.1.6

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 (55) hide show
  1. package/README.md +164 -82
  2. package/dist/main.ts +0 -0
  3. package/package.json +2 -1
  4. package/.claude/settings.local.json +0 -23
  5. package/commits.csv +0 -2
  6. package/csv-to-report-prompt.md +0 -97
  7. package/eslint.config.mts +0 -45
  8. package/prompt.md +0 -69
  9. package/src/1.domain/analysis.ts +0 -93
  10. package/src/1.domain/analyzed-commit.ts +0 -97
  11. package/src/1.domain/application-error.ts +0 -32
  12. package/src/1.domain/category.ts +0 -52
  13. package/src/1.domain/commit-analysis-service.ts +0 -92
  14. package/src/1.domain/commit-hash.ts +0 -40
  15. package/src/1.domain/commit.ts +0 -99
  16. package/src/1.domain/date-formatting-service.ts +0 -81
  17. package/src/1.domain/date-range.ts +0 -76
  18. package/src/1.domain/report-generation-service.ts +0 -443
  19. package/src/2.application/analyze-commits.usecase.ts +0 -307
  20. package/src/2.application/generate-report.usecase.ts +0 -209
  21. package/src/2.application/llm-service.ts +0 -54
  22. package/src/2.application/resume-analysis.usecase.ts +0 -123
  23. package/src/3.presentation/analysis-repository.interface.ts +0 -27
  24. package/src/3.presentation/analyze-command.ts +0 -128
  25. package/src/3.presentation/cli-application.ts +0 -278
  26. package/src/3.presentation/command-handler.interface.ts +0 -4
  27. package/src/3.presentation/commit-analysis-controller.ts +0 -101
  28. package/src/3.presentation/commit-repository.interface.ts +0 -47
  29. package/src/3.presentation/console-formatter.ts +0 -129
  30. package/src/3.presentation/progress-repository.interface.ts +0 -49
  31. package/src/3.presentation/report-command.ts +0 -50
  32. package/src/3.presentation/resume-command.ts +0 -59
  33. package/src/3.presentation/storage-repository.interface.ts +0 -33
  34. package/src/3.presentation/storage-service.interface.ts +0 -32
  35. package/src/3.presentation/version-control-service.interface.ts +0 -46
  36. package/src/4.infrastructure/cache-service.ts +0 -271
  37. package/src/4.infrastructure/cached-analysis-repository.ts +0 -46
  38. package/src/4.infrastructure/claude-llm-adapter.ts +0 -124
  39. package/src/4.infrastructure/csv-service.ts +0 -252
  40. package/src/4.infrastructure/file-storage-repository.ts +0 -108
  41. package/src/4.infrastructure/file-system-storage-adapter.ts +0 -87
  42. package/src/4.infrastructure/gemini-llm-adapter.ts +0 -46
  43. package/src/4.infrastructure/git-adapter.ts +0 -143
  44. package/src/4.infrastructure/git-commit-repository.ts +0 -85
  45. package/src/4.infrastructure/json-progress-tracker.ts +0 -182
  46. package/src/4.infrastructure/llm-adapter-factory.ts +0 -26
  47. package/src/4.infrastructure/llm-adapter.ts +0 -485
  48. package/src/4.infrastructure/llm-analysis-repository.ts +0 -38
  49. package/src/4.infrastructure/openai-llm-adapter.ts +0 -57
  50. package/src/di.ts +0 -109
  51. package/src/main.ts +0 -63
  52. package/src/utils/app-paths.ts +0 -36
  53. package/src/utils/concurrency.ts +0 -81
  54. package/src/utils.ts +0 -77
  55. package/tsconfig.json +0 -25
@@ -1,93 +0,0 @@
1
- import { Category } from "./category"
2
-
3
- /**
4
- * Domain entity representing the analysis of a commit
5
- */
6
- export class Analysis {
7
- private static readonly MAX_SUMMARY_LENGTH = 80
8
- private static readonly MIN_DESCRIPTION_LENGTH = 10
9
- private readonly category: Category
10
- private readonly summary: string
11
- private readonly description: string
12
-
13
- constructor(params: {
14
- category: Category
15
- summary: string
16
- description: string
17
- }) {
18
- const { category, summary, description } = params
19
- this.category = category
20
- this.summary = summary
21
- this.description = description
22
-
23
- if (!summary || summary.trim().length === 0) {
24
- throw new Error("Summary cannot be empty")
25
- }
26
-
27
- if (!description || description.trim().length === 0) {
28
- throw new Error("Description cannot be empty")
29
- }
30
-
31
- if (summary.length > Analysis.MAX_SUMMARY_LENGTH) {
32
- throw new Error(
33
- `Summary cannot exceed ${Analysis.MAX_SUMMARY_LENGTH} characters`,
34
- )
35
- }
36
-
37
- if (description.length < Analysis.MIN_DESCRIPTION_LENGTH) {
38
- throw new Error(
39
- `Description must be at least ${Analysis.MIN_DESCRIPTION_LENGTH} characters`,
40
- )
41
- }
42
- }
43
-
44
- getCategory(): Category {
45
- return this.category
46
- }
47
-
48
- getSummary(): string {
49
- return this.summary
50
- }
51
-
52
- getDescription(): string {
53
- return this.description
54
- }
55
-
56
- getSummaryTruncated(): string {
57
- return this.summary.length > Analysis.MAX_SUMMARY_LENGTH
58
- ? this.summary.substring(0, Analysis.MAX_SUMMARY_LENGTH - 3) + "..."
59
- : this.summary
60
- }
61
-
62
- isFeatureAnalysis(): boolean {
63
- return this.category.isFeature()
64
- }
65
-
66
- isTweakAnalysis(): boolean {
67
- return this.category.isTweak()
68
- }
69
-
70
- isProcessAnalysis(): boolean {
71
- return this.category.isProcess()
72
- }
73
-
74
- equals(other: Analysis): boolean {
75
- return (
76
- this.category.equals(other.category) &&
77
- this.summary === other.summary &&
78
- this.description === other.description
79
- )
80
- }
81
-
82
- toPlainObject(): {
83
- category: string
84
- summary: string
85
- description: string
86
- } {
87
- return {
88
- category: this.category.getValue(),
89
- summary: this.summary,
90
- description: this.description,
91
- }
92
- }
93
- }
@@ -1,97 +0,0 @@
1
- import { Analysis } from "./analysis"
2
- import { Commit } from "./commit"
3
- import { CommitHash } from "./commit-hash"
4
-
5
- /**
6
- * Domain entity representing a commit with its analysis
7
- */
8
- export class AnalyzedCommit {
9
- constructor(
10
- private readonly commit: Commit,
11
- private readonly analysis: Analysis,
12
- ) {
13
- if (!commit) {
14
- throw new Error("Commit is required")
15
- }
16
-
17
- if (!analysis) {
18
- throw new Error("Analysis is required")
19
- }
20
- }
21
-
22
- getCommit(): Commit {
23
- return this.commit
24
- }
25
-
26
- getAnalysis(): Analysis {
27
- return this.analysis
28
- }
29
-
30
- getHash(): CommitHash {
31
- return this.commit.getHash()
32
- }
33
-
34
- getMessage(): string {
35
- return this.commit.getMessage()
36
- }
37
-
38
- getDate(): Date {
39
- return this.commit.getDate()
40
- }
41
-
42
- getYear(): number {
43
- return this.commit.getYear()
44
- }
45
-
46
- getShortHash(length: number = 8): string {
47
- return this.commit.getShortHash(length)
48
- }
49
-
50
- isLargeChange(): boolean {
51
- return this.commit.isLargeChange()
52
- }
53
-
54
- equals(other: AnalyzedCommit): boolean {
55
- return (
56
- this.commit.equals(other.commit) && this.analysis.equals(other.analysis)
57
- )
58
- }
59
-
60
- toCSVRow(): {
61
- timestamp: string
62
- category: string
63
- summary: string
64
- description: string
65
- } {
66
- return {
67
- timestamp: this.getDate().toISOString(),
68
- category: this.analysis.getCategory().getValue(),
69
- summary: this.analysis.getSummary(),
70
- description: this.analysis.getDescription(),
71
- }
72
- }
73
-
74
- toReportData(): {
75
- hash: string
76
- shortHash: string
77
- message: string
78
- date: Date
79
- year: number
80
- category: string
81
- summary: string
82
- description: string
83
- isLargeChange: boolean
84
- } {
85
- return {
86
- hash: this.commit.getHash().getValue(),
87
- shortHash: this.getShortHash(),
88
- message: this.getMessage(),
89
- date: this.getDate(),
90
- year: this.getYear(),
91
- category: this.analysis.getCategory().getValue(),
92
- summary: this.analysis.getSummary(),
93
- description: this.analysis.getDescription(),
94
- isLargeChange: this.isLargeChange(),
95
- }
96
- }
97
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * Base class for application errors
3
- */
4
- export abstract class ApplicationError extends Error {
5
- constructor(
6
- message: string,
7
- public readonly code: string,
8
- ) {
9
- super(message)
10
- this.name = "ApplicationError"
11
- }
12
- }
13
-
14
- /**
15
- * Error for validation failures
16
- */
17
- export class ValidationError extends ApplicationError {
18
- constructor(message: string) {
19
- super(message, "VALIDATION_ERROR")
20
- this.name = "ValidationError"
21
- }
22
- }
23
-
24
- /**
25
- * Error for not found resources
26
- */
27
- export class NotFoundError extends ApplicationError {
28
- constructor(resource: string, identifier: string) {
29
- super(`${resource} not found: ${identifier}`, "NOT_FOUND")
30
- this.name = "NotFoundError"
31
- }
32
- }
@@ -1,52 +0,0 @@
1
- /**
2
- * Value object for commit analysis category
3
- */
4
- export type CategoryType = "tweak" | "feature" | "process"
5
-
6
- export class Category {
7
- private static readonly VALID_CATEGORIES: CategoryType[] = ["tweak", "feature", "process"]
8
-
9
- private constructor(private readonly value: CategoryType) {}
10
-
11
- static create(category: string): Category {
12
- if (!category || typeof category !== 'string') {
13
- throw new Error('Category cannot be empty')
14
- }
15
-
16
- const normalizedCategory = category.toLowerCase().trim() as CategoryType
17
-
18
- if (!this.VALID_CATEGORIES.includes(normalizedCategory)) {
19
- throw new Error(`Invalid category: ${category}. Must be one of: ${this.VALID_CATEGORIES.join(', ')}`)
20
- }
21
-
22
- return new Category(normalizedCategory)
23
- }
24
-
25
- static fromType(category: CategoryType): Category {
26
- return new Category(category)
27
- }
28
-
29
- getValue(): CategoryType {
30
- return this.value
31
- }
32
-
33
- equals(other: Category): boolean {
34
- return this.value === other.value
35
- }
36
-
37
- toString(): string {
38
- return this.value
39
- }
40
-
41
- isFeature(): boolean {
42
- return this.value === "feature"
43
- }
44
-
45
- isTweak(): boolean {
46
- return this.value === "tweak"
47
- }
48
-
49
- isProcess(): boolean {
50
- return this.value === "process"
51
- }
52
- }
@@ -1,92 +0,0 @@
1
- import { IAnalysisRepository } from "@presentation/analysis-repository.interface"
2
- import { ICommitRepository } from "@presentation/commit-repository.interface"
3
-
4
- import { AnalyzedCommit } from "./analyzed-commit"
5
- import { Commit } from "./commit"
6
- import { CommitHash } from "./commit-hash"
7
-
8
- /**
9
- * Domain service for commit analysis operations
10
- */
11
- export class CommitAnalysisService {
12
- constructor(
13
- private readonly commitRepository: ICommitRepository,
14
- private readonly analysisRepository: IAnalysisRepository,
15
- ) {}
16
-
17
- /**
18
- * Analyzes a single commit
19
- */
20
- async analyzeCommit(hash: CommitHash): Promise<AnalyzedCommit> {
21
- const commit = await this.commitRepository.getByHash(hash)
22
- const analysis = await this.analysisRepository.analyze(commit)
23
-
24
- return new AnalyzedCommit(commit, analysis)
25
- }
26
-
27
- /**
28
- * Analyzes multiple commits
29
- */
30
- async analyzeCommits(hashes: CommitHash[]): Promise<AnalyzedCommit[]> {
31
- const analyzedCommits: AnalyzedCommit[] = []
32
-
33
- for (const hash of hashes) {
34
- try {
35
- const analyzedCommit = await this.analyzeCommit(hash)
36
- analyzedCommits.push(analyzedCommit)
37
- } catch (error) {
38
- // Re-throw the error - let the application layer handle logging
39
- throw new Error(
40
- `Failed to analyze commit ${hash.getShortHash()}: ${error instanceof Error ? error.message : String(error)}`,
41
- )
42
- }
43
- }
44
-
45
- return analyzedCommits
46
- }
47
-
48
- /**
49
- * Validates that all commit hashes exist
50
- */
51
- async validateCommits(
52
- hashes: CommitHash[],
53
- ): Promise<{ valid: CommitHash[]; invalid: CommitHash[] }> {
54
- const valid: CommitHash[] = []
55
- const invalid: CommitHash[] = []
56
-
57
- for (const hash of hashes) {
58
- const exists = await this.commitRepository.exists(hash)
59
- if (exists) {
60
- valid.push(hash)
61
- } else {
62
- invalid.push(hash)
63
- }
64
- }
65
-
66
- return { valid, invalid }
67
- }
68
-
69
- /**
70
- * Gets commits authored by the current user
71
- */
72
- async getCurrentUserCommits(params?: {
73
- limit?: number
74
- since?: string
75
- until?: string
76
- }): Promise<Commit[]> {
77
- const userEmail = await this.commitRepository.getCurrentUserEmail()
78
- return this.commitRepository.getByAuthor({
79
- authorEmail: userEmail,
80
- limit: params?.limit,
81
- since: params?.since,
82
- until: params?.until,
83
- })
84
- }
85
-
86
- /**
87
- * Checks if the analysis service is ready
88
- */
89
- async isAnalysisServiceReady(): Promise<boolean> {
90
- return this.analysisRepository.isAvailable()
91
- }
92
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * Value object for Git commit hash
3
- */
4
- export class CommitHash {
5
- private constructor(private readonly value: string) {}
6
-
7
- static create(hash: string): CommitHash {
8
- if (!hash || typeof hash !== 'string') {
9
- throw new Error('Commit hash cannot be empty')
10
- }
11
-
12
- // Git short hash is minimum 4 characters, full hash is 40
13
- if (hash.length < 4 || hash.length > 40) {
14
- throw new Error('Invalid commit hash length')
15
- }
16
-
17
- // Only hexadecimal characters allowed
18
- if (!/^[a-f0-9]+$/i.test(hash)) {
19
- throw new Error('Commit hash must contain only hexadecimal characters')
20
- }
21
-
22
- return new CommitHash(hash)
23
- }
24
-
25
- getValue(): string {
26
- return this.value
27
- }
28
-
29
- getShortHash(length: number = 8): string {
30
- return this.value.substring(0, Math.min(length, this.value.length))
31
- }
32
-
33
- equals(other: CommitHash): boolean {
34
- return this.value === other.value
35
- }
36
-
37
- toString(): string {
38
- return this.value
39
- }
40
- }
@@ -1,99 +0,0 @@
1
- import { CommitHash } from "./commit-hash"
2
-
3
- /**
4
- * Domain entity representing a Git commit
5
- */
6
- export class Commit {
7
- private readonly hash: CommitHash
8
- private readonly message: string
9
- private readonly date: Date
10
- private readonly diff: string
11
-
12
- constructor(params: {
13
- hash: CommitHash
14
- message: string
15
- date: Date
16
- diff: string
17
- }) {
18
- const { hash, message, date, diff } = params
19
- this.hash = hash
20
- this.message = message
21
- this.date = date
22
- this.diff = diff
23
- if (!message || message.trim().length === 0) {
24
- throw new Error("Commit message cannot be empty")
25
- }
26
-
27
- if (!date) {
28
- throw new Error("Commit date is required")
29
- }
30
-
31
- if (!diff) {
32
- throw new Error("Commit diff is required")
33
- }
34
- }
35
-
36
- getHash(): CommitHash {
37
- return this.hash
38
- }
39
-
40
- getMessage(): string {
41
- return this.message
42
- }
43
-
44
- getDate(): Date {
45
- return new Date(this.date)
46
- }
47
-
48
- getDiff(): string {
49
- return this.diff
50
- }
51
-
52
- getYear(): number {
53
- return this.date.getFullYear()
54
- }
55
-
56
- getShortHash(length: number = 8): string {
57
- return this.hash.getShortHash(length)
58
- }
59
-
60
- getDiffStats(): {
61
- additions: number
62
- deletions: number
63
- filesChanged: number
64
- } {
65
- const lines = this.diff.split("\n")
66
- let additions = 0
67
- let deletions = 0
68
- const filesChanged = new Set<string>()
69
-
70
- for (const line of lines) {
71
- if (line.startsWith("+") && !line.startsWith("+++")) {
72
- additions++
73
- } else if (line.startsWith("-") && !line.startsWith("---")) {
74
- deletions++
75
- } else if (line.startsWith("diff --git")) {
76
- // Extract filename from diff header
77
- const match = line.match(/diff --git a\/(.+) b\/(.+)/)
78
- if (match) {
79
- filesChanged.add(match[1])
80
- }
81
- }
82
- }
83
-
84
- return {
85
- additions,
86
- deletions,
87
- filesChanged: filesChanged.size,
88
- }
89
- }
90
-
91
- isLargeChange(): boolean {
92
- const stats = this.getDiffStats()
93
- return stats.additions + stats.deletions > 100 || stats.filesChanged > 10
94
- }
95
-
96
- equals(other: Commit): boolean {
97
- return this.hash.equals(other.hash)
98
- }
99
- }
@@ -1,81 +0,0 @@
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
- }
@@ -1,76 +0,0 @@
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
- }