@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 +112 -0
- package/dist/services/analyzerService.d.ts +25 -1
- package/dist/services/analyzerService.d.ts.map +1 -1
- package/dist/services/analyzerService.js +288 -11
- package/dist/services/analyzerService.js.map +1 -1
- package/dist/services/analyzerService.test.js +97 -0
- package/dist/services/analyzerService.test.js.map +1 -1
- package/dist/services/fileContentAnalyzer.d.ts +10 -0
- package/dist/services/fileContentAnalyzer.d.ts.map +1 -0
- package/dist/services/fileContentAnalyzer.js +170 -0
- package/dist/services/fileContentAnalyzer.js.map +1 -0
- package/dist/services/fileContentAnalyzer.test.d.ts +2 -0
- package/dist/services/fileContentAnalyzer.test.d.ts.map +1 -0
- package/dist/services/fileContentAnalyzer.test.js +118 -0
- package/dist/services/fileContentAnalyzer.test.js.map +1 -0
- package/package.json +1 -1
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
|
|
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":"
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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);
|