@m3hti/commit-genie 3.0.0 → 3.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.md ADDED
@@ -0,0 +1,112 @@
1
+ # CLAUDE.md - CommitGenie
2
+
3
+ ## Project Overview
4
+
5
+ CommitGenie (`@m3hti/commit-genie`) is a Node.js CLI tool that generates intelligent Git commit messages by analyzing staged code changes. It uses rule-based pattern matching, AST analysis (via `@typescript-eslint/typescript-estree`), and optional AI integration to produce Conventional Commits-formatted messages.
6
+
7
+ ## Tech Stack
8
+
9
+ - **Language**: TypeScript 5.3 (strict mode)
10
+ - **Runtime**: Node.js >= 16
11
+ - **CLI Framework**: Commander.js
12
+ - **AST Parsing**: @typescript-eslint/typescript-estree
13
+ - **Testing**: Jest 30 + ts-jest
14
+ - **Build Target**: ES2020, compiled to CommonJS
15
+
16
+ ## Project Structure
17
+
18
+ ```
19
+ src/
20
+ ├── index.ts # CLI entry point (Commander setup)
21
+ ├── commands/ # Command handlers (generate, hook, config, stats)
22
+ ├── services/ # Core business logic (static service classes)
23
+ │ ├── gitService.ts # Git operations (status, diff, history)
24
+ │ ├── analyzerService.ts # Change analysis & message generation
25
+ │ ├── astAnalyzer.ts # AST parsing utilities
26
+ │ ├── astClassifier.ts # AST-based commit type classification
27
+ │ ├── astDiffEngine.ts # AST diff computation
28
+ │ ├── astExportAnalyzer.ts # Export tracking for breaking changes
29
+ │ ├── historyService.ts # Commit history learning & ticket detection
30
+ │ ├── configService.ts # .commitgenierc.json loading
31
+ │ ├── lintService.ts # Commit message linting
32
+ │ ├── spellService.ts # Spell checking
33
+ │ ├── splitService.ts # Commit splitting suggestions
34
+ │ ├── statsService.ts # Repository statistics
35
+ │ ├── hookService.ts # Git hook management
36
+ │ └── aiService.ts # Optional LLM integration
37
+ ├── data/
38
+ │ └── wordlist.ts # Bundled wordlist for spell check
39
+ ├── utils/
40
+ │ ├── filePatterns.ts # File type detection patterns
41
+ │ └── prompt.ts # Interactive CLI prompts (readline)
42
+ └── types/
43
+ └── index.ts # All TypeScript interfaces & types
44
+ ```
45
+
46
+ Test files are colocated with source files (`*.test.ts`).
47
+
48
+ ## Common Commands
49
+
50
+ ```bash
51
+ # Install dependencies
52
+ npm install
53
+
54
+ # Build (compile TypeScript to dist/)
55
+ npm run build
56
+
57
+ # Run in development mode
58
+ npm run dev
59
+
60
+ # Run tests
61
+ npm test
62
+
63
+ # Run tests in watch mode
64
+ npm run test:watch
65
+
66
+ # Run tests with coverage report
67
+ npm run test:coverage
68
+
69
+ # Watch mode for continuous compilation
70
+ npm run watch
71
+
72
+ # Run compiled version
73
+ npm start
74
+ ```
75
+
76
+ ## Architecture Notes
77
+
78
+ - **Service layer pattern**: All business logic lives in service classes with static methods (no instantiation needed).
79
+ - **No barrel exports**: Each file imports directly from its source, not via index re-exports.
80
+ - **Entry point**: `src/index.ts` sets up the Commander CLI; the default command is `generate`.
81
+ - **Config**: User configuration loaded from `.commitgenierc.json` at the git root or current directory.
82
+ - **Binary**: Published as `commit-genie` CLI via the `bin` field in package.json.
83
+
84
+ ## Code Conventions
85
+
86
+ - 2-space indentation
87
+ - Single quotes for strings
88
+ - Semicolons required
89
+ - PascalCase for classes and interfaces
90
+ - camelCase for functions and variables
91
+ - TypeScript strict mode is on — all strict checks enforced
92
+ - No ESLint or Prettier configured; follow existing style
93
+
94
+ ## Testing
95
+
96
+ - Tests use Jest with the `ts-jest` preset
97
+ - Test files live alongside source: `src/services/gitService.test.ts`
98
+ - Tests use `describe`/`it` structure with `expect` assertions
99
+ - Dependencies are mocked with `jest.mock()` and manual mock implementations
100
+ - Coverage excludes `*.d.ts` files and `src/index.ts`
101
+ - Run `npm test` before submitting changes
102
+
103
+ ## Key Types
104
+
105
+ Defined in `src/types/index.ts`:
106
+
107
+ - `CommitType`: `'feat' | 'fix' | 'docs' | 'style' | 'refactor' | 'test' | 'chore' | 'perf'`
108
+ - `ChangeAnalysis`: The central analysis result including commit type, scope, description, file changes, semantic analysis, and AST results
109
+ - `CommitMessage`: Formatted commit message with type, scope, description, body, footer
110
+ - `CommitGenieConfig`: Full user configuration interface
111
+ - `SemanticAnalysis`: Role-based analysis (UI, logic, data, API, style, config, test)
112
+ - `ASTClassifierResult`: AST-based commit classification output
@@ -106,9 +106,32 @@ export declare class AnalyzerService {
106
106
  * Generate the WHAT changed description
107
107
  */
108
108
  private static generateWhatChanged;
109
+ /**
110
+ * Generate a description from AST analysis results.
111
+ * Produces specific, human-readable descriptions based on structural signals.
112
+ * Returns empty string if AST data is insufficient for a good description.
113
+ */
114
+ private static generateASTDescription;
115
+ /**
116
+ * Detect test+implementation file pairs.
117
+ * Returns the pair info if files match naming conventions, null otherwise.
118
+ */
119
+ private static detectTestImplPair;
120
+ /**
121
+ * Generate description using lightweight file content analysis (JSON, CSS, YAML, MD).
122
+ * Returns null if no structural info is available.
123
+ */
124
+ private static generateFileContentDescription;
125
+ /**
126
+ * Generate description using hunk header context (function/class names).
127
+ * Works for any language that git can identify scope for.
128
+ * Returns null if no hunk context is available.
129
+ */
130
+ private static generateHunkDescription;
109
131
  /**
110
132
  * Generate a descriptive commit message
111
- * Uses semantic analysis when available for intent-based descriptions
133
+ * Uses AST analysis for specific descriptions when available,
134
+ * falls back to semantic analysis and regex-based descriptions.
112
135
  */
113
136
  private static generateDescription;
114
137
  /**
@@ -151,6 +174,7 @@ export declare class AnalyzerService {
151
174
  static generateSuggestionsWithAI(useAI?: boolean): Promise<MessageSuggestion[]>;
152
175
  /**
153
176
  * Generate an alternative description style
177
+ * Uses AST data when available, otherwise falls back to regex extraction.
154
178
  */
155
179
  private static generateAlternativeDescription;
156
180
  }
