@m3hti/commit-genie 2.0.0 → 3.0.1

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 (35) hide show
  1. package/CLAUDE.md +112 -0
  2. package/dist/services/analyzerService.d.ts +9 -1
  3. package/dist/services/analyzerService.d.ts.map +1 -1
  4. package/dist/services/analyzerService.js +192 -8
  5. package/dist/services/analyzerService.js.map +1 -1
  6. package/dist/services/astAnalyzer.d.ts +42 -0
  7. package/dist/services/astAnalyzer.d.ts.map +1 -0
  8. package/dist/services/astAnalyzer.js +613 -0
  9. package/dist/services/astAnalyzer.js.map +1 -0
  10. package/dist/services/astAnalyzer.test.d.ts +2 -0
  11. package/dist/services/astAnalyzer.test.d.ts.map +1 -0
  12. package/dist/services/astAnalyzer.test.js +319 -0
  13. package/dist/services/astAnalyzer.test.js.map +1 -0
  14. package/dist/services/astClassifier.d.ts +17 -0
  15. package/dist/services/astClassifier.d.ts.map +1 -0
  16. package/dist/services/astClassifier.js +390 -0
  17. package/dist/services/astClassifier.js.map +1 -0
  18. package/dist/services/astClassifier.test.d.ts +2 -0
  19. package/dist/services/astClassifier.test.d.ts.map +1 -0
  20. package/dist/services/astClassifier.test.js +141 -0
  21. package/dist/services/astClassifier.test.js.map +1 -0
  22. package/dist/services/astDiffEngine.d.ts +45 -0
  23. package/dist/services/astDiffEngine.d.ts.map +1 -0
  24. package/dist/services/astDiffEngine.js +261 -0
  25. package/dist/services/astDiffEngine.js.map +1 -0
  26. package/dist/services/astDiffEngine.test.d.ts +2 -0
  27. package/dist/services/astDiffEngine.test.d.ts.map +1 -0
  28. package/dist/services/astDiffEngine.test.js +234 -0
  29. package/dist/services/astDiffEngine.test.js.map +1 -0
  30. package/dist/services/astExportAnalyzer.d.ts.map +1 -1
  31. package/dist/services/astExportAnalyzer.js +122 -292
  32. package/dist/services/astExportAnalyzer.js.map +1 -1
  33. package/dist/types/index.d.ts +68 -0
  34. package/dist/types/index.d.ts.map +1 -1
  35. package/package.json +2 -4
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,16 @@ 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;
109
115
  /**
110
116
  * Generate a descriptive commit message
111
- * Uses semantic analysis when available for intent-based descriptions
117
+ * Uses AST analysis for specific descriptions when available,
118
+ * falls back to semantic analysis and regex-based descriptions.
112
119
  */
113
120
  private static generateDescription;
114
121
  /**
@@ -151,6 +158,7 @@ export declare class AnalyzerService {
151
158
  static generateSuggestionsWithAI(useAI?: boolean): Promise<MessageSuggestion[]>;
152
159
  /**
153
160
  * Generate an alternative description style
161
+ * Uses AST data when available, otherwise falls back to regex extraction.
154
162
  */
155
163
  private static generateAlternativeDescription;
156
164
  }
