cast-code 1.0.8 → 1.0.10
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/dist/modules/core/services/deep-agent.service.js +1 -1
- package/dist/modules/core/services/deep-agent.service.js.map +1 -1
- package/dist/modules/git/services/commit-generator.service.js +432 -114
- package/dist/modules/git/services/commit-generator.service.js.map +1 -1
- package/dist/modules/kanban/kanban.module.js +36 -0
- package/dist/modules/kanban/kanban.module.js.map +1 -0
- package/dist/modules/kanban/services/kanban-server.service.js +322 -0
- package/dist/modules/kanban/services/kanban-server.service.js.map +1 -0
- package/dist/modules/kanban/views/kanban-ui.js +858 -0
- package/dist/modules/kanban/views/kanban-ui.js.map +1 -0
- package/dist/modules/repl/repl.module.js +3 -1
- package/dist/modules/repl/repl.module.js.map +1 -1
- package/dist/modules/repl/services/repl.service.js +13 -2
- package/dist/modules/repl/services/repl.service.js.map +1 -1
- package/dist/modules/tasks/services/plan-executor.service.js +2 -2
- package/dist/modules/tasks/services/plan-executor.service.js.map +1 -1
- package/dist/modules/tasks/services/task-management.service.js +35 -2
- package/dist/modules/tasks/services/task-management.service.js.map +1 -1
- package/dist/modules/tasks/types/task.types.js.map +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "CommitGeneratorService", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _common = require("@nestjs/common");
|
|
12
|
+
const _fs = require("fs");
|
|
12
13
|
const _child_process = require("child_process");
|
|
13
14
|
const _messages = require("@langchain/core/messages");
|
|
14
15
|
const _multillmservice = require("../../../common/services/multi-llm.service");
|
|
@@ -22,19 +23,82 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
22
23
|
function _ts_metadata(k, v) {
|
|
23
24
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
24
25
|
}
|
|
26
|
+
const COMMIT_TYPES = [
|
|
27
|
+
'feat',
|
|
28
|
+
'fix',
|
|
29
|
+
'docs',
|
|
30
|
+
'style',
|
|
31
|
+
'refactor',
|
|
32
|
+
'perf',
|
|
33
|
+
'test',
|
|
34
|
+
'build',
|
|
35
|
+
'ci',
|
|
36
|
+
'chore'
|
|
37
|
+
];
|
|
38
|
+
const COMMIT_TYPE_SET = new Set(COMMIT_TYPES);
|
|
39
|
+
const COMMIT_TYPE_ALIASES = {
|
|
40
|
+
feature: 'feat',
|
|
41
|
+
features: 'feat',
|
|
42
|
+
bug: 'fix',
|
|
43
|
+
bugfix: 'fix',
|
|
44
|
+
documentation: 'docs',
|
|
45
|
+
docs: 'docs',
|
|
46
|
+
test: 'test',
|
|
47
|
+
tests: 'test',
|
|
48
|
+
testing: 'test',
|
|
49
|
+
performance: 'perf',
|
|
50
|
+
optimize: 'perf',
|
|
51
|
+
optimization: 'perf',
|
|
52
|
+
dependency: 'build',
|
|
53
|
+
dependencies: 'build',
|
|
54
|
+
maintenance: 'chore',
|
|
55
|
+
housekeeping: 'chore',
|
|
56
|
+
cleanup: 'chore',
|
|
57
|
+
remove: 'refactor'
|
|
58
|
+
};
|
|
59
|
+
const LEADING_VERB_TRANSLATIONS = {
|
|
60
|
+
add: 'adiciona',
|
|
61
|
+
adds: 'adiciona',
|
|
62
|
+
update: 'atualiza',
|
|
63
|
+
updates: 'atualiza',
|
|
64
|
+
upgrade: 'atualiza',
|
|
65
|
+
upgrades: 'atualiza',
|
|
66
|
+
fix: 'corrige',
|
|
67
|
+
fixes: 'corrige',
|
|
68
|
+
remove: 'remove',
|
|
69
|
+
removes: 'remove',
|
|
70
|
+
refactor: 'refatora',
|
|
71
|
+
refactors: 'refatora',
|
|
72
|
+
improve: 'melhora',
|
|
73
|
+
improves: 'melhora',
|
|
74
|
+
create: 'cria',
|
|
75
|
+
creates: 'cria',
|
|
76
|
+
implement: 'implementa',
|
|
77
|
+
implements: 'implementa',
|
|
78
|
+
rename: 'renomeia',
|
|
79
|
+
renames: 'renomeia'
|
|
80
|
+
};
|
|
25
81
|
let CommitGeneratorService = class CommitGeneratorService {
|
|
26
82
|
getDiffInfo() {
|
|
27
83
|
try {
|
|
28
84
|
const cwd = process.cwd();
|
|
29
|
-
const staged = (0, _child_process.execSync)('git diff --cached', {
|
|
85
|
+
const staged = (0, _child_process.execSync)('git diff --cached --unified=1 --no-ext-diff', {
|
|
86
|
+
cwd,
|
|
87
|
+
encoding: 'utf-8'
|
|
88
|
+
});
|
|
89
|
+
const unstaged = (0, _child_process.execSync)('git diff --unified=1 --no-ext-diff', {
|
|
30
90
|
cwd,
|
|
31
91
|
encoding: 'utf-8'
|
|
32
92
|
});
|
|
33
|
-
const
|
|
93
|
+
const stagedStats = (0, _child_process.execSync)('git diff --cached --stat', {
|
|
34
94
|
cwd,
|
|
35
95
|
encoding: 'utf-8'
|
|
36
96
|
});
|
|
37
|
-
const
|
|
97
|
+
const unstagedStats = (0, _child_process.execSync)('git diff --stat', {
|
|
98
|
+
cwd,
|
|
99
|
+
encoding: 'utf-8'
|
|
100
|
+
});
|
|
101
|
+
const statusShort = (0, _child_process.execSync)('git status --short', {
|
|
38
102
|
cwd,
|
|
39
103
|
encoding: 'utf-8'
|
|
40
104
|
});
|
|
@@ -49,7 +113,7 @@ let CommitGeneratorService = class CommitGeneratorService {
|
|
|
49
113
|
stagedFiles: this.extractFiles(staged),
|
|
50
114
|
unstagedFiles: this.extractFiles(unstaged),
|
|
51
115
|
untrackedFiles,
|
|
52
|
-
stats
|
|
116
|
+
stats: this.buildStatsSummary(stagedStats, unstagedStats, statusShort, untrackedFiles)
|
|
53
117
|
};
|
|
54
118
|
} catch {
|
|
55
119
|
return null;
|
|
@@ -83,7 +147,7 @@ let CommitGeneratorService = class CommitGeneratorService {
|
|
|
83
147
|
new _messages.HumanMessage(prompt)
|
|
84
148
|
]);
|
|
85
149
|
const message = this.extractContent(response.content);
|
|
86
|
-
return this.
|
|
150
|
+
return this.normalizeCommitMessage(message, 'chore', scope);
|
|
87
151
|
}
|
|
88
152
|
async splitCommits() {
|
|
89
153
|
const diffInfo = this.getDiffInfo();
|
|
@@ -103,16 +167,16 @@ let CommitGeneratorService = class CommitGeneratorService {
|
|
|
103
167
|
const splitContent = this.extractContent(splitResponse.content);
|
|
104
168
|
const commitGroups = this.parseCommitGroups(splitContent);
|
|
105
169
|
if (!commitGroups?.length) return null;
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
for (const group of
|
|
170
|
+
const normalizedGroups = this.normalizeCommitGroups(commitGroups, allFiles);
|
|
171
|
+
if (normalizedGroups.length === 0) return null;
|
|
172
|
+
for (const group of normalizedGroups){
|
|
109
173
|
if (!group.scope) {
|
|
110
174
|
group.scope = this.monorepoDetector.determineScope(group.files, monorepoInfo);
|
|
111
175
|
}
|
|
112
176
|
}
|
|
113
177
|
const splitCommits = [];
|
|
114
|
-
for (const group of
|
|
115
|
-
const message = await this.generateMessageForGroup(group
|
|
178
|
+
for (const group of normalizedGroups){
|
|
179
|
+
const message = await this.generateMessageForGroup(group);
|
|
116
180
|
splitCommits.push({
|
|
117
181
|
...group,
|
|
118
182
|
message
|
|
@@ -179,9 +243,9 @@ let CommitGeneratorService = class CommitGeneratorService {
|
|
|
179
243
|
(0, _child_process.execSync)('git reset', {
|
|
180
244
|
cwd
|
|
181
245
|
});
|
|
182
|
-
for (const file of commit.files){
|
|
246
|
+
for (const file of this.normalizeFiles(commit.files)){
|
|
183
247
|
try {
|
|
184
|
-
(0, _child_process.execSync)(`git add
|
|
248
|
+
(0, _child_process.execSync)(`git add -- ${this.escapeShellArg(file)}`, {
|
|
185
249
|
cwd
|
|
186
250
|
});
|
|
187
251
|
} catch {}
|
|
@@ -213,13 +277,25 @@ let CommitGeneratorService = class CommitGeneratorService {
|
|
|
213
277
|
}
|
|
214
278
|
async refineCommitMessage(currentMessage, userSuggestion, diffInfo) {
|
|
215
279
|
const llm = this.multiLlmService.createModel('cheap');
|
|
216
|
-
const
|
|
280
|
+
const context = this.buildDiffContext(diffInfo, {
|
|
281
|
+
maxLength: 6000,
|
|
282
|
+
maxCharsPerFile: 1200,
|
|
283
|
+
maxUntrackedFiles: 3,
|
|
284
|
+
maxUntrackedLines: 40
|
|
285
|
+
});
|
|
286
|
+
const currentMetadata = this.extractTypeAndScope(currentMessage);
|
|
287
|
+
const prompt = `Mensagem atual: ${currentMessage}
|
|
288
|
+
|
|
289
|
+
Sugestão do usuário: ${userSuggestion}
|
|
290
|
+
|
|
291
|
+
Contexto do diff:
|
|
292
|
+
${context}`;
|
|
217
293
|
const response = await llm.invoke([
|
|
218
294
|
new _messages.SystemMessage(this.getRefineSystemPrompt()),
|
|
219
295
|
new _messages.HumanMessage(prompt)
|
|
220
296
|
]);
|
|
221
297
|
const message = this.extractContent(response.content);
|
|
222
|
-
return this.
|
|
298
|
+
return this.normalizeCommitMessage(message, currentMetadata.type ?? 'chore', currentMetadata.scope, 'atualiza código', currentMetadata.breaking ?? false);
|
|
223
299
|
}
|
|
224
300
|
extractFiles(diff) {
|
|
225
301
|
const files = new Set();
|
|
@@ -244,61 +320,76 @@ let CommitGeneratorService = class CommitGeneratorService {
|
|
|
244
320
|
}
|
|
245
321
|
return String(content);
|
|
246
322
|
}
|
|
247
|
-
cleanCommitMessage(message) {
|
|
248
|
-
const lines = message.trim().split('\n').map((l)=>l.trim()).filter((l)=>l.length > 0);
|
|
249
|
-
const firstCommitLine = lines.find((l)=>l.includes(':')) || lines[0] || '';
|
|
250
|
-
return firstCommitLine.replace(/^["']|["']$/g, '').trim();
|
|
251
|
-
}
|
|
252
323
|
buildCommitPrompt(diffInfo, scope) {
|
|
253
|
-
const scopeHint = scope ?
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
324
|
+
const scopeHint = scope ? `Escopo provável do monorepo: "${scope}".` : 'Escopo do monorepo não identificado automaticamente.';
|
|
325
|
+
const fullDiff = this.buildDiffContext(diffInfo, {
|
|
326
|
+
maxLength: 12000,
|
|
327
|
+
maxCharsPerFile: 1800,
|
|
328
|
+
maxUntrackedFiles: 4,
|
|
329
|
+
maxUntrackedLines: 60
|
|
330
|
+
});
|
|
331
|
+
return `Analise TODO o contexto de mudanças e gere UMA mensagem de commit no padrão Conventional Commits.
|
|
332
|
+
|
|
333
|
+
${scopeHint}
|
|
334
|
+
|
|
335
|
+
Regras obrigatórias:
|
|
336
|
+
- Formato: "type(scope): descrição", "type: descrição" ou com breaking "type(scope)!: descrição"
|
|
337
|
+
- Tipos permitidos: ${COMMIT_TYPES.join(', ')}
|
|
338
|
+
- Descrição em português (pt-BR), objetiva, no imperativo e sem ponto final
|
|
339
|
+
- Máximo de 72 caracteres no assunto completo
|
|
340
|
+
- A mensagem deve refletir a intenção principal do conjunto total de mudanças
|
|
341
|
+
- Se for breaking change, inclua "!" após o type/scope
|
|
342
|
+
- Considere staged, unstaged e arquivos novos
|
|
343
|
+
|
|
344
|
+
Contexto do diff:
|
|
345
|
+
${fullDiff}`;
|
|
267
346
|
}
|
|
268
347
|
buildSplitPrompt(diffInfo, files) {
|
|
269
|
-
|
|
270
|
-
if (diffInfo.staged) {
|
|
271
|
-
fullDiff += `=== Staged changes ===\n${diffInfo.staged}\n\n`;
|
|
272
|
-
}
|
|
273
|
-
if (diffInfo.unstaged) {
|
|
274
|
-
fullDiff += `=== Unstaged changes ===\n${diffInfo.unstaged}\n\n`;
|
|
275
|
-
}
|
|
276
|
-
const allFiles = [
|
|
348
|
+
const allFiles = this.normalizeFiles([
|
|
277
349
|
...files,
|
|
278
|
-
...diffInfo.untrackedFiles
|
|
279
|
-
];
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
350
|
+
...diffInfo.untrackedFiles
|
|
351
|
+
]);
|
|
352
|
+
const fullDiff = this.buildDiffContext(diffInfo, {
|
|
353
|
+
maxLength: 15000,
|
|
354
|
+
maxCharsPerFile: 1600,
|
|
355
|
+
maxUntrackedFiles: 6,
|
|
356
|
+
maxUntrackedLines: 80
|
|
357
|
+
});
|
|
358
|
+
return `Analise o diff completo e divida em commits lógicos no padrão Conventional Commits.
|
|
359
|
+
|
|
360
|
+
Regras obrigatórias:
|
|
361
|
+
- Cada arquivo da lista deve aparecer exatamente uma vez no resultado
|
|
362
|
+
- Inclua TODOS os arquivos listados
|
|
363
|
+
- Separe mudanças por coesão funcional (feature, fix, docs, refactor etc.)
|
|
364
|
+
- Evite misturar objetivos diferentes no mesmo commit
|
|
365
|
+
- Tipos permitidos: ${COMMIT_TYPES.join(', ')}
|
|
366
|
+
- Descrição em português (pt-BR), no imperativo e sem ponto final
|
|
367
|
+
|
|
368
|
+
Arquivos esperados:
|
|
369
|
+
${allFiles.join(', ') || '(nenhum arquivo detectado)'}
|
|
370
|
+
|
|
371
|
+
Contexto do diff:
|
|
372
|
+
${fullDiff}`;
|
|
286
373
|
}
|
|
287
|
-
async generateMessageForGroup(group
|
|
374
|
+
async generateMessageForGroup(group) {
|
|
288
375
|
const llm = this.multiLlmService.createModel('cheap');
|
|
289
376
|
const scopePart = group.scope ? `(${group.scope})` : '';
|
|
290
|
-
const prompt = `
|
|
377
|
+
const prompt = `Gere uma mensagem Conventional Commit (máximo 72 caracteres) para este grupo:
|
|
378
|
+
|
|
379
|
+
Tipo: ${group.type}${scopePart}
|
|
380
|
+
Arquivos: ${group.files.join(', ')}
|
|
381
|
+
Resumo: ${group.description}
|
|
382
|
+
|
|
383
|
+
Retorne APENAS uma linha no formato:
|
|
384
|
+
"type(scope): descrição", "type: descrição" ou "type(scope)!: descrição"
|
|
385
|
+
|
|
386
|
+
Descrição obrigatoriamente em português (pt-BR).`;
|
|
291
387
|
const response = await llm.invoke([
|
|
292
388
|
new _messages.SystemMessage(this.getCommitSystemPrompt()),
|
|
293
389
|
new _messages.HumanMessage(prompt)
|
|
294
390
|
]);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (!message.includes(':')) {
|
|
298
|
-
const scope = group.scope ? `(${group.scope})` : '';
|
|
299
|
-
message = `${group.type}${scope}: ${message}`;
|
|
300
|
-
}
|
|
301
|
-
return message;
|
|
391
|
+
const message = this.extractContent(response.content);
|
|
392
|
+
return this.normalizeCommitMessage(message, group.type, group.scope, group.description);
|
|
302
393
|
}
|
|
303
394
|
parseCommitGroups(content) {
|
|
304
395
|
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
|
|
@@ -319,88 +410,315 @@ let CommitGeneratorService = class CommitGeneratorService {
|
|
|
319
410
|
return null;
|
|
320
411
|
}
|
|
321
412
|
getCommitSystemPrompt() {
|
|
322
|
-
return `
|
|
323
|
-
|
|
324
|
-
**Available types:**
|
|
325
|
-
- feat: new feature
|
|
326
|
-
- fix: bug fix
|
|
327
|
-
- docs: documentation
|
|
328
|
-
- style: formatting (no logic change)
|
|
329
|
-
- refactor: refactoring (no functionality change)
|
|
330
|
-
- perf: performance improvement
|
|
331
|
-
- test: tests
|
|
332
|
-
- build: build/dependencies
|
|
333
|
-
- ci: continuous integration
|
|
334
|
-
- chore: general tasks
|
|
335
|
-
- cleanup: code cleanup
|
|
336
|
-
- remove: code removal
|
|
413
|
+
return `Você é especialista em mensagens de commit no padrão Conventional Commits.
|
|
337
414
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
- Without scope: <type>: <description>
|
|
415
|
+
Tipos permitidos:
|
|
416
|
+
${COMMIT_TYPES.join(', ')}
|
|
341
417
|
|
|
342
|
-
|
|
343
|
-
-
|
|
344
|
-
-
|
|
345
|
-
-
|
|
346
|
-
- Use imperative mood ("add" not "added")
|
|
347
|
-
- Be specific but concise
|
|
418
|
+
Formato obrigatório:
|
|
419
|
+
- Com escopo: <type>(<scope>): <descrição>
|
|
420
|
+
- Sem escopo: <type>: <descrição>
|
|
421
|
+
- Breaking change no assunto: <type>(<scope>)!: <descrição> (ou <type>!: <descrição>)
|
|
348
422
|
|
|
349
|
-
|
|
423
|
+
Regras:
|
|
424
|
+
- Assunto completo com no máximo 72 caracteres
|
|
425
|
+
- Descrição em português (pt-BR)
|
|
426
|
+
- Verbo no imperativo
|
|
427
|
+
- Sem ponto final
|
|
428
|
+
- Seja específico e evite mensagens genéricas
|
|
429
|
+
- Use escopo quando ele estiver claro
|
|
430
|
+
- Tipos feat e fix devem ser usados de forma semântica (feature e correção)
|
|
350
431
|
|
|
351
|
-
|
|
432
|
+
Retorne SOMENTE a linha do commit, sem explicações.`;
|
|
352
433
|
}
|
|
353
434
|
getSplitSystemPrompt() {
|
|
354
|
-
return `
|
|
435
|
+
return `Você é especialista em organizar diffs em commits lógicos.
|
|
355
436
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
3. **Related files**: Files that work together
|
|
437
|
+
Tarefa:
|
|
438
|
+
- Agrupar mudanças por coesão funcional
|
|
439
|
+
- Separar corretamente feature, fix, docs, refactor etc.
|
|
440
|
+
- Garantir que todos os arquivos apareçam exatamente uma vez
|
|
361
441
|
|
|
362
|
-
|
|
363
|
-
|
|
442
|
+
Formato de resposta:
|
|
443
|
+
Retorne SOMENTE JSON válido:
|
|
364
444
|
\`\`\`json
|
|
365
445
|
{
|
|
366
446
|
"commits": [
|
|
367
447
|
{
|
|
368
448
|
"type": "feat",
|
|
369
|
-
"files": ["src/
|
|
370
|
-
"description": "
|
|
449
|
+
"files": ["src/chatbot/service.ts"],
|
|
450
|
+
"description": "adiciona service de chatbot"
|
|
371
451
|
},
|
|
372
452
|
{
|
|
373
453
|
"type": "docs",
|
|
374
454
|
"files": ["README.md"],
|
|
375
|
-
"description": "
|
|
455
|
+
"description": "atualiza documentação de uso"
|
|
376
456
|
}
|
|
377
457
|
]
|
|
378
458
|
}
|
|
379
459
|
\`\`\`
|
|
380
460
|
|
|
381
|
-
|
|
382
|
-
|
|
461
|
+
Regras:
|
|
462
|
+
- Tipos permitidos: ${COMMIT_TYPES.join(', ')}
|
|
463
|
+
- Cada commit com propósito claro
|
|
464
|
+
- Descrição em português (pt-BR), no imperativo, sem ponto final
|
|
465
|
+
- Máximo recomendado de 5 arquivos por commit
|
|
466
|
+
- Pode retornar 1 commit apenas se o diff for pequeno e coeso
|
|
383
467
|
|
|
384
|
-
|
|
385
|
-
- Each commit should have a clear purpose
|
|
386
|
-
- Group functionally related files
|
|
387
|
-
- Separate features from fixes from documentation
|
|
388
|
-
- Maximum 5 files per commit (ideally fewer)
|
|
389
|
-
- If diff is small (<3 files), can be 1 commit only
|
|
390
|
-
- Description must ALWAYS be in English
|
|
391
|
-
- Include ALL files in the result
|
|
392
|
-
|
|
393
|
-
**IMPORTANT:** Return ONLY the JSON, no additional text.`;
|
|
468
|
+
Retorne SOMENTE o JSON, sem texto adicional.`;
|
|
394
469
|
}
|
|
395
470
|
getRefineSystemPrompt() {
|
|
396
|
-
return `
|
|
471
|
+
return `Você está refinando uma mensagem de commit a partir do feedback do usuário.
|
|
472
|
+
|
|
473
|
+
Instruções:
|
|
474
|
+
- Respeite Conventional Commits
|
|
475
|
+
- Mantenha no máximo 72 caracteres
|
|
476
|
+
- Mensagem em português (pt-BR), no imperativo e sem ponto final
|
|
477
|
+
- Incorpore a sugestão do usuário sem perder precisão técnica
|
|
397
478
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
479
|
+
Retorne SOMENTE a nova linha de commit.`;
|
|
480
|
+
}
|
|
481
|
+
normalizeCommitGroups(commitGroups, files) {
|
|
482
|
+
const expectedFiles = new Set(this.normalizeFiles(files));
|
|
483
|
+
const usedFiles = new Set();
|
|
484
|
+
const normalizedGroups = [];
|
|
485
|
+
for (const group of commitGroups){
|
|
486
|
+
const filesFromGroup = this.normalizeFiles(Array.isArray(group.files) ? group.files : []).filter((file)=>expectedFiles.has(file)).filter((file)=>{
|
|
487
|
+
if (usedFiles.has(file)) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
usedFiles.add(file);
|
|
491
|
+
return true;
|
|
492
|
+
});
|
|
493
|
+
if (filesFromGroup.length === 0) continue;
|
|
494
|
+
normalizedGroups.push({
|
|
495
|
+
type: this.normalizeCommitType(group.type, this.inferTypeFromFiles(filesFromGroup)),
|
|
496
|
+
files: filesFromGroup,
|
|
497
|
+
description: this.normalizeDescription(group.description, 'organiza mudanças relacionadas'),
|
|
498
|
+
scope: this.normalizeScope(group.scope)
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const missingFiles = this.normalizeFiles(files).filter((file)=>!usedFiles.has(file));
|
|
502
|
+
if (missingFiles.length > 0) {
|
|
503
|
+
normalizedGroups.push({
|
|
504
|
+
type: this.inferTypeFromFiles(missingFiles),
|
|
505
|
+
files: missingFiles,
|
|
506
|
+
description: 'organiza arquivos restantes do diff'
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return normalizedGroups;
|
|
510
|
+
}
|
|
511
|
+
normalizeCommitMessage(rawMessage, fallbackType, fallbackScope, fallbackDescription = 'atualiza código', fallbackBreaking = false) {
|
|
512
|
+
const candidateLine = this.extractCandidateCommitLine(rawMessage);
|
|
513
|
+
const match = candidateLine.match(/^([a-zA-Z-]+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/);
|
|
514
|
+
const type = this.normalizeCommitType(match?.[1], fallbackType);
|
|
515
|
+
const scope = this.normalizeScope(match?.[2] ?? fallbackScope);
|
|
516
|
+
const breaking = Boolean(match?.[3]) || fallbackBreaking;
|
|
517
|
+
const rawDescription = match?.[4] ?? candidateLine;
|
|
518
|
+
const description = this.normalizeDescription(rawDescription, fallbackDescription);
|
|
519
|
+
const breakingFlag = breaking ? '!' : '';
|
|
520
|
+
const prefix = scope ? `${type}(${scope})${breakingFlag}: ` : `${type}${breakingFlag}: `;
|
|
521
|
+
const maxDescriptionLength = Math.max(12, 72 - prefix.length);
|
|
522
|
+
const truncatedDescription = this.truncateText(description, maxDescriptionLength);
|
|
523
|
+
return `${prefix}${truncatedDescription}`;
|
|
524
|
+
}
|
|
525
|
+
extractTypeAndScope(message) {
|
|
526
|
+
const candidateLine = this.extractCandidateCommitLine(message);
|
|
527
|
+
const match = candidateLine.match(/^([a-zA-Z-]+)(?:\(([^)]+)\))?(!)?:\s*.+$/);
|
|
528
|
+
if (!match) return {};
|
|
529
|
+
return {
|
|
530
|
+
type: this.normalizeCommitType(match[1], 'chore'),
|
|
531
|
+
scope: this.normalizeScope(match[2]),
|
|
532
|
+
breaking: Boolean(match[3])
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
extractCandidateCommitLine(rawMessage) {
|
|
536
|
+
const withoutCodeBlock = rawMessage.replace(/```(?:[\w-]+)?/g, '');
|
|
537
|
+
const lines = withoutCodeBlock.split('\n').map((line)=>line.trim()).filter((line)=>line.length > 0).map((line)=>line.replace(/^[-*]\s+/, ''));
|
|
538
|
+
if (lines.length === 0) return '';
|
|
539
|
+
const conventionalLine = lines.find((line)=>/^[a-zA-Z-]+(?:\([^)]+\))?!?:\s+/.test(line));
|
|
540
|
+
if (conventionalLine) {
|
|
541
|
+
return conventionalLine.replace(/^["'`]|["'`]$/g, '').trim();
|
|
542
|
+
}
|
|
543
|
+
const prefixedLine = lines.find((line)=>/^commit\s*:/i.test(line));
|
|
544
|
+
if (prefixedLine) {
|
|
545
|
+
return prefixedLine.replace(/^commit\s*:/i, '').replace(/^["'`]|["'`]$/g, '').trim();
|
|
546
|
+
}
|
|
547
|
+
return lines[0].replace(/^["'`]|["'`]$/g, '').trim();
|
|
548
|
+
}
|
|
549
|
+
normalizeCommitType(type, fallbackType) {
|
|
550
|
+
const normalized = (type || '').toLowerCase().trim().replace(/[^a-z]/g, '');
|
|
551
|
+
if (!normalized) return fallbackType;
|
|
552
|
+
if (COMMIT_TYPE_SET.has(normalized)) return normalized;
|
|
553
|
+
if (COMMIT_TYPE_ALIASES[normalized]) return COMMIT_TYPE_ALIASES[normalized];
|
|
554
|
+
return fallbackType;
|
|
555
|
+
}
|
|
556
|
+
normalizeScope(scope) {
|
|
557
|
+
if (!scope) return undefined;
|
|
558
|
+
const normalized = scope.trim().replace(/^["'`]|["'`]$/g, '').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9/_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
559
|
+
return normalized || undefined;
|
|
560
|
+
}
|
|
561
|
+
normalizeDescription(description, fallback) {
|
|
562
|
+
let normalized = description.replace(/^["'`]|["'`]$/g, '').replace(/^([a-zA-Z-]+)(?:\([^)]+\))?!?:\s*/, '').replace(/\s+/g, ' ').trim();
|
|
563
|
+
normalized = normalized.replace(/[.;:!?]+$/, '').trim();
|
|
564
|
+
if (!normalized) normalized = fallback;
|
|
565
|
+
normalized = this.translateLeadingVerb(normalized);
|
|
566
|
+
if (/^[A-ZÀ-Ý]/.test(normalized)) {
|
|
567
|
+
normalized = normalized.charAt(0).toLowerCase() + normalized.slice(1);
|
|
568
|
+
}
|
|
569
|
+
return normalized;
|
|
570
|
+
}
|
|
571
|
+
translateLeadingVerb(description) {
|
|
572
|
+
const match = description.match(/^([a-zA-Z]+)(\b.*)$/);
|
|
573
|
+
if (!match) return description;
|
|
574
|
+
const translated = LEADING_VERB_TRANSLATIONS[match[1].toLowerCase()];
|
|
575
|
+
if (!translated) return description;
|
|
576
|
+
return `${translated}${match[2]}`;
|
|
577
|
+
}
|
|
578
|
+
truncateText(text, maxLength) {
|
|
579
|
+
if (text.length <= maxLength) return text;
|
|
580
|
+
const truncated = text.slice(0, maxLength).trimEnd();
|
|
581
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
582
|
+
if (lastSpace > maxLength * 0.6) {
|
|
583
|
+
return truncated.slice(0, lastSpace).trimEnd();
|
|
584
|
+
}
|
|
585
|
+
return truncated;
|
|
586
|
+
}
|
|
587
|
+
normalizeFiles(files) {
|
|
588
|
+
const unique = new Set();
|
|
589
|
+
for (const file of files){
|
|
590
|
+
if (typeof file !== 'string') continue;
|
|
591
|
+
const normalized = file.trim().replace(/^["']|["']$/g, '');
|
|
592
|
+
if (!normalized) continue;
|
|
593
|
+
unique.add(normalized);
|
|
594
|
+
}
|
|
595
|
+
return Array.from(unique);
|
|
596
|
+
}
|
|
597
|
+
buildStatsSummary(stagedStats, unstagedStats, statusShort, untrackedFiles) {
|
|
598
|
+
const sections = [];
|
|
599
|
+
if (statusShort.trim()) {
|
|
600
|
+
sections.push(`Git status (--short):\n${statusShort.trim()}`);
|
|
601
|
+
}
|
|
602
|
+
if (stagedStats.trim()) {
|
|
603
|
+
sections.push(`Diff staged (--cached --stat):\n${stagedStats.trim()}`);
|
|
604
|
+
}
|
|
605
|
+
if (unstagedStats.trim()) {
|
|
606
|
+
sections.push(`Diff unstaged (--stat):\n${unstagedStats.trim()}`);
|
|
607
|
+
}
|
|
608
|
+
if (untrackedFiles.length > 0) {
|
|
609
|
+
sections.push(`Arquivos novos (${untrackedFiles.length}): ${untrackedFiles.join(', ')}`);
|
|
610
|
+
}
|
|
611
|
+
return sections.join('\n\n');
|
|
612
|
+
}
|
|
613
|
+
buildDiffContext(diffInfo, options) {
|
|
614
|
+
const sections = [];
|
|
615
|
+
if (diffInfo.stats.trim()) {
|
|
616
|
+
sections.push(`Resumo:\n${diffInfo.stats.trim()}`);
|
|
617
|
+
}
|
|
618
|
+
sections.push(this.buildFilesSummary(diffInfo));
|
|
619
|
+
const stagedByFile = this.limitDiffByFile(diffInfo.staged, options.maxCharsPerFile);
|
|
620
|
+
if (stagedByFile) {
|
|
621
|
+
sections.push(`=== STAGED ===\n${stagedByFile}`);
|
|
622
|
+
}
|
|
623
|
+
const unstagedByFile = this.limitDiffByFile(diffInfo.unstaged, options.maxCharsPerFile);
|
|
624
|
+
if (unstagedByFile) {
|
|
625
|
+
sections.push(`=== UNSTAGED ===\n${unstagedByFile}`);
|
|
626
|
+
}
|
|
627
|
+
const untrackedPreview = this.buildUntrackedPreview(diffInfo.untrackedFiles, options.maxUntrackedFiles, options.maxUntrackedLines);
|
|
628
|
+
if (untrackedPreview) {
|
|
629
|
+
sections.push(`=== PREVIEW ARQUIVOS NOVOS ===\n${untrackedPreview}`);
|
|
630
|
+
}
|
|
631
|
+
let combined = sections.filter(Boolean).join('\n\n');
|
|
632
|
+
if (combined.length > options.maxLength) {
|
|
633
|
+
combined = `${combined.slice(0, options.maxLength).trimEnd()}\n\n... (contexto truncado)`;
|
|
634
|
+
}
|
|
635
|
+
return combined;
|
|
636
|
+
}
|
|
637
|
+
buildFilesSummary(diffInfo) {
|
|
638
|
+
const lines = [];
|
|
639
|
+
const stagedFiles = this.normalizeFiles(diffInfo.stagedFiles);
|
|
640
|
+
const unstagedFiles = this.normalizeFiles(diffInfo.unstagedFiles);
|
|
641
|
+
const untrackedFiles = this.normalizeFiles(diffInfo.untrackedFiles);
|
|
642
|
+
if (stagedFiles.length > 0) {
|
|
643
|
+
lines.push(`Staged (${stagedFiles.length}): ${this.summarizeFileList(stagedFiles)}`);
|
|
644
|
+
}
|
|
645
|
+
if (unstagedFiles.length > 0) {
|
|
646
|
+
lines.push(`Unstaged (${unstagedFiles.length}): ${this.summarizeFileList(unstagedFiles)}`);
|
|
647
|
+
}
|
|
648
|
+
if (untrackedFiles.length > 0) {
|
|
649
|
+
lines.push(`Novos (${untrackedFiles.length}): ${this.summarizeFileList(untrackedFiles)}`);
|
|
650
|
+
}
|
|
651
|
+
if (lines.length === 0) {
|
|
652
|
+
return 'Arquivos afetados: nenhum arquivo identificado';
|
|
653
|
+
}
|
|
654
|
+
return `Arquivos afetados:\n${lines.join('\n')}`;
|
|
655
|
+
}
|
|
656
|
+
summarizeFileList(files, limit = 20) {
|
|
657
|
+
if (files.length <= limit) {
|
|
658
|
+
return files.join(', ');
|
|
659
|
+
}
|
|
660
|
+
const remaining = files.length - limit;
|
|
661
|
+
return `${files.slice(0, limit).join(', ')}, ... (+${remaining})`;
|
|
662
|
+
}
|
|
663
|
+
limitDiffByFile(diff, maxCharsPerFile) {
|
|
664
|
+
if (!diff.trim()) return '';
|
|
665
|
+
const sections = this.splitDiffByFile(diff);
|
|
666
|
+
if (sections.length === 0) return '';
|
|
667
|
+
const limitedSections = sections.map((section)=>{
|
|
668
|
+
if (section.length <= maxCharsPerFile) return section.trimEnd();
|
|
669
|
+
return `${section.slice(0, maxCharsPerFile).trimEnd()}\n... (diff deste arquivo truncado)`;
|
|
670
|
+
});
|
|
671
|
+
return limitedSections.join('\n\n');
|
|
672
|
+
}
|
|
673
|
+
splitDiffByFile(diff) {
|
|
674
|
+
const rawSections = diff.split(/^diff --git /m).map((section)=>section.trim()).filter((section)=>section.length > 0);
|
|
675
|
+
return rawSections.map((section)=>`diff --git ${section}`);
|
|
676
|
+
}
|
|
677
|
+
buildUntrackedPreview(files, maxFiles, maxLines) {
|
|
678
|
+
const selectedFiles = this.normalizeFiles(files).slice(0, maxFiles);
|
|
679
|
+
const sections = [];
|
|
680
|
+
for (const file of selectedFiles){
|
|
681
|
+
const preview = this.readTextFilePreview(file, maxLines);
|
|
682
|
+
if (!preview) continue;
|
|
683
|
+
sections.push(`--- ${file} ---\n${preview}`);
|
|
684
|
+
}
|
|
685
|
+
if (files.length > maxFiles) {
|
|
686
|
+
sections.push(`... ${files.length - maxFiles} arquivo(s) novo(s) omitido(s)`);
|
|
687
|
+
}
|
|
688
|
+
return sections.join('\n\n');
|
|
689
|
+
}
|
|
690
|
+
readTextFilePreview(file, maxLines) {
|
|
691
|
+
try {
|
|
692
|
+
const content = (0, _fs.readFileSync)(file, 'utf-8');
|
|
693
|
+
if (content.includes('\u0000')) {
|
|
694
|
+
return '[arquivo binário omitido]';
|
|
695
|
+
}
|
|
696
|
+
if (!content.length) {
|
|
697
|
+
return '[arquivo vazio]';
|
|
698
|
+
}
|
|
699
|
+
const lines = content.split('\n');
|
|
700
|
+
const preview = lines.slice(0, maxLines).join('\n');
|
|
701
|
+
const clippedPreview = preview.length > 2500 ? `${preview.slice(0, 2500)}\n...` : preview;
|
|
702
|
+
if (lines.length > maxLines && !clippedPreview.endsWith('\n...')) {
|
|
703
|
+
return `${clippedPreview}\n...`;
|
|
704
|
+
}
|
|
705
|
+
return clippedPreview;
|
|
706
|
+
} catch {
|
|
707
|
+
return '';
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
inferTypeFromFiles(files) {
|
|
711
|
+
if (files.length === 0) return 'chore';
|
|
712
|
+
const onlyDocs = files.every((file)=>/^docs\//i.test(file) || /(^|\/)README\.md$/i.test(file) || /(^|\/)CHANGELOG\.md$/i.test(file) || /\.(md|mdx|txt)$/i.test(file));
|
|
713
|
+
if (onlyDocs) return 'docs';
|
|
714
|
+
const onlyTests = files.every((file)=>/(^|\/)__tests__\//.test(file) || /\.(spec|test)\.[cm]?[jt]sx?$/.test(file));
|
|
715
|
+
if (onlyTests) return 'test';
|
|
716
|
+
const onlyInfra = files.every((file)=>/(^|\/)package(-lock)?\.json$/.test(file) || /(^|\/)(pnpm-lock\.yaml|yarn\.lock)$/i.test(file) || /(^|\/)Dockerfile/i.test(file) || /(^|\/)docker-compose\.ya?ml$/i.test(file) || /(^|\/)\.github\/workflows\//.test(file));
|
|
717
|
+
if (onlyInfra) return 'build';
|
|
718
|
+
return 'chore';
|
|
719
|
+
}
|
|
720
|
+
escapeShellArg(value) {
|
|
721
|
+
return `'${value.replace(/'/g, '\'\\\'\'')}'`;
|
|
404
722
|
}
|
|
405
723
|
constructor(multiLlmService, monorepoDetector){
|
|
406
724
|
this.multiLlmService = multiLlmService;
|