commit-analyzer 1.1.2 → 1.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commit-analyzer",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Analyze git commits and generate categories, summaries, and descriptions for each commit. Optionally generate a yearly breakdown report of your commit history.",
5
5
  "main": "dist/main.ts",
6
6
  "bin": {
@@ -2,6 +2,14 @@ import { AnalyzedCommit } from "./analyzed-commit"
2
2
  import { Category, CategoryType } from "./category"
3
3
  import { DateRange } from "./date-range"
4
4
 
5
+ export type TimePeriod =
6
+ | "hourly"
7
+ | "daily"
8
+ | "weekly"
9
+ | "monthly"
10
+ | "quarterly"
11
+ | "yearly"
12
+
5
13
  /**
6
14
  * Statistics for analyzed commits
7
15
  */
@@ -151,27 +159,31 @@ export class ReportGenerationService {
151
159
  /**
152
160
  * Determines the appropriate time period for summaries based on date range
153
161
  */
154
- determineTimePeriod(commits: AnalyzedCommit[]): 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly' {
155
- if (commits.length === 0) return 'yearly'
162
+ determineTimePeriod(commits: AnalyzedCommit[]): TimePeriod {
163
+ if (commits.length === 0) return "yearly"
164
+
165
+ const dates = commits.map((c) => c.getDate())
166
+ const minDate = new Date(Math.min(...dates.map((d) => d.getTime())))
167
+ const maxDate = new Date(Math.max(...dates.map((d) => d.getTime())))
156
168
 
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
169
  const diffInMilliseconds = maxDate.getTime() - minDate.getTime()
162
170
  const diffInDays = diffInMilliseconds / (1000 * 60 * 60 * 24)
163
171
 
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'
172
+ if (diffInDays <= 1) return "hourly"
173
+ if (diffInDays <= 7) return "daily"
174
+ if (diffInDays <= 31) return "weekly"
175
+ if (diffInDays <= 93) return "monthly" // ~3 months
176
+ if (diffInDays <= 365) return "quarterly" // ~3 months
177
+ return "yearly"
169
178
  }
170
179
 
171
180
  /**
172
181
  * Groups commits by the appropriate time period
173
182
  */
174
- groupByTimePeriod(commits: AnalyzedCommit[], period: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'): Map<string, AnalyzedCommit[]> {
183
+ groupByTimePeriod(
184
+ commits: AnalyzedCommit[],
185
+ period: TimePeriod,
186
+ ): Map<string, AnalyzedCommit[]> {
175
187
  const grouped = new Map<string, AnalyzedCommit[]>()
176
188
 
177
189
  for (const commit of commits) {
@@ -179,19 +191,22 @@ export class ReportGenerationService {
179
191
  let key: string
180
192
 
181
193
  switch (period) {
182
- case 'daily':
194
+ case "hourly":
195
+ key = this.formatHourlyKey(date)
196
+ break
197
+ case "daily":
183
198
  key = this.formatDailyKey(date)
184
199
  break
185
- case 'weekly':
200
+ case "weekly":
186
201
  key = this.formatWeeklyKey(date)
187
202
  break
188
- case 'monthly':
203
+ case "monthly":
189
204
  key = this.formatMonthlyKey(date)
190
205
  break
191
- case 'quarterly':
206
+ case "quarterly":
192
207
  key = this.formatQuarterlyKey(date)
193
208
  break
194
- case 'yearly':
209
+ case "yearly":
195
210
  default:
196
211
  key = date.getFullYear().toString()
197
212
  break
@@ -206,15 +221,24 @@ export class ReportGenerationService {
206
221
  return grouped
207
222
  }
208
223
 
224
+ private formatHourlyKey(date: Date): string {
225
+ const hour = date.getHours()
226
+ const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour
227
+ const ampm = hour < 12 ? "AM" : "PM"
228
+ return `${displayHour}:00 ${ampm}`
229
+ }
230
+
209
231
  private formatDailyKey(date: Date): string {
210
232
  const year = date.getFullYear()
211
233
  const month = date.getMonth() + 1
212
234
  const day = date.getDate()
213
235
  const hour = date.getHours()
214
236
 
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`
237
+ if (hour < 12)
238
+ return `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")} Morning`
239
+ if (hour < 17)
240
+ return `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")} Afternoon`
241
+ return `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")} Evening`
218
242
  }
219
243
 
220
244
  private formatWeeklyKey(date: Date): string {
@@ -223,15 +247,27 @@ export class ReportGenerationService {
223
247
  const endOfWeek = new Date(startOfWeek)
224
248
  endOfWeek.setDate(startOfWeek.getDate() + 6)
225
249
 
226
- const formatDate = (d: Date) =>
227
- `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`
250
+ const formatDate = (d: Date) =>
251
+ `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`
228
252
 
229
253
  return `Week of ${formatDate(startOfWeek)} to ${formatDate(endOfWeek)}`
230
254
  }
231
255
 
232
256
  private formatMonthlyKey(date: Date): string {
233
- const months = ['January', 'February', 'March', 'April', 'May', 'June',
234
- 'July', 'August', 'September', 'October', 'November', 'December']
257
+ const months = [
258
+ "January",
259
+ "February",
260
+ "March",
261
+ "April",
262
+ "May",
263
+ "June",
264
+ "July",
265
+ "August",
266
+ "September",
267
+ "October",
268
+ "November",
269
+ "December",
270
+ ]
235
271
  return `${months[date.getMonth()]} ${date.getFullYear()}`
236
272
  }
237
273
 
@@ -241,45 +277,160 @@ export class ReportGenerationService {
241
277
  }
242
278
 
243
279
  /**
244
- * Converts analyzed commits to CSV string format for LLM consumption
280
+ * Converts analyzed commits to CSV string format for LLM consumption with enhanced context
245
281
  */
246
282
  convertToCSVString(commits: AnalyzedCommit[]): string {
247
- const header = "year,category,summary,description"
283
+ const header = "year,category,summary,description,commit_count,date_range"
284
+
285
+ // Group commits by year and category for context
286
+ const contextMap = new Map<string, { count: number; dates: Date[] }>()
287
+
248
288
  const rows = commits.map((commit) => {
249
289
  const analysis = commit.getAnalysis()
290
+ const key = `${commit.getYear()}-${analysis.getCategory().getValue()}`
291
+
292
+ if (!contextMap.has(key)) {
293
+ contextMap.set(key, { count: 0, dates: [] })
294
+ }
295
+
296
+ const context = contextMap.get(key)!
297
+ context.count++
298
+ context.dates.push(commit.getDate())
299
+
300
+ const dateRange =
301
+ context.dates.length > 1
302
+ ? `${this.formatDate(Math.min(...context.dates.map((d) => d.getTime())))} to ${this.formatDate(Math.max(...context.dates.map((d) => d.getTime())))}`
303
+ : this.formatDate(commit.getDate())
304
+
250
305
  return [
251
306
  commit.getYear().toString(),
252
307
  this.escapeCsvField(analysis.getCategory().getValue()),
253
308
  this.escapeCsvField(analysis.getSummary()),
254
309
  this.escapeCsvField(analysis.getDescription()),
310
+ context.count.toString(),
311
+ this.escapeCsvField(dateRange),
255
312
  ].join(",")
256
313
  })
257
314
 
258
315
  return [header, ...rows].join("\n")
259
316
  }
260
317
 
318
+ private formatDate(date: Date | number): string {
319
+ const d = new Date(date)
320
+ return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`
321
+ }
322
+
261
323
  /**
262
- * Converts grouped commits to CSV with time period information
324
+ * Converts grouped commits to CSV with time period information and enhanced context
263
325
  */
264
- convertGroupedToCSV(groupedCommits: Map<string, AnalyzedCommit[]>, period: string): string {
265
- const header = `${period},category,summary,description`
326
+ convertGroupedToCSV(
327
+ groupedCommits: Map<string, AnalyzedCommit[]>,
328
+ period: string,
329
+ ): string {
330
+ const header = `${period},category,summary,description,commit_count,similar_commits`
266
331
  const rows: string[] = []
267
-
332
+
268
333
  for (const [timePeriod, commits] of groupedCommits) {
334
+ // Group commits by category within the time period for context
335
+ const categoryGroups = new Map<string, AnalyzedCommit[]>()
336
+
337
+ for (const commit of commits) {
338
+ const category = commit.getAnalysis().getCategory().getValue()
339
+ if (!categoryGroups.has(category)) {
340
+ categoryGroups.set(category, [])
341
+ }
342
+ categoryGroups.get(category)!.push(commit)
343
+ }
344
+
345
+ // Add context about similar commits in the same period and category
269
346
  for (const commit of commits) {
270
347
  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(","))
348
+ const category = analysis.getCategory().getValue()
349
+ const similarCommits = categoryGroups.get(category)!
350
+
351
+ // Find similar summaries in the same category
352
+ const similarSummaries = similarCommits
353
+ .filter((c) => c !== commit)
354
+ .map((c) => c.getAnalysis().getSummary())
355
+ .filter((summary) =>
356
+ this.isSimilarSummary(analysis.getSummary(), summary),
357
+ )
358
+ .slice(0, 3) // Limit to 3 similar items
359
+
360
+ rows.push(
361
+ [
362
+ this.escapeCsvField(timePeriod),
363
+ this.escapeCsvField(category),
364
+ this.escapeCsvField(analysis.getSummary()),
365
+ this.escapeCsvField(analysis.getDescription()),
366
+ similarCommits.length.toString(),
367
+ this.escapeCsvField(similarSummaries.join("; ")),
368
+ ].join(","),
369
+ )
277
370
  }
278
371
  }
279
372
 
280
373
  return [header, ...rows].join("\n")
281
374
  }
282
375
 
376
+ /**
377
+ * Determines if two summaries are similar based on common keywords
378
+ */
379
+ private isSimilarSummary(summary1: string, summary2: string): boolean {
380
+ const keywords1 = this.extractKeywords(summary1)
381
+ const keywords2 = this.extractKeywords(summary2)
382
+
383
+ // Check if they share significant keywords (at least 2 common words)
384
+ const commonKeywords = keywords1.filter((word) => keywords2.includes(word))
385
+ return commonKeywords.length >= 2
386
+ }
387
+
388
+ /**
389
+ * Extracts meaningful keywords from a summary for similarity detection
390
+ */
391
+ private extractKeywords(summary: string): string[] {
392
+ // Remove common stopwords and extract meaningful terms
393
+ const stopwords = new Set([
394
+ "the",
395
+ "a",
396
+ "an",
397
+ "and",
398
+ "or",
399
+ "but",
400
+ "in",
401
+ "on",
402
+ "at",
403
+ "to",
404
+ "for",
405
+ "of",
406
+ "with",
407
+ "by",
408
+ "is",
409
+ "are",
410
+ "was",
411
+ "were",
412
+ "be",
413
+ "been",
414
+ "have",
415
+ "has",
416
+ "had",
417
+ "do",
418
+ "does",
419
+ "did",
420
+ "will",
421
+ "would",
422
+ "could",
423
+ "should",
424
+ ])
425
+
426
+ return summary
427
+ .toLowerCase()
428
+ .replace(/[^\w\s]/g, " ")
429
+ .split(/\s+/)
430
+ .filter((word) => word.length > 2 && !stopwords.has(word))
431
+ .slice(0, 5) // Take first 5 meaningful words
432
+ }
433
+
283
434
  /**
284
435
  * Escape CSV fields that contain commas, quotes, or newlines
285
436
  */
@@ -24,7 +24,7 @@ export interface CLIOptions {
24
24
  }
25
25
 
26
26
  export class CLIApplication {
27
- private static readonly VERSION = "1.1.2"
27
+ private static readonly VERSION = "1.1.4"
28
28
  private static readonly DEFAULT_COMMITS_OUTPUT_FILE = "results/commits.csv"
29
29
  private static readonly DEFAULT_REPORT_OUTPUT_FILE = "results/report.md"
30
30
 
@@ -271,9 +271,8 @@ export class CLIApplication {
271
271
  }
272
272
 
273
273
  private getReportPathFromCsv(csvPath: string): string {
274
- if (csvPath.endsWith(".csv")) {
275
- return csvPath.replace(/\.csv$/, ".md")
276
- }
277
- return csvPath + ".md"
274
+ const lastSlash = csvPath.lastIndexOf("/")
275
+ const directory = lastSlash >= 0 ? csvPath.substring(0, lastSlash + 1) : ""
276
+ return directory + "report.md"
278
277
  }
279
278
  }
@@ -243,9 +243,11 @@ ${csvContent}
243
243
  INSTRUCTIONS:
244
244
  1. Group the data by year (descending order, most recent first)
245
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
246
+ 3. Use the 'commit_count' and 'date_range' columns to understand the scope and timeline of work
247
+ 4. Consolidate similar items within each category to create readable summaries
248
+ 5. Focus on what was accomplished rather than individual commit details
249
+ 6. Use clear, professional language appropriate for stakeholders
250
+ 7. Pay attention to recurring themes and patterns across commits
249
251
 
250
252
  CATEGORY MAPPING:
251
253
  - "feature" → "Features" section
@@ -253,12 +255,15 @@ CATEGORY MAPPING:
253
255
  - "tweak" → "Tweaks & Bug Fixes" section
254
256
 
255
257
  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
258
+ - FIRST: Extract common themes and keywords from commit summaries within each category
259
+ - SECOND: Identify and merge duplicate or highly similar work items (e.g., multiple "fix auth bug" commits become "resolved authentication issues")
260
+ - Group similar features together by theme (e.g., "authentication system improvements", "payment processing enhancements")
261
+ - Combine related bug fixes by area/system (e.g., "resolved 8 authentication issues", "fixed 5 database connection problems")
262
+ - Summarize process changes by theme (e.g., "CI/CD pipeline enhancements", "testing infrastructure improvements")
263
+ - Use bullet points for individual consolidated items within categories
260
264
  - Aim for 3-7 bullet points per category per year
261
265
  - Include specific numbers when relevant (e.g., "15 bug fixes", "3 new features")
266
+ - Avoid listing near-identical items separately - consolidate them into meaningful groups
262
267
 
263
268
  OUTPUT FORMAT:
264
269
  Generate yearly summary sections with this exact structure (DO NOT include the main title or commit analysis section):
@@ -290,6 +295,16 @@ QUALITY REQUIREMENTS:
290
295
  - Avoid technical jargon where possible
291
296
  - Ensure each bullet point represents meaningful work
292
297
  - Make the report valuable for both technical and non-technical readers
298
+ - Focus on business impact and user value rather than technical implementation details
299
+ - When consolidating, preserve the most important aspects from similar commits
300
+ - Use progressive disclosure: start with high-level themes, then add specific details
301
+
302
+ CONTEXT ANALYSIS:
303
+ Before consolidating, analyze the commit data for:
304
+ 1. Common file patterns or system areas being modified
305
+ 2. Recurring keywords in commit messages that indicate related work
306
+ 3. Sequential commits that build upon each other
307
+ 4. Bug fixes that address the same underlying issue
293
308
 
294
309
  Generate the markdown report now:`
295
310
  }
@@ -306,10 +321,12 @@ ${csvContent}
306
321
  INSTRUCTIONS:
307
322
  1. Group the data by ${periodDisplayName} (descending order, most recent first)
308
323
  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
324
+ 3. Use the 'commit_count' and 'similar_commits' columns to understand related work and consolidation opportunities
325
+ 4. Consolidate similar items within each category to create readable summaries
326
+ 5. Focus on what was accomplished rather than individual commit details
327
+ 6. Use clear, professional language appropriate for stakeholders
328
+ 7. Only include sections for time periods that have commits
329
+ 8. Pay attention to recurring themes and patterns across commits
313
330
 
314
331
  CATEGORY MAPPING:
315
332
  - "feature" → "Features" section
@@ -317,12 +334,15 @@ CATEGORY MAPPING:
317
334
  - "tweak" → "Tweaks & Bug Fixes" section
318
335
 
319
336
  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
337
+ - FIRST: Extract common themes and keywords from commit summaries within each category
338
+ - SECOND: Identify and merge duplicate or highly similar work items (e.g., multiple "fix auth bug" commits become "resolved authentication issues")
339
+ - Group similar features together by theme (e.g., "authentication system improvements", "payment processing enhancements")
340
+ - Combine related bug fixes by area/system (e.g., "resolved 8 authentication issues", "fixed 5 database connection problems")
341
+ - Summarize process changes by theme (e.g., "CI/CD pipeline enhancements", "testing infrastructure improvements")
342
+ - Use bullet points for individual consolidated items within categories
324
343
  - Aim for 3-7 bullet points per category per ${periodDisplayName.toLowerCase()}
325
344
  - Include specific numbers when relevant (e.g., "15 bug fixes", "3 new features")
345
+ - Avoid listing near-identical items separately - consolidate them into meaningful groups
326
346
 
327
347
  OUTPUT FORMAT:
328
348
  Generate ${periodDisplayName.toLowerCase()} summary sections with this exact structure (DO NOT include the main title or commit analysis section):
@@ -354,6 +374,16 @@ QUALITY REQUIREMENTS:
354
374
  - Avoid technical jargon where possible
355
375
  - Ensure each bullet point represents meaningful work
356
376
  - Make the report valuable for both technical and non-technical readers
377
+ - Focus on business impact and user value rather than technical implementation details
378
+ - When consolidating, preserve the most important aspects from similar commits
379
+ - Use progressive disclosure: start with high-level themes, then add specific details
380
+
381
+ CONTEXT ANALYSIS:
382
+ Before consolidating, analyze the commit data for:
383
+ 1. Common file patterns or system areas being modified
384
+ 2. Recurring keywords in commit messages that indicate related work
385
+ 3. Sequential commits that build upon each other
386
+ 4. Bug fixes that address the same underlying issue
357
387
 
358
388
  Generate the markdown report now:`
359
389
  }