@@ -1 +1 @@
1
- {"version":3,"file":"analyzerService.d.ts","sourceRoot":"","sources":["../../src/services/analyzerService.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,cAAc,EAEd,aAAa,EAKb,iBAAiB,EAKlB,MAAM,UAAU,CAAC;AAsOlB,qBAAa,eAAe;IAC1B;;OAEG;IACH,MAAM,CAAC,cAAc,IAAI,cAAc;IA2HvC;;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":"AAWA,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;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;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IA4HrC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAwGlC;;;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"}
@@ -7,6 +7,7 @@ const historyService_1 = require("./historyService");
7
7
  const aiService_1 = require("./aiService");
8
8
  const filePatterns_1 = require("../utils/filePatterns");
9
9
  const astExportAnalyzer_1 = require("./astExportAnalyzer");
10
+ const astClassifier_1 = require("./astClassifier");
10
11
  const COMMIT_EMOJIS = {
11
12
  feat: '✨',
12
13
  fix: '🐛',
@@ -271,12 +272,37 @@ class AnalyzerService {
271
272
  const isLargeChange = stagedFiles.length >= 3 || totalChanges >= 100;
272
273
  // Compute export diff once (AST-based) and share across all detection methods
273
274
  const exportDiff = computeExportDiffForStagedFiles(stagedFiles);
274
- // Determine commit type based on file types and changes
275
- let commitType = this.determineCommitType(filesAffected, diff, stagedFiles, exportDiff);
275
+ // Run AST analysis pipeline on JS/TS files
276
+ let astResult;
277
+ try {
278
+ const astClassification = (0, astClassifier_1.analyzeChangesAST)(stagedFiles);
279
+ if (astClassification.primaryType !== null || astClassification.signals.length > 0) {
280
+ astResult = astClassification;
281
+ }
282
+ }
283
+ catch {
284
+ // AST pipeline failure: fall through to regex analysis
285
+ }
286
+ // Determine commit type: AST wins when it has strong signals (weight > 1)
287
+ // Weak signals (weight 1 only, e.g. new non-exported helper) allow regex/semantic override
288
+ const astMaxWeight = astResult
289
+ ? Math.max(...astResult.signals.map(s => s.weight), 0)
290
+ : 0;
291
+ let commitType;
292
+ if (astResult?.primaryType && astMaxWeight > 1) {
293
+ commitType = astResult.primaryType;
294
+ }
295
+ else {
296
+ commitType = this.determineCommitType(filesAffected, diff, stagedFiles, exportDiff);
297
+ }
276
298
  // Determine scope if applicable
277
299
  const scope = this.determineScope(stagedFiles);
278
- // Detect breaking changes
300
+ // Detect breaking changes (merge AST + regex results)
279
301
  let { isBreaking, reasons } = this.detectBreakingChanges(diff, stagedFiles, exportDiff);
302
+ if (astResult?.isBreaking) {
303
+ isBreaking = true;
304
+ reasons = [...new Set([...reasons, ...astResult.breakingReasons])];
305
+ }
280
306
  // Perform semantic analysis for intent-based messages
281
307
  // Only perform for source files to avoid overhead on simple config/doc changes
282
308
  let semanticAnalysis;
@@ -297,7 +323,8 @@ class AnalyzerService {
297
323
  // Apply semantic intent → commit type mapping policy
298
324
  // Only override heuristic 'feat' type when semantic analysis provides specific intent
299
325
  // This preserves specialized types (perf, docs, test, style, chore) detected by heuristics
300
- if (semanticAnalysis && commitType === 'feat') {
326
+ // Skip this override if AST pipeline determined the type with strong signals
327
+ if (semanticAnalysis && commitType === 'feat' && astMaxWeight <= 1) {
301
328
  const intentToType = {
302
329
  fix: 'fix',
303
330
  refactor: 'refactor',
@@ -308,13 +335,13 @@ class AnalyzerService {
308
335
  commitType = mappedType;
309
336
  }
310
337
  }
311
- // Generate description (uses semantic analysis when available)
338
+ // Generate description (uses AST analysis, then semantic analysis as fallback)
312
339
  const description = this.generateDescription(filesAffected, {
313
340
  added: fileChanges.added.length,
314
341
  modified: fileChanges.modified.length,
315
342
  deleted: fileChanges.deleted.length,
316
343
  renamed: fileChanges.renamed.length
317
- }, stagedFiles, diff, semanticAnalysis);
344
+ }, stagedFiles, diff, semanticAnalysis, astResult, commitType);
318
345
  return {
319
346
  commitType,
320
347
  scope,
@@ -325,6 +352,7 @@ class AnalyzerService {
325
352
  isBreakingChange: isBreaking,
326
353
  breakingChangeReasons: reasons,
327
354
  semanticAnalysis,
355
+ astResult,
328
356
  };
329
357
  }
330
358
  /**
@@ -1470,11 +1498,127 @@ class AnalyzerService {
1470
1498
  }
1471
1499
  return 'code';
1472
1500
  }
1501
+ /**
1502
+ * Generate a description from AST analysis results.
1503
+ * Produces specific, human-readable descriptions based on structural signals.
1504
+ * Returns empty string if AST data is insufficient for a good description.
1505
+ */
1506
+ static generateASTDescription(astResult, commitType) {
1507
+ const { declarations, signals } = astResult;
1508
+ // Handle renames — most specific and readable
1509
+ if (declarations.renamed.length > 0) {
1510
+ const r = declarations.renamed[0];
1511
+ if (declarations.renamed.length === 1) {
1512
+ return `rename ${r.oldName} to ${r.newName}`;
1513
+ }
1514
+ return `rename ${r.oldName} to ${r.newName} and ${declarations.renamed.length - 1} more`;
1515
+ }
1516
+ // Handle removed exported declarations (breaking refactor)
1517
+ const removedExported = declarations.removed.filter(d => d.exported);
1518
+ if (removedExported.length > 0) {
1519
+ if (removedExported.length === 1) {
1520
+ return `remove ${removedExported[0].name} export`;
1521
+ }
1522
+ return `remove ${removedExported[0].name} and ${removedExported.length - 1} more exports`;
1523
+ }
1524
+ // Handle breaking signature changes
1525
+ const sigChangeSignals = signals.filter(s => s.signal === 'changed-exported-sig');
1526
+ if (sigChangeSignals.length > 0) {
1527
+ const nameMatch = sigChangeSignals[0].detail.match(/on '([^']+)'/);
1528
+ if (nameMatch) {
1529
+ return `change ${nameMatch[1]} signature`;
1530
+ }
1531
+ }
1532
+ // Handle new declarations (feat)
1533
+ if (commitType === 'feat' && declarations.added.length > 0) {
1534
+ const exported = declarations.added.filter(d => d.exported);
1535
+ const target = exported.length > 0 ? exported : declarations.added;
1536
+ if (target.length === 1) {
1537
+ return `add ${target[0].name} ${target[0].kind}`;
1538
+ }
1539
+ if (target.length === 2) {
1540
+ return `add ${target[0].name} and ${target[1].name}`;
1541
+ }
1542
+ return `add ${target[0].name}, ${target[1].name} and ${target.length - 2} more`;
1543
+ }
1544
+ // Handle fix signals (error handling, guards)
1545
+ if (commitType === 'fix') {
1546
+ const errorSignals = signals.filter(s => s.signal === 'error-handling-change');
1547
+ if (errorSignals.length > 0) {
1548
+ const nameMatch = errorSignals[0].detail.match(/in '([^']+)'/);
1549
+ if (nameMatch) {
1550
+ return `handle errors in ${nameMatch[1]}`;
1551
+ }
1552
+ }
1553
+ const guardSignals = signals.filter(s => s.signal === 'guard-added');
1554
+ if (guardSignals.length > 0) {
1555
+ const nameMatch = guardSignals[0].detail.match(/in '([^']+)'/);
1556
+ if (nameMatch) {
1557
+ return `add validation to ${nameMatch[1]}`;
1558
+ }
1559
+ }
1560
+ // Literal-only changes
1561
+ const literalSignals = signals.filter(s => s.signal === 'literal-only-change');
1562
+ if (literalSignals.length > 0) {
1563
+ const nameMatch = literalSignals[0].detail.match(/in '([^']+)'/);
1564
+ if (nameMatch) {
1565
+ return `fix values in ${nameMatch[1]}`;
1566
+ }
1567
+ }
1568
+ }
1569
+ // Handle perf signals
1570
+ if (commitType === 'perf') {
1571
+ const perfSignals = signals.filter(s => s.signal === 'perf-pattern');
1572
+ if (perfSignals.length > 0) {
1573
+ const nameMatch = perfSignals[0].detail.match(/in '([^']+)'/);
1574
+ if (nameMatch) {
1575
+ return `optimize ${nameMatch[1]} performance`;
1576
+ }
1577
+ }
1578
+ }
1579
+ // Handle refactor signals (control flow, modified)
1580
+ if (commitType === 'refactor') {
1581
+ const controlFlowSignals = signals.filter(s => s.signal === 'control-flow-restructured');
1582
+ if (controlFlowSignals.length > 0) {
1583
+ const nameMatch = controlFlowSignals[0].detail.match(/in '([^']+)'/);
1584
+ if (nameMatch) {
1585
+ return `simplify ${nameMatch[1]} control flow`;
1586
+ }
1587
+ }
1588
+ if (declarations.modified.length > 0) {
1589
+ const names = declarations.modified.map(m => m.name);
1590
+ if (names.length === 1) {
1591
+ return `refactor ${names[0]}`;
1592
+ }
1593
+ if (names.length === 2) {
1594
+ return `refactor ${names[0]} and ${names[1]}`;
1595
+ }
1596
+ return `refactor ${names[0]}, ${names[1]} and ${names.length - 2} more`;
1597
+ }
1598
+ }
1599
+ // Generic fallback using affectedElements
1600
+ if (astResult.affectedElements.length > 0) {
1601
+ const verb = commitType === 'feat' ? 'add' :
1602
+ commitType === 'fix' ? 'fix' :
1603
+ commitType === 'refactor' ? 'refactor' :
1604
+ commitType === 'perf' ? 'optimize' : 'update';
1605
+ const els = astResult.affectedElements;
1606
+ if (els.length === 1) {
1607
+ return `${verb} ${els[0]}`;
1608
+ }
1609
+ if (els.length === 2) {
1610
+ return `${verb} ${els[0]} and ${els[1]}`;
1611
+ }
1612
+ return `${verb} ${els[0]}, ${els[1]} and ${els.length - 2} more`;
1613
+ }
1614
+ return '';
1615
+ }
1473
1616
  /**
1474
1617
  * Generate a descriptive commit message
1475
- * Uses semantic analysis when available for intent-based descriptions
1618
+ * Uses AST analysis for specific descriptions when available,
1619
+ * falls back to semantic analysis and regex-based descriptions.
1476
1620
  */