@@ -1 +1 @@
1
- {"version":3,"file":"analyzerService.d.ts","sourceRoot":"","sources":["../../src/services/analyzerService.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,cAAc,EAEd,aAAa,EAKb,iBAAiB,EAKlB,MAAM,UAAU,CAAC;AAsOlB,qBAAa,eAAe;IAC1B;;OAEG;IACH,MAAM,CAAC,cAAc,IAAI,cAAc;IAqJvC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA6EpC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA6TlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IA4B9B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IA8DrC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,6BAA6B;IAyF5C;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAkEvC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA6BvC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA+ClC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAkCrC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,iBAAiB;IA8ChC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAkBjC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAkD/B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,oBAAoB;IA6BnC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IAsD9B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAuEtC;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAmDjC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAkClC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IA0C5B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IA8CrC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAUlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,yBAAyB;IA2BxC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAgDlC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAwFlC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,2BAA2B;IAyE1C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAiD7B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAK1B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,YAAY;IAmD3B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IA8B5B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAkE/B;;OAEG;IACH,MAAM,CAAC,qBAAqB,IAAI,aAAa;IAkC7C;;OAEG;IACH,MAAM,CAAC,2BAA2B,IAAI,iBAAiB,EAAE;IAqLzD;;OAEG;WACU,yBAAyB,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IA0E5F;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,8BAA8B;CAoE9C"}
1
+ {"version":3,"file":"analyzerService.d.ts","sourceRoot":"","sources":["../../src/services/analyzerService.ts"],"names":[],"mappings":"AAYA,OAAO,EAEL,cAAc,EAEd,aAAa,EAKb,iBAAiB,EAKlB,MAAM,UAAU,CAAC;AAsOlB,qBAAa,eAAe;IAC1B;;OAEG;IACH,MAAM,CAAC,cAAc,IAAI,cAAc;IAuJvC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA6EpC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA+TlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IA4B9B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IA8DrC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,6BAA6B;IAyF5C;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAkEvC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAsCvC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA+ClC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAkCrC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,iBAAiB;IA8ChC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAkBjC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAkD/B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,oBAAoB;IA6BnC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IAsD9B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAuEtC;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAmDjC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAkClC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IA0C5B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IA8CrC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAUlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,yBAAyB;IA2BxC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAgDlC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IA4HrC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAyCjC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,8BAA8B;IAuB7C;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAyBtC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAqIlC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,2BAA2B;IAyE1C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAiD7B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAK1B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,YAAY;IAmD3B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IA8B5B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAkE/B;;OAEG;IACH,MAAM,CAAC,qBAAqB,IAAI,aAAa;IAkC7C;;OAEG;IACH,MAAM,CAAC,2BAA2B,IAAI,iBAAiB,EAAE;IAqLzD;;OAEG;WACU,yBAAyB,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IA0E5F;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,8BAA8B;CAiG9C"}
@@ -8,6 +8,7 @@ const aiService_1 = require("./aiService");
8
8
  const filePatterns_1 = require("../utils/filePatterns");
9
9
  const astExportAnalyzer_1 = require("./astExportAnalyzer");
10
10
  const astClassifier_1 = require("./astClassifier");
11
+ const fileContentAnalyzer_1 = require("./fileContentAnalyzer");
11
12
  const COMMIT_EMOJIS = {
12
13
  feat: '✨',
13
14
  fix: '🐛',
@@ -335,13 +336,13 @@ class AnalyzerService {
335
336
  commitType = mappedType;
336
337
  }
337
338
  }
338
- // Generate description (uses semantic analysis when available)
339
+ // Generate description (uses AST analysis, then semantic analysis as fallback)
339
340
  const description = this.generateDescription(filesAffected, {
340
341
  added: fileChanges.added.length,
341
342
  modified: fileChanges.modified.length,
342
343
  deleted: fileChanges.deleted.length,
343
344
  renamed: fileChanges.renamed.length
344
- }, stagedFiles, diff, semanticAnalysis);
345
+ }, stagedFiles, diff, semanticAnalysis, astResult, commitType);
345
346
  return {
346
347
  commitType,
347
348
  scope,
@@ -419,6 +420,8 @@ class AnalyzerService {
419
420
  */
420
421
  static determineCommitType(filesAffected, diff, stagedFiles, exportDiff) {
421
422
  const diffLower = diff.toLowerCase();
423
+ const cleanedDiff = this.extractNonCommentChanges(diff);
424
+ const cleanedDiffLower = cleanedDiff.toLowerCase();
422
425
  const filePaths = stagedFiles.map((f) => f.path.toLowerCase());
423
426
  // === FILE TYPE BASED DETECTION (highest priority) ===
424
427
  // If only test files changed
@@ -496,7 +499,7 @@ class AnalyzerService {
496
499
  /\bparallel\b/i,
497
500
  /\bbatch(ing)?\b/i,
498
501
  ];
499
- if (perfPatterns.some(p => p.test(diffLower))) {
502
+ if (perfPatterns.some(p => p.test(cleanedDiffLower))) {
500
503
  return 'perf';
501
504
  }
502
505
  // === DETECT NEW FUNCTIONALITY FIRST ===
@@ -555,7 +558,7 @@ class AnalyzerService {
555
558
  /\benable(s|d|ing)?\s+(new\s+)?\w+/i,
556
559
  /\bnew\s+(feature|function|method|api|endpoint)/i,
557
560
  ];
558
- if (featPatterns.some(p => p.test(diff))) {
561
+ if (featPatterns.some(p => p.test(cleanedDiff))) {
559
562
  return 'feat';
560
563
  }
561
564
  // === FIX DETECTION ===
@@ -612,7 +615,7 @@ class AnalyzerService {
612
615
  /\bwas\s*broken\b/i, // "was broken" indicates fixing
613
616
  /\bbroken\b.*\bfix/i, // broken...fix pattern
614
617
  ];
615
- if (fixPatterns.some(p => p.test(diff))) {
618
+ if (fixPatterns.some(p => p.test(cleanedDiff))) {
616
619
  return 'fix';
617
620
  }
618
621
  // === REFACTOR DETECTION ===
@@ -629,7 +632,7 @@ class AnalyzerService {
629
632
  /\bDRY\b/,
630
633
  /\breorganiz(e|es|ed|ing)\b/i,
631
634
  ];
632
- if (refactorPatterns.some(p => p.test(diff))) {
635
+ if (refactorPatterns.some(p => p.test(cleanedDiff))) {
633
636
  return 'refactor';
634
637
  }
635
638
  // If modifications with balanced adds/removes and no new functionality, likely refactor
@@ -666,7 +669,7 @@ class AnalyzerService {
666
669
  /\bci\b.*\b(config|setup)\b/i,
667
670
  /\blint(er|ing)?\b/i,
668
671
  ];
669
- if (chorePatterns.some(p => p.test(diff)) || chorePatterns.some(p => filePaths.some(f => p.test(f)))) {
672
+ if (chorePatterns.some(p => p.test(cleanedDiff)) || chorePatterns.some(p => filePaths.some(f => p.test(f)))) {
670
673
  return 'chore';
671
674
  }
672
675
  // === FALLBACK ===
@@ -917,7 +920,7 @@ class AnalyzerService {
917
920
  if (trimmed.length === 0)
918
921
  return false;
919
922
  // Filter out comment lines
920
- return !(trimmed.startsWith('//') ||
923
+ if (trimmed.startsWith('//') ||
921
924
  trimmed.startsWith('/*') ||
922
925
  trimmed.startsWith('*') ||
923
926
  trimmed.startsWith('*/') ||
@@ -927,7 +930,14 @@ class AnalyzerService {
927
930
  trimmed.startsWith('"""') ||
928
931
  trimmed.startsWith("'''") ||
929
932
  /^\/\*\*/.test(trimmed) ||
930
- /^\*\s*@\w+/.test(trimmed));
933
+ /^\*\s*@\w+/.test(trimmed)) {
934
+ return false;
935
+ }
936
+ // Filter out lines that are purely string literals
937
+ if (/^(?:return\s+)?(['"`]).*\1[;,]?\s*$/.test(trimmed)) {
938
+ return false;
939
+ }
940
+ return true;
931
941
  })
932
942
  .join('\n');
933
943
  }
@@ -1498,11 +1508,209 @@ class AnalyzerService {
1498
1508
  }
1499
1509
  return 'code';
1500
1510
  }
1511
+ /**
1512
+ * Generate a description from AST analysis results.
1513
+ * Produces specific, human-readable descriptions based on structural signals.
1514
+ * Returns empty string if AST data is insufficient for a good description.
1515
+ */
1516
+ static generateASTDescription(astResult, commitType) {
1517
+ const { declarations, signals } = astResult;
1518
+ // Handle renames — most specific and readable
1519
+ if (declarations.renamed.length > 0) {
1520
+ const r = declarations.renamed[0];
1521
+ if (declarations.renamed.length === 1) {
1522
+ return `rename ${r.oldName} to ${r.newName}`;
1523
+ }
1524
+ return `rename ${r.oldName} to ${r.newName} and ${declarations.renamed.length - 1} more`;
1525
+ }
1526
+ // Handle removed exported declarations (breaking refactor)
1527
+ const removedExported = declarations.removed.filter(d => d.exported);
1528
+ if (removedExported.length > 0) {
1529
+ if (removedExported.length === 1) {
1530
+ return `remove ${removedExported[0].name} export`;
1531
+ }
1532
+ return `remove ${removedExported[0].name} and ${removedExported.length - 1} more exports`;
1533
+ }
1534
+ // Handle breaking signature changes
1535
+ const sigChangeSignals = signals.filter(s => s.signal === 'changed-exported-sig');
1536
+ if (sigChangeSignals.length > 0) {
1537
+ const nameMatch = sigChangeSignals[0].detail.match(/on '([^']+)'/);
1538
+ if (nameMatch) {
1539
+ return `change ${nameMatch[1]} signature`;
1540
+ }
1541
+ }
1542
+ // Handle new declarations (feat)
1543
+ if (commitType === 'feat' && declarations.added.length > 0) {
1544
+ const exported = declarations.added.filter(d => d.exported);
1545
+ const target = exported.length > 0 ? exported : declarations.added;
1546
+ if (target.length === 1) {
1547
+ return `add ${target[0].name} ${target[0].kind}`;
1548
+ }
1549
+ if (target.length === 2) {
1550
+ return `add ${target[0].name} and ${target[1].name}`;
1551
+ }
1552
+ return `add ${target[0].name}, ${target[1].name} and ${target.length - 2} more`;
1553
+ }
1554
+ // Handle fix signals (error handling, guards)
1555
+ if (commitType === 'fix') {
1556
+ const errorSignals = signals.filter(s => s.signal === 'error-handling-change');
1557
+ if (errorSignals.length > 0) {
1558
+ const nameMatch = errorSignals[0].detail.match(/in '([^']+)'/);
1559
+ if (nameMatch) {
1560
+ return `handle errors in ${nameMatch[1]}`;
1561
+ }
1562
+ }
1563
+ const guardSignals = signals.filter(s => s.signal === 'guard-added');
1564
+ if (guardSignals.length > 0) {
1565
+ const nameMatch = guardSignals[0].detail.match(/in '([^']+)'/);
1566
+ if (nameMatch) {
1567
+ return `add validation to ${nameMatch[1]}`;
1568
+ }
1569
+ }
1570
+ // Literal-only changes
1571
+ const literalSignals = signals.filter(s => s.signal === 'literal-only-change');
1572
+ if (literalSignals.length > 0) {
1573
+ const nameMatch = literalSignals[0].detail.match(/in '([^']+)'/);
1574
+ if (nameMatch) {
1575
+ return `fix values in ${nameMatch[1]}`;
1576
+ }
1577
+ }
1578
+ }
1579
+ // Handle perf signals
1580
+ if (commitType === 'perf') {
1581
+ const perfSignals = signals.filter(s => s.signal === 'perf-pattern');
1582
+ if (perfSignals.length > 0) {
1583
+ const nameMatch = perfSignals[0].detail.match(/in '([^']+)'/);
1584
+ if (nameMatch) {
1585
+ return `optimize ${nameMatch[1]} performance`;
1586
+ }
1587
+ }
1588
+ }
1589
+ // Handle refactor signals (control flow, modified)
1590
+ if (commitType === 'refactor') {
1591
+ const controlFlowSignals = signals.filter(s => s.signal === 'control-flow-restructured');
1592
+ if (controlFlowSignals.length > 0) {
1593
+ const nameMatch = controlFlowSignals[0].detail.match(/in '([^']+)'/);
1594
+ if (nameMatch) {
1595
+ return `simplify ${nameMatch[1]} control flow`;
1596
+ }
1597
+ }
1598
+ if (declarations.modified.length > 0) {
1599
+ const names = declarations.modified.map(m => m.name);
1600
+ if (names.length === 1) {
1601
+ return `refactor ${names[0]}`;
1602
+ }
1603
+ if (names.length === 2) {
1604
+ return `refactor ${names[0]} and ${names[1]}`;
1605
+ }
1606
+ return `refactor ${names[0]}, ${names[1]} and ${names.length - 2} more`;
1607
+ }
1608
+ }
1609
+ // Generic fallback using affectedElements
1610
+ if (astResult.affectedElements.length > 0) {
1611
+ const verb = commitType === 'feat' ? 'add' :
1612
+ commitType === 'fix' ? 'fix' :
1613
+ commitType === 'refactor' ? 'refactor' :
1614
+ commitType === 'perf' ? 'optimize' : 'update';
1615
+ const els = astResult.affectedElements;
1616
+ if (els.length === 1) {
1617
+ return `${verb} ${els[0]}`;
1618
+ }
1619
+ if (els.length === 2) {
1620
+ return `${verb} ${els[0]} and ${els[1]}`;
1621
+ }
1622
+ return `${verb} ${els[0]}, ${els[1]} and ${els.length - 2} more`;
1623
+ }
1624
+ return '';
1625
+ }
1626
+ /**
1627
+ * Detect test+implementation file pairs.
1628
+ * Returns the pair info if files match naming conventions, null otherwise.
1629
+ */
1630
+ static detectTestImplPair(stagedFiles) {
1631
+ if (stagedFiles.length < 2)
1632
+ return null;
1633
+ for (const file of stagedFiles) {
1634
+ const path = file.path;
1635
+ // Check if this is a test file
1636
+ const testMatch = path.match(/^(.+)\.(test|spec)\.(ts|tsx|js|jsx|mts|cts)$/);
1637
+ const dirTestMatch = path.match(/^(.+\/)__tests__\/(.+)\.(ts|tsx|js|jsx|mts|cts)$/);
1638
+ if (testMatch) {
1639
+ const implPath = `${testMatch[1]}.${testMatch[3]}`;
1640
+ const implFile = stagedFiles.find(f => f.path === implPath);
1641
+ if (implFile) {
1642
+ return {
1643
+ implFile: this.getFileName(implFile.path),
1644
+ testFile: this.getFileName(file.path),
1645
+ implStatus: implFile.status,
1646
+ testStatus: file.status,
1647
+ };
1648
+ }
1649
+ }
1650
+ if (dirTestMatch) {
1651
+ const implPath = `${dirTestMatch[1]}${dirTestMatch[2]}.${dirTestMatch[3]}`;
1652
+ const implFile = stagedFiles.find(f => f.path === implPath);
1653
+ if (implFile) {
1654
+ return {
1655
+ implFile: this.getFileName(implFile.path),
1656
+ testFile: this.getFileName(file.path),
1657
+ implStatus: implFile.status,
1658
+ testStatus: file.status,
1659
+ };
1660
+ }
1661
+ }
1662
+ }
1663
+ return null;
1664
+ }
1665
+ /**
1666
+ * Generate description using lightweight file content analysis (JSON, CSS, YAML, MD).
1667
+ * Returns null if no structural info is available.
1668
+ */
1669
+ static generateFileContentDescription(stagedFiles, diff) {
1670
+ if (stagedFiles.length !== 1)
1671
+ return null;
1672
+ const file = stagedFiles[0];
1673
+ const analysis = (0, fileContentAnalyzer_1.analyzeFileContent)(file.path, diff);
1674
+ if (!analysis || analysis.changedElements.length === 0)
1675
+ return null;
1676
+ const fileName = this.getFileName(file.path);
1677
+ const elements = analysis.changedElements;
1678
+ const elementStr = elements.length <= 2
1679
+ ? elements.join(' and ')
1680
+ : `${elements[0]} and ${elements.length - 1} more`;
1681
+ const verb = analysis.changeKind === 'added' ? 'add'
1682
+ : analysis.changeKind === 'removed' ? 'remove'
1683
+ : 'update';
1684
+ return `${verb} ${elementStr} in ${fileName}`;
1685
+ }
1686
+ /**
1687
+ * Generate description using hunk header context (function/class names).
1688
+ * Works for any language that git can identify scope for.
1689
+ * Returns null if no hunk context is available.
1690
+ */
1691
+ static generateHunkDescription(stagedFiles, diff, commitType) {
1692
+ const hunkNames = this.extractHunkContext(diff);
1693
+ if (hunkNames.length === 0)
1694
+ return null;
1695
+ const verb = commitType === 'feat' ? 'add' :
1696
+ commitType === 'fix' ? 'fix' :
1697
+ commitType === 'refactor' ? 'refactor' :
1698
+ commitType === 'perf' ? 'optimize' : 'update';
1699
+ const nameStr = hunkNames.length <= 2
1700
+ ? hunkNames.join(' and ')
1701
+ : `${hunkNames[0]} and ${hunkNames.length - 1} more`;
1702
+ if (stagedFiles.length === 1) {
1703
+ const fileName = this.getFileName(stagedFiles[0].path);
1704
+ return `${verb} ${nameStr} in ${fileName}`;
1705
+ }
1706
+ return `${verb} ${nameStr}`;
1707
+ }
1501
1708
  /**
1502
1709
  * Generate a descriptive commit message
1503
- * Uses semantic analysis when available for intent-based descriptions
1710
+ * Uses AST analysis for specific descriptions when available,
1711
+ * falls back to semantic analysis and regex-based descriptions.
1504
1712
  */
1505
- static generateDescription(filesAffected, fileStatuses, stagedFiles, diff, semanticAnalysis) {
1713
+ static generateDescription(filesAffected, fileStatuses, stagedFiles, diff, semanticAnalysis, astResult, commitType) {
1506
1714
  // Check for comment-only changes (documentation in source files)
1507
1715
  if (this.isCommentOnlyChange(diff)) {
1508
1716
  const fileName = stagedFiles.length === 1 ? this.getFileName(stagedFiles[0].path) : null;
@@ -1523,10 +1731,52 @@ class AnalyzerService {
1523
1731
  if (this.isFormattingOnlyChange(diff)) {
1524
1732
  return this.generateFormattingDescription(diff, stagedFiles);
1525
1733
  }
1734
+ // Use AST analysis for specific, human-readable descriptions when available
1735
+ // AST descriptions are preferred because they name exact functions/classes
1736
+ if (astResult && commitType) {
1737
+ const astMaxWeight = astResult.signals.length > 0
1738
+ ? Math.max(...astResult.signals.map(s => s.weight), 0)
1739
+ : 0;
1740
+ if (astMaxWeight > 1 || astResult.declarations.renamed.length > 0) {
1741
+ const astDesc = this.generateASTDescription(astResult, commitType);
1742
+ if (astDesc) {
1743
+ return astDesc;
1744
+ }
1745
+ }
1746
+ }
1526
1747
  // Use semantic analysis for intent-based descriptions when available
1527
1748
  if (semanticAnalysis && semanticAnalysis.roleChanges.length > 0) {
1528
1749
  return this.generateSemanticDescription(semanticAnalysis, stagedFiles);
1529
1750
  }
1751
+ // Detect test + implementation file pairs
1752
+ const pair = this.detectTestImplPair(stagedFiles);
1753
+ if (pair) {
1754
+ const implName = pair.implFile.replace(/\.\w+$/, '');
1755
+ if (pair.implStatus === 'A' && pair.testStatus === 'A') {
1756
+ return `add ${implName} with tests`;
1757
+ }
1758
+ else if (pair.implStatus === 'M' && pair.testStatus === 'A') {
1759
+ return `update ${implName} and add tests`;
1760
+ }
1761
+ else if (pair.implStatus === 'A' && pair.testStatus === 'M') {
1762
+ return `add ${implName} and update tests`;
1763
+ }
1764
+ else {
1765
+ return `update ${implName} and tests`;
1766
+ }
1767
+ }
1768
+ // Use lightweight file content analysis (JSON keys, CSS selectors, etc.)
1769
+ const fileContentDesc = this.generateFileContentDescription(stagedFiles, diff);
1770
+ if (fileContentDesc) {
1771
+ return fileContentDesc;
1772
+ }
1773
+ // Use hunk header context for function/class names (works for any language)
1774
+ if (commitType) {
1775
+ const hunkDesc = this.generateHunkDescription(stagedFiles, diff, commitType);
1776
+ if (hunkDesc) {
1777
+ return hunkDesc;
1778
+ }
1779
+ }
1530
1780
  // Single file changes (fallback)
1531
1781
  if (stagedFiles.length === 1) {
1532
1782
  const file = stagedFiles[0];
@@ -2009,11 +2259,38 @@ class AnalyzerService {
2009
2259
  }
2010
2260
  /**
2011
2261
  * Generate an alternative description style
2262
+ * Uses AST data when available, otherwise falls back to regex extraction.
2012
2263
  */
2013
2264
  static generateAlternativeDescription(analysis, diff) {
2014
2265
  const { fileChanges, filesAffected } = analysis;
2015
2266
  const totalFiles = fileChanges.added.length + fileChanges.modified.length +
2016
2267
  fileChanges.deleted.length + fileChanges.renamed.length;
2268
+ // Use AST data to build a detailed alternative with file context
2269
+ if (analysis.astResult && analysis.astResult.affectedElements.length > 0) {
2270
+ const elements = analysis.astResult.affectedElements;
2271
+ const verb = analysis.commitType === 'feat' ? 'add' :
2272
+ analysis.commitType === 'fix' ? 'fix' :
2273
+ analysis.commitType === 'refactor' ? 'refactor' : 'update';
2274
+ // For single file, include file name for extra context
2275
+ if (totalFiles === 1) {
2276
+ const fileName = fileChanges.added[0] || fileChanges.modified[0] || fileChanges.deleted[0] || fileChanges.renamed[0];
2277
+ if (elements.length === 1) {
2278
+ return `${verb} ${elements[0]} in ${fileName}`;
2279
+ }
2280
+ if (elements.length === 2) {
2281
+ return `${verb} ${elements[0]} and ${elements[1]} in ${fileName}`;
2282
+ }
2283
+ return `${verb} ${elements[0]}, ${elements[1]} and ${elements.length - 2} more in ${fileName}`;
2284
+ }
2285
+ // For multiple files, list elements across files
2286
+ if (elements.length === 1) {
2287
+ return `${verb} ${elements[0]}`;
2288
+ }
2289
+ if (elements.length === 2) {
2290
+ return `${verb} ${elements[0]} and ${elements[1]}`;
2291
+ }
2292
+ return `${verb} ${elements[0]}, ${elements[1]} and ${elements.length - 2} more`;
2293
+ }
2017
2294
  // For single file, provide more detail using extracted elements when available
2018
2295
  if (totalFiles === 1) {
2019
2296
  const elements = this.extractAffectedElements(diff);