@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.
- package/CLAUDE.md +112 -0
- package/dist/services/analyzerService.d.ts +9 -1
- package/dist/services/analyzerService.d.ts.map +1 -1
- package/dist/services/analyzerService.js +192 -8
- package/dist/services/analyzerService.js.map +1 -1
- package/dist/services/astAnalyzer.d.ts +42 -0
- package/dist/services/astAnalyzer.d.ts.map +1 -0
- package/dist/services/astAnalyzer.js +613 -0
- package/dist/services/astAnalyzer.js.map +1 -0
- package/dist/services/astAnalyzer.test.d.ts +2 -0
- package/dist/services/astAnalyzer.test.d.ts.map +1 -0
- package/dist/services/astAnalyzer.test.js +319 -0
- package/dist/services/astAnalyzer.test.js.map +1 -0
- package/dist/services/astClassifier.d.ts +17 -0
- package/dist/services/astClassifier.d.ts.map +1 -0
- package/dist/services/astClassifier.js +390 -0
- package/dist/services/astClassifier.js.map +1 -0
- package/dist/services/astClassifier.test.d.ts +2 -0
- package/dist/services/astClassifier.test.d.ts.map +1 -0
- package/dist/services/astClassifier.test.js +141 -0
- package/dist/services/astClassifier.test.js.map +1 -0
- package/dist/services/astDiffEngine.d.ts +45 -0
- package/dist/services/astDiffEngine.d.ts.map +1 -0
- package/dist/services/astDiffEngine.js +261 -0
- package/dist/services/astDiffEngine.js.map +1 -0
- package/dist/services/astDiffEngine.test.d.ts +2 -0
- package/dist/services/astDiffEngine.test.d.ts.map +1 -0
- package/dist/services/astDiffEngine.test.js +234 -0
- package/dist/services/astDiffEngine.test.js.map +1 -0
- package/dist/services/astExportAnalyzer.d.ts.map +1 -1
- package/dist/services/astExportAnalyzer.js +122 -292
- package/dist/services/astExportAnalyzer.js.map +1 -1
- package/dist/types/index.d.ts +68 -0
- package/dist/types/index.d.ts.map +1 -1
- 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
|
|
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":"
|
|
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
|
-
//
|
|
275
|
-
let
|
|
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
|
|
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
|
|
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
|
|
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);
|