1477
- static generateDescription(filesAffected, fileStatuses, stagedFiles, diff, semanticAnalysis) {
1621
+ static generateDescription(filesAffected, fileStatuses, stagedFiles, diff, semanticAnalysis, astResult, commitType) {
1478
1622
  // Check for comment-only changes (documentation in source files)
1479
1623
  if (this.isCommentOnlyChange(diff)) {
1480
1624
  const fileName = stagedFiles.length === 1 ? this.getFileName(stagedFiles[0].path) : null;
@@ -1495,6 +1639,19 @@ class AnalyzerService {
1495
1639
  if (this.isFormattingOnlyChange(diff)) {
1496
1640
  return this.generateFormattingDescription(diff, stagedFiles);
1497
1641
  }
1642
+ // Use AST analysis for specific, human-readable descriptions when available
1643
+ // AST descriptions are preferred because they name exact functions/classes
1644
+ if (astResult && commitType) {
1645
+ const astMaxWeight = astResult.signals.length > 0
1646
+ ? Math.max(...astResult.signals.map(s => s.weight), 0)
1647
+ : 0;
1648
+ if (astMaxWeight > 1 || astResult.declarations.renamed.length > 0) {
1649
+ const astDesc = this.generateASTDescription(astResult, commitType);
1650
+ if (astDesc) {
1651
+ return astDesc;
1652
+ }
1653
+ }
1654
+ }
1498
1655
  // Use semantic analysis for intent-based descriptions when available
1499
1656
  if (semanticAnalysis && semanticAnalysis.roleChanges.length > 0) {
1500
1657
  return this.generateSemanticDescription(semanticAnalysis, stagedFiles);
@@ -1981,11 +2138,38 @@ class AnalyzerService {
1981
2138
  }
1982
2139
  /**
1983
2140
  * Generate an alternative description style
2141
+ * Uses AST data when available, otherwise falls back to regex extraction.
1984
2142
  */
1985
2143
  static generateAlternativeDescription(analysis, diff) {
1986
2144
  const { fileChanges, filesAffected } = analysis;
1987
2145
  const totalFiles = fileChanges.added.length + fileChanges.modified.length +
1988
2146
  fileChanges.deleted.length + fileChanges.renamed.length;
2147
+ // Use AST data to build a detailed alternative with file context
2148
+ if (analysis.astResult && analysis.astResult.affectedElements.length > 0) {
2149
+ const elements = analysis.astResult.affectedElements;
2150
+ const verb = analysis.commitType === 'feat' ? 'add' :
2151
+ analysis.commitType === 'fix' ? 'fix' :
2152
+ analysis.commitType === 'refactor' ? 'refactor' : 'update';
2153
+ // For single file, include file name for extra context
2154
+ if (totalFiles === 1) {
2155
+ const fileName = fileChanges.added[0] || fileChanges.modified[0] || fileChanges.deleted[0] || fileChanges.renamed[0];
2156
+ if (elements.length === 1) {
2157
+ return `${verb} ${elements[0]} in ${fileName}`;
2158
+ }
2159
+ if (elements.length === 2) {
2160
+ return `${verb} ${elements[0]} and ${elements[1]} in ${fileName}`;
2161
+ }
2162
+ return `${verb} ${elements[0]}, ${elements[1]} and ${elements.length - 2} more in ${fileName}`;
2163
+ }
2164
+ // For multiple files, list elements across files
2165
+ if (elements.length === 1) {
2166
+ return `${verb} ${elements[0]}`;
2167
+ }
2168
+ if (elements.length === 2) {
2169
+ return `${verb} ${elements[0]} and ${elements[1]}`;
2170
+ }
2171
+ return `${verb} ${elements[0]}, ${elements[1]} and ${elements.length - 2} more`;
2172
+ }
1989
2173
  // For single file, provide more detail using extracted elements when available
1990
2174
  if (totalFiles === 1) {
1991
2175
  const elements = this.extractAffectedElements(diff);