agent-master-devsecops 3.9.0 → 5.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/README.md +205 -371
- package/agents/AGENT_DEVSECOPS.md +32 -417
- package/agents/AGENT_INFRA.md +31 -0
- package/agents/AGENT_ORCHESTRATOR.md +39 -264
- package/agents/AGENT_QA.md +58 -186
- package/agents/AGENT_SCRUM.md +23 -190
- package/agents/AGENT_SRE.md +22 -173
- package/agents/AGENT_UIUX.md +26 -162
- package/docs/ARCHITECTURE.md +152 -148
- package/docs/CALCULATIONS.md +17 -17
- package/docs/CHANGELOG.md +112 -49
- package/docs/ENGINEERING_RULES.md +56 -252
- package/docs/IDEAS.md +17 -17
- package/docs/LOCAL_BACKLOG.md +20 -0
- package/docs/STATE.md +111 -43
- package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -58
- package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -58
- package/package.json +59 -47
- package/scripts/auditors/a11y_auditor.js +462 -0
- package/scripts/auditors/code_quality_auditor.js +444 -0
- package/scripts/auditors/contract_test_validator.js +396 -0
- package/scripts/auditors/eval_harness.js +213 -0
- package/scripts/auditors/git_hygiene_linter.js +391 -0
- package/scripts/auditors/pr_reviewer.js +627 -0
- package/scripts/auditors/sast_scanner.js +442 -0
- package/scripts/auditors/sca_dependency_auditor.js +673 -0
- package/scripts/{bootstrap_cli.js → bin/bootstrap_cli.js} +597 -468
- package/scripts/bin/domain_idea_miner.js +658 -0
- package/scripts/bin/smart_onboarding.js +905 -0
- package/scripts/{web_inspector.js → bin/web_inspector.js} +578 -355
- package/scripts/core/architecture_advisor.js +378 -0
- package/scripts/core/ast_skeletonizer.js +413 -0
- package/scripts/core/autonomous_remediation_engine.js +305 -0
- package/scripts/{circuit_breaker.js → core/circuit_breaker.js} +546 -330
- package/scripts/core/context_compressor.js +237 -0
- package/scripts/core/fast_feature_scaffold.js +840 -0
- package/scripts/core/hot_state_cache.js +238 -0
- package/scripts/core/knowledge_gateway.js +888 -0
- package/scripts/{magic_prompt_generator.js → core/magic_prompt_generator.js} +250 -205
- package/scripts/{multi_agent_orchestrator.js → core/multi_agent_orchestrator.js} +421 -258
- package/scripts/{otel_tracer.js → core/otel_tracer.js} +335 -228
- package/scripts/{perf_benchmark.js → core/perf_benchmark.js} +212 -153
- package/scripts/core/sarif_engine.js +299 -0
- package/scripts/{sre_remediation_engine.js → core/sre_remediation_engine.js} +236 -174
- package/scripts/{universal_packager.js → core/universal_packager.js} +357 -313
- package/scripts/integrations/atlassian_client.js +600 -0
- package/scripts/integrations/atlassian_sync_action.js +576 -0
- package/scripts/integrations/confluence_sync.js +684 -0
- package/scripts/integrations/credentials_guard.js +500 -0
- package/scripts/integrations/jira_manager.js +722 -0
- package/scripts/mcp/handlers/atlassian_handlers.js +300 -0
- package/scripts/mcp/handlers/core_handlers.js +419 -0
- package/scripts/mcp/handlers/qa_handlers.js +502 -0
- package/scripts/mcp/handlers/rules_handlers.js +379 -0
- package/scripts/mcp/handlers/scaffold_handlers.js +334 -0
- package/scripts/mcp/handlers/sre_handlers.js +443 -0
- package/scripts/mcp/resources_registry.js +453 -0
- package/scripts/mcp/server.js +408 -0
- package/scripts/mcp/tool_handlers.js +444 -0
- package/scripts/mcp/tools_registry.js +1123 -0
- package/scripts/mcp_server.js +236 -1884
- package/.cursorignore +0 -23
- package/agents/compact/AGENT_ORCHESTRATOR_COMPACTO.md +0 -53
- package/scripts/a11y_auditor.js +0 -187
- package/scripts/ast_skeletonizer.js +0 -220
- package/scripts/atlassian_client.js +0 -238
- package/scripts/atlassian_sync_action.js +0 -269
- package/scripts/code_quality_auditor.js +0 -142
- package/scripts/confluence_sync.js +0 -337
- package/scripts/context_compressor.js +0 -139
- package/scripts/contract_test_validator.js +0 -166
- package/scripts/credentials_guard.js +0 -216
- package/scripts/domain_idea_miner.js +0 -364
- package/scripts/eval_harness.js +0 -100
- package/scripts/fast_feature_scaffold.js +0 -338
- package/scripts/git_hygiene_linter.js +0 -186
- package/scripts/hot_state_cache.js +0 -95
- package/scripts/jira_manager.js +0 -408
- package/scripts/pr_reviewer.js +0 -373
- package/scripts/sast_scanner.js +0 -180
- package/scripts/sca_dependency_auditor.js +0 -167
- package/scripts/smart_onboarding.js +0 -610
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/bin/domain_idea_miner.js
|
|
4
|
+
*
|
|
5
|
+
* Domain Idea Miner & Automated Codebase Opportunity Scanner (v5.1 Enterprise).
|
|
6
|
+
* Varredura inteligente de código-fonte de aplicação para identificar gargalos reais
|
|
7
|
+
* de performance, resiliência, arquitetura e documentação de contratos.
|
|
8
|
+
* Em conformidade estrita com a Regra 14 (Ideation Sanity Filter), Regra 6 (Suggestions),
|
|
9
|
+
* os 10 Princípios Canônicos de Engenharia e o Gate de Arquitetura do AGENT_QA.
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { createSarifReport } = require('../core/sarif_engine.js');
|
|
16
|
+
|
|
17
|
+
// Diretórios ignorados durante a varredura recursiva (imutável)
|
|
18
|
+
const IGNORED_DIRS = Object.freeze(new Set([
|
|
19
|
+
'node_modules',
|
|
20
|
+
'.git',
|
|
21
|
+
'.github',
|
|
22
|
+
'build',
|
|
23
|
+
'target',
|
|
24
|
+
'dist',
|
|
25
|
+
'.idea',
|
|
26
|
+
'.vscode',
|
|
27
|
+
'coverage',
|
|
28
|
+
'.gradle',
|
|
29
|
+
'.mvn',
|
|
30
|
+
'vendor',
|
|
31
|
+
'.agents'
|
|
32
|
+
]));
|
|
33
|
+
|
|
34
|
+
// Extensões de código analisadas (imutável)
|
|
35
|
+
const CODE_EXTENSIONS = Object.freeze(new Set([
|
|
36
|
+
'.java',
|
|
37
|
+
'.kt',
|
|
38
|
+
'.ts',
|
|
39
|
+
'.js',
|
|
40
|
+
'.py',
|
|
41
|
+
'.go'
|
|
42
|
+
]));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Catálogo Canônico e Imutável de Regras de Oportunidade Técnica (Regra 14).
|
|
46
|
+
* Cada regra define um predicado funcional puro (match) e metadados de valor/custo.
|
|
47
|
+
*/
|
|
48
|
+
const IDEA_RULES = Object.freeze([
|
|
49
|
+
{
|
|
50
|
+
id: 'AGDEV-OPP-CACHE',
|
|
51
|
+
ruleId: 'AGDEV-OPP-CACHE',
|
|
52
|
+
name: 'Implementar Cache em Leituras Repetitivas',
|
|
53
|
+
category: 'SPEED',
|
|
54
|
+
severity: 'LOW',
|
|
55
|
+
cost: 'S',
|
|
56
|
+
priority: 85,
|
|
57
|
+
match: (ctx, line, lineNum) => {
|
|
58
|
+
if (ctx.isController && ctx.isJava) {
|
|
59
|
+
if (/@GetMapping/i.test(line) && !ctx.hasCache) {
|
|
60
|
+
return {
|
|
61
|
+
category: 'SPEED',
|
|
62
|
+
title: 'Implementar cache local em memória (Caffeine/Spring Cache) no endpoint de leitura',
|
|
63
|
+
evidence: `${ctx.relativePath}:${lineNum}`,
|
|
64
|
+
value: 'redução de latência p99 < 15ms em consultas repetitivas',
|
|
65
|
+
cost: 'S',
|
|
66
|
+
priority: 85,
|
|
67
|
+
line: lineNum,
|
|
68
|
+
filename: ctx.relativePath
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
} else if (ctx.isController && ctx.isJsTs) {
|
|
72
|
+
if (/(app|router)\.get\s*\(/i.test(line) && !ctx.hasCache) {
|
|
73
|
+
return {
|
|
74
|
+
category: 'SPEED',
|
|
75
|
+
title: 'Adicionar cache HTTP (ETag / Cache-Control) nas rotas de leitura',
|
|
76
|
+
evidence: `${ctx.relativePath}:${lineNum}`,
|
|
77
|
+
value: 'redução de requisições redundantes ao backend',
|
|
78
|
+
cost: 'S',
|
|
79
|
+
priority: 80,
|
|
80
|
+
line: lineNum,
|
|
81
|
+
filename: ctx.relativePath
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'AGDEV-OPP-PAGINATION',
|
|
90
|
+
ruleId: 'AGDEV-OPP-PAGINATION',
|
|
91
|
+
name: 'Suporte a Paginação em Consultas de Banco',
|
|
92
|
+
category: 'RISK',
|
|
93
|
+
severity: 'MEDIUM',
|
|
94
|
+
cost: 'S',
|
|
95
|
+
priority: 90,
|
|
96
|
+
match: (ctx, line, lineNum) => {
|
|
97
|
+
if (ctx.isRepository || ctx.isController) {
|
|
98
|
+
if (/List<[A-Za-z0-9_]+>\s+findAll\s*\(\s*\)/i.test(line) ||
|
|
99
|
+
(ctx.isJava && /List<[A-Za-z0-9_]+>\s+(get|find|list)[A-Za-z0-9_]*\s*\([^)]*\)/i.test(line) && !line.includes('Pageable'))) {
|
|
100
|
+
return {
|
|
101
|
+
category: 'RISK',
|
|
102
|
+
title: 'Adicionar suporte a paginação (Pageable / Page<T>) na listagem de dados',
|
|
103
|
+
evidence: `${ctx.relativePath}:${lineNum}`,
|
|
104
|
+
value: 'prevenção de OutOfMemoryError com crescimento da base de dados',
|
|
105
|
+
cost: 'S',
|
|
106
|
+
priority: 90,
|
|
107
|
+
line: lineNum,
|
|
108
|
+
filename: ctx.relativePath
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'AGDEV-OPP-RESILIENCE',
|
|
117
|
+
ruleId: 'AGDEV-OPP-RESILIENCE',
|
|
118
|
+
name: 'Circuit Breaker e Timeouts Downstream',
|
|
119
|
+
category: 'RISK',
|
|
120
|
+
severity: 'HIGH',
|
|
121
|
+
cost: 'M',
|
|
122
|
+
priority: 95,
|
|
123
|
+
match: (ctx, line, lineNum) => {
|
|
124
|
+
if (/RestTemplate|WebClient|HttpClient|fetch\(|axios\./i.test(line)) {
|
|
125
|
+
if (!ctx.hasCircuitBreaker) {
|
|
126
|
+
return {
|
|
127
|
+
category: 'RISK',
|
|
128
|
+
title: ctx.isJava
|
|
129
|
+
? 'Configurar Circuit Breaker e Timeouts com Resilience4j em chamadas downstream'
|
|
130
|
+
: 'Configurar Timeouts (AbortSignal.timeout) e Circuit Breaker em chamadas HTTP downstream',
|
|
131
|
+
evidence: `${ctx.relativePath}:${lineNum}`,
|
|
132
|
+
value: 'isolamento de falhas em cascata entre serviços',
|
|
133
|
+
cost: 'M',
|
|
134
|
+
priority: 95,
|
|
135
|
+
line: lineNum,
|
|
136
|
+
filename: ctx.relativePath
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'AGDEV-OPP-CONTRACT',
|
|
145
|
+
ruleId: 'AGDEV-OPP-CONTRACT',
|
|
146
|
+
name: 'Especificação de Contratos OpenAPI',
|
|
147
|
+
category: 'CODE',
|
|
148
|
+
severity: 'LOW',
|
|
149
|
+
cost: 'S',
|
|
150
|
+
priority: 70,
|
|
151
|
+
match: (ctx, line, lineNum) => {
|
|
152
|
+
if (ctx.isController && ctx.isJava) {
|
|
153
|
+
if (/@(GetMapping|PostMapping|PutMapping|DeleteMapping)/i.test(line) && !ctx.hasOpenApi) {
|
|
154
|
+
return {
|
|
155
|
+
category: 'CODE',
|
|
156
|
+
title: 'Especificar anotações de contrato OpenAPI 3.0 no Controller',
|
|
157
|
+
evidence: `${ctx.relativePath}:${lineNum}`,
|
|
158
|
+
value: 'catálogo de APIs homologado e documentação viva',
|
|
159
|
+
cost: 'S',
|
|
160
|
+
priority: 70,
|
|
161
|
+
line: lineNum,
|
|
162
|
+
filename: ctx.relativePath
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'AGDEV-OPP-INDEX',
|
|
171
|
+
ruleId: 'AGDEV-OPP-INDEX',
|
|
172
|
+
name: 'Indexação em Colunas de Busca Frequente',
|
|
173
|
+
category: 'SPEED',
|
|
174
|
+
severity: 'LOW',
|
|
175
|
+
cost: 'S',
|
|
176
|
+
priority: 75,
|
|
177
|
+
match: (ctx, line, lineNum) => {
|
|
178
|
+
if (ctx.isEntity && ctx.isJava) {
|
|
179
|
+
if (/@Entity/i.test(line) && !ctx.hasIndexes && ctx.hasSearchableFields) {
|
|
180
|
+
return {
|
|
181
|
+
category: 'SPEED',
|
|
182
|
+
title: 'Configurar índices de banco de dados (@Index) em colunas de busca frequente',
|
|
183
|
+
evidence: `${ctx.relativePath}:${lineNum}`,
|
|
184
|
+
value: 'redução de Full Table Scans em consultas de texto e chaves',
|
|
185
|
+
cost: 'S',
|
|
186
|
+
priority: 75,
|
|
187
|
+
line: lineNum,
|
|
188
|
+
filename: ctx.relativePath
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Cria o contexto sintático do arquivo em O(1), evitando verificações repetidas.
|
|
199
|
+
* @param {string} filePath
|
|
200
|
+
* @param {string} rootDir
|
|
201
|
+
* @param {string} content
|
|
202
|
+
* @returns {object}
|
|
203
|
+
*/
|
|
204
|
+
function createFileContext(filePath, rootDir, content) {
|
|
205
|
+
const safeRootDir = (typeof rootDir === 'string' && rootDir) ? rootDir : process.cwd();
|
|
206
|
+
const safeFilePath = (typeof filePath === 'string' && filePath) ? filePath : '';
|
|
207
|
+
const relativePath = safeFilePath ? path.relative(safeRootDir, safeFilePath).replace(/\\/g, '/') : 'unknown';
|
|
208
|
+
const ext = path.extname(safeFilePath).toLowerCase();
|
|
209
|
+
|
|
210
|
+
const isJava = ext === '.java' || ext === '.kt';
|
|
211
|
+
const isJsTs = ext === '.js' || ext === '.ts';
|
|
212
|
+
const isPython = ext === '.py';
|
|
213
|
+
const isGo = ext === '.go';
|
|
214
|
+
|
|
215
|
+
const isController = /controller|resource|router|endpoint/i.test(relativePath) || /@RestController|@Controller/i.test(content);
|
|
216
|
+
const isRepository = /repository|dao|mapper/i.test(relativePath) || /@Repository/i.test(content);
|
|
217
|
+
const isEntity = /entity|model|domain/i.test(relativePath) || /@Entity|@Table/i.test(content);
|
|
218
|
+
|
|
219
|
+
const hasCache = content.includes('@Cacheable') || content.includes('CacheControl') || content.includes('cache') || content.includes('etag');
|
|
220
|
+
const hasCircuitBreaker = content.includes('@CircuitBreaker') || content.includes('Resilience4j') || content.includes('timeout') || content.includes('AbortSignal');
|
|
221
|
+
const hasOpenApi = content.includes('@Operation') || content.includes('@Tag') || content.includes('swagger') || content.includes('OpenAPI');
|
|
222
|
+
const hasIndexes = content.includes('indexes =');
|
|
223
|
+
const hasSearchableFields = /(?:name|title|code|email|status|slug|identifier|titulo|nome|codigo)/i.test(content);
|
|
224
|
+
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
filePath: safeFilePath,
|
|
227
|
+
relativePath,
|
|
228
|
+
ext,
|
|
229
|
+
isJava,
|
|
230
|
+
isJsTs,
|
|
231
|
+
isPython,
|
|
232
|
+
isGo,
|
|
233
|
+
isController,
|
|
234
|
+
isRepository,
|
|
235
|
+
isEntity,
|
|
236
|
+
hasCache,
|
|
237
|
+
hasCircuitBreaker,
|
|
238
|
+
hasOpenApi,
|
|
239
|
+
hasIndexes,
|
|
240
|
+
hasSearchableFields
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Classe Canônica DomainIdeaMiner
|
|
246
|
+
*/
|
|
247
|
+
class DomainIdeaMiner {
|
|
248
|
+
static get IGNORED_DIRS() {
|
|
249
|
+
return IGNORED_DIRS;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static get CODE_EXTENSIONS() {
|
|
253
|
+
return CODE_EXTENSIONS;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
static get IDEA_RULES() {
|
|
257
|
+
return IDEA_RULES;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
constructor(options = {}) {
|
|
261
|
+
const safeOptions = (options && typeof options === 'object') ? options : {};
|
|
262
|
+
this.rootDir = path.resolve(safeOptions.rootDir || process.cwd());
|
|
263
|
+
this.maxOpenIdeas = Number.isInteger(safeOptions.maxOpenIdeas) && safeOptions.maxOpenIdeas > 0
|
|
264
|
+
? safeOptions.maxOpenIdeas
|
|
265
|
+
: 10;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Coleta recursiva de arquivos de código relevantes com tolerância a falhas.
|
|
270
|
+
* @param {string} [targetDir]
|
|
271
|
+
* @returns {string[]}
|
|
272
|
+
*/
|
|
273
|
+
collectCodeFiles(targetDir) {
|
|
274
|
+
if (targetDir === null) return [];
|
|
275
|
+
const dirToScan = targetDir !== undefined ? targetDir : this.rootDir;
|
|
276
|
+
if (!dirToScan || typeof dirToScan !== 'string' || !fs.existsSync(dirToScan)) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
const safeDir = path.resolve(dirToScan);
|
|
280
|
+
const files = [];
|
|
281
|
+
|
|
282
|
+
const walk = (currentDir) => {
|
|
283
|
+
if (!fs.existsSync(currentDir)) return;
|
|
284
|
+
let entries = [];
|
|
285
|
+
try {
|
|
286
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
287
|
+
} catch (_) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const entry of entries) {
|
|
292
|
+
if (entry.isDirectory()) {
|
|
293
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
294
|
+
walk(path.join(currentDir, entry.name));
|
|
295
|
+
}
|
|
296
|
+
} else if (entry.isFile()) {
|
|
297
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
298
|
+
if (CODE_EXTENSIONS.has(ext)) {
|
|
299
|
+
files.push(path.join(currentDir, entry.name));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
walk(safeDir);
|
|
306
|
+
return files;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Analisa um arquivo e extrai oportunidades de melhoria aplicando o catálogo funcional.
|
|
311
|
+
* @param {string} filePath
|
|
312
|
+
* @param {string} [rootDir=this.rootDir]
|
|
313
|
+
* @returns {object[]}
|
|
314
|
+
*/
|
|
315
|
+
inspectFile(filePath, rootDir = this.rootDir) {
|
|
316
|
+
if (!filePath || typeof filePath !== 'string' || !fs.existsSync(filePath)) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let content = '';
|
|
321
|
+
try {
|
|
322
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
323
|
+
} catch (_) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const ctx = createFileContext(filePath, rootDir, content);
|
|
328
|
+
const lines = content.split('\n');
|
|
329
|
+
const opportunities = [];
|
|
330
|
+
|
|
331
|
+
for (let index = 0; index < lines.length; index++) {
|
|
332
|
+
const lineNum = index + 1;
|
|
333
|
+
const trimmed = lines[index].trim();
|
|
334
|
+
|
|
335
|
+
for (const rule of IDEA_RULES) {
|
|
336
|
+
const opp = rule.match(ctx, trimmed, lineNum);
|
|
337
|
+
if (opp) {
|
|
338
|
+
opportunities.push(opp);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return opportunities;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Executa varredura completa no projeto alvo.
|
|
348
|
+
* @param {string} [targetDir]
|
|
349
|
+
* @returns {object[]}
|
|
350
|
+
*/
|
|
351
|
+
mineIdeas(targetDir) {
|
|
352
|
+
if (targetDir === null) return [];
|
|
353
|
+
const dirToScan = targetDir !== undefined ? targetDir : this.rootDir;
|
|
354
|
+
if (!dirToScan || typeof dirToScan !== 'string' || !fs.existsSync(dirToScan)) {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
const safeDir = path.resolve(dirToScan);
|
|
358
|
+
const files = this.collectCodeFiles(safeDir);
|
|
359
|
+
const allOpportunities = [];
|
|
360
|
+
|
|
361
|
+
for (const file of files) {
|
|
362
|
+
const opps = this.inspectFile(file, safeDir);
|
|
363
|
+
allOpportunities.push(...opps);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Deduplicação determinística por categoria e título similar
|
|
367
|
+
const uniqueMap = new Map();
|
|
368
|
+
for (const opp of allOpportunities) {
|
|
369
|
+
const key = `${opp.category}:${opp.title}`;
|
|
370
|
+
if (!uniqueMap.has(key)) {
|
|
371
|
+
uniqueMap.set(key, opp);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Ordenação por prioridade decrescente
|
|
376
|
+
return Array.from(uniqueMap.values()).sort((a, b) => b.priority - a.priority);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Exporta os resultados da mineração no padrão industrial OASIS SARIF v2.1.0.
|
|
381
|
+
* @param {string} [targetDir]
|
|
382
|
+
* @returns {object} SARIF v2.1.0 report
|
|
383
|
+
*/
|
|
384
|
+
mineToSarif(targetDir) {
|
|
385
|
+
const dirToScan = targetDir !== undefined ? targetDir : this.rootDir;
|
|
386
|
+
const opportunities = this.mineIdeas(dirToScan);
|
|
387
|
+
const findings = opportunities.map(opp => ({
|
|
388
|
+
ruleId: opp.ruleId || `OPP-${opp.category}`,
|
|
389
|
+
name: opp.title,
|
|
390
|
+
severity: opp.category === 'RISK' ? 'HIGH' : (opp.category === 'SPEED' ? 'MEDIUM' : 'LOW'),
|
|
391
|
+
message: `${opp.title} — Impacto: ${opp.value} (Custo: ${opp.cost})`,
|
|
392
|
+
filename: opp.filename || (opp.evidence ? opp.evidence.split(':')[0] : 'unknown'),
|
|
393
|
+
line: opp.line || (opp.evidence ? parseInt(opp.evidence.split(':')[1], 10) : 1),
|
|
394
|
+
column: 1
|
|
395
|
+
}));
|
|
396
|
+
|
|
397
|
+
const rules = IDEA_RULES.map(r => ({
|
|
398
|
+
id: r.id,
|
|
399
|
+
name: r.name,
|
|
400
|
+
severity: r.category === 'RISK' ? 'HIGH' : (r.category === 'SPEED' ? 'MEDIUM' : 'LOW'),
|
|
401
|
+
description: r.name,
|
|
402
|
+
remediation: 'Considere aplicar esta oportunidade de domínio para ganho de arquitetura e mitigação de risco.'
|
|
403
|
+
}));
|
|
404
|
+
|
|
405
|
+
return createSarifReport({
|
|
406
|
+
toolName: 'agent-master-domain-idea-miner',
|
|
407
|
+
toolVersion: '5.1.0',
|
|
408
|
+
rules,
|
|
409
|
+
findings,
|
|
410
|
+
properties: {
|
|
411
|
+
category: 'Domain Architectural Opportunities',
|
|
412
|
+
opportunitiesCount: opportunities.length
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Lê as ideias existentes em IDEAS.md e extrai o maior ID numérico.
|
|
419
|
+
* @param {string} ideasFilePath
|
|
420
|
+
* @returns {object}
|
|
421
|
+
*/
|
|
422
|
+
getExistingIdeas(ideasFilePath) {
|
|
423
|
+
if (!ideasFilePath || typeof ideasFilePath !== 'string' || !fs.existsSync(ideasFilePath)) {
|
|
424
|
+
return { highestId: 0, openIdeas: [], content: '' };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let content = '';
|
|
428
|
+
try {
|
|
429
|
+
content = fs.readFileSync(ideasFilePath, 'utf8');
|
|
430
|
+
} catch (_) {
|
|
431
|
+
return { highestId: 0, openIdeas: [], content: '' };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const lines = content.split(/\r?\n/);
|
|
435
|
+
let highestId = 0;
|
|
436
|
+
const openIdeas = [];
|
|
437
|
+
|
|
438
|
+
for (const line of lines) {
|
|
439
|
+
const trimmed = line.trim();
|
|
440
|
+
const match = trimmed.match(/^- I-(\d+)/);
|
|
441
|
+
if (match) {
|
|
442
|
+
const idNum = parseInt(match[1], 10);
|
|
443
|
+
if (idNum > highestId) highestId = idNum;
|
|
444
|
+
|
|
445
|
+
// Verificar se é aberta
|
|
446
|
+
if (!/\[(CONCLUÍDO|EM PRODUÇÃO|DONE|DROP|REJEITAD[OA]|ARCHIVED)\]/i.test(trimmed)) {
|
|
447
|
+
openIdeas.push(trimmed);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return { highestId, openIdeas, content };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Formata uma oportunidade no formato canônico da Regra 14 / ENGINEERING_RULES.
|
|
457
|
+
* @param {number} idNum
|
|
458
|
+
* @param {object} opp
|
|
459
|
+
* @returns {string}
|
|
460
|
+
*/
|
|
461
|
+
formatCanonicalIdea(idNum, opp) {
|
|
462
|
+
if (!opp || typeof opp !== 'object') return '';
|
|
463
|
+
return `- I-${idNum} [${opp.category || 'CODE'}] ${opp.title || ''} | evidence: ${opp.evidence || 'N/A'} | value: ${opp.value || 'N/A'} | cost: ${opp.cost || 'S'}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Sincroniza as ideias mineradas com docs/IDEAS.md.
|
|
468
|
+
* @param {object[]} minedIdeas
|
|
469
|
+
* @param {string} ideasFilePath
|
|
470
|
+
* @param {object} [options={}]
|
|
471
|
+
* @returns {object}
|
|
472
|
+
*/
|
|
473
|
+
syncWithIdeasFile(minedIdeas = [], ideasFilePath, options = {}) {
|
|
474
|
+
const safeIdeas = Array.isArray(minedIdeas) ? minedIdeas : [];
|
|
475
|
+
const safePath = (typeof ideasFilePath === 'string' && ideasFilePath) ? ideasFilePath : '';
|
|
476
|
+
const { highestId, openIdeas } = this.getExistingIdeas(safePath);
|
|
477
|
+
let nextId = highestId + 1;
|
|
478
|
+
const newlyAdded = [];
|
|
479
|
+
|
|
480
|
+
const existingNormalized = openIdeas.map(i => i.toLowerCase());
|
|
481
|
+
|
|
482
|
+
for (const opp of safeIdeas) {
|
|
483
|
+
if (openIdeas.length + newlyAdded.length >= this.maxOpenIdeas) {
|
|
484
|
+
break; // Limite de 10 ideias abertas estritamente respeitado (Regra 14)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Evita duplicatas conceituais
|
|
488
|
+
const titleLower = String(opp.title || '').toLowerCase();
|
|
489
|
+
const evidenceLower = String(opp.evidence || '').toLowerCase();
|
|
490
|
+
const isDuplicate = existingNormalized.some(ex =>
|
|
491
|
+
(titleLower && ex.includes(titleLower)) ||
|
|
492
|
+
(evidenceLower && ex.includes(evidenceLower))
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
if (!isDuplicate) {
|
|
496
|
+
const canonicalLine = this.formatCanonicalIdea(nextId, opp);
|
|
497
|
+
newlyAdded.push(canonicalLine);
|
|
498
|
+
nextId++;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!safePath || options.dryRun || newlyAdded.length === 0) {
|
|
503
|
+
return {
|
|
504
|
+
applied: false,
|
|
505
|
+
newlyAdded,
|
|
506
|
+
totalOpenCount: openIdeas.length + newlyAdded.length
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Leitura e inserção atômica no arquivo IDEAS.md
|
|
511
|
+
let currentContent = fs.existsSync(safePath) ? fs.readFileSync(safePath, 'utf8') : '';
|
|
512
|
+
|
|
513
|
+
// Se o arquivo tiver uma seção de backlog aberto, anexa nela
|
|
514
|
+
const sectionHeader = '## 🎯 Backlog Aberto de Domínio';
|
|
515
|
+
if (currentContent.includes(sectionHeader)) {
|
|
516
|
+
const parts = currentContent.split(sectionHeader);
|
|
517
|
+
const afterHeader = parts[1];
|
|
518
|
+
const nextDividerIndex = afterHeader.indexOf('\n---');
|
|
519
|
+
|
|
520
|
+
if (nextDividerIndex !== -1) {
|
|
521
|
+
const insertion = newlyAdded.join('\n') + '\n';
|
|
522
|
+
const sanitizedAfterContent = afterHeader.slice(0, nextDividerIndex) + '\n' + insertion + afterHeader.slice(nextDividerIndex);
|
|
523
|
+
currentContent = parts[0] + sectionHeader + sanitizedAfterContent;
|
|
524
|
+
} else {
|
|
525
|
+
currentContent += '\n' + newlyAdded.join('\n') + '\n';
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
// Cria template básico se não existir
|
|
529
|
+
currentContent = `# IDEAS (Backlog Vivo de Oportunidades & Inovação de Domínio)\n\n` +
|
|
530
|
+
`Este documento registra o backlog rotativo de melhorias e hipóteses técnicas em aberto, auditadas pelo **AGENT_QA (Shift-Left Gatekeeper)** e estritamente aderentes à **Regra 14 (Filtro de Sanidade Contextual)**.\n\n` +
|
|
531
|
+
`## 🎯 Backlog Aberto de Domínio (Até 10 Itens Ordenados por Valor)\n\n` +
|
|
532
|
+
newlyAdded.join('\n') + '\n';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
const parentDir = path.dirname(safePath);
|
|
537
|
+
if (!fs.existsSync(parentDir)) {
|
|
538
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
539
|
+
}
|
|
540
|
+
fs.writeFileSync(safePath, currentContent, 'utf8');
|
|
541
|
+
return {
|
|
542
|
+
applied: true,
|
|
543
|
+
newlyAdded,
|
|
544
|
+
totalOpenCount: openIdeas.length + newlyAdded.length
|
|
545
|
+
};
|
|
546
|
+
} catch (_) {
|
|
547
|
+
return {
|
|
548
|
+
applied: false,
|
|
549
|
+
newlyAdded: [],
|
|
550
|
+
totalOpenCount: openIdeas.length
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Formata relatório executivo em Markdown do escaneamento.
|
|
557
|
+
* @param {object} result
|
|
558
|
+
* @returns {string}
|
|
559
|
+
*/
|
|
560
|
+
formatReport(result) {
|
|
561
|
+
return formatReport(result);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Formata relatório executivo em Markdown da mineração de ideias.
|
|
567
|
+
* @param {object} result
|
|
568
|
+
* @returns {string}
|
|
569
|
+
*/
|
|
570
|
+
function formatReport(result) {
|
|
571
|
+
if (!result || typeof result !== 'object') {
|
|
572
|
+
return '# 💡 Agent-Master DevSecOps — Domain Idea Miner Report\n\nNenhuma oportunidade minerada ou resultado indisponível.';
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
let output = '# 💡 Agent-Master DevSecOps — Domain Idea Miner Report\n\n';
|
|
576
|
+
output += `**Arquivos Analisados:** ${result.scannedFilesCount || 0}\n`;
|
|
577
|
+
output += `**Oportunidades Identificadas:** ${result.opportunitiesFound || (result.opportunities ? result.opportunities.length : 0)}\n\n`;
|
|
578
|
+
|
|
579
|
+
const opps = Array.isArray(result.opportunities) ? result.opportunities : [];
|
|
580
|
+
if (opps.length === 0) {
|
|
581
|
+
output += '✅ **Nenhum gargalo evidente detectado.** O código analisado atende aos padrões de resiliência, performance e contratos.\n';
|
|
582
|
+
} else {
|
|
583
|
+
output += '### 🎯 Oportunidades por Prioridade (Regra 14)\n\n';
|
|
584
|
+
opps.forEach((opp, idx) => {
|
|
585
|
+
output += `${idx + 1}. **[${opp.category}]** ${opp.title}\n`;
|
|
586
|
+
output += ` - **Evidência:** \`${opp.evidence}\`\n`;
|
|
587
|
+
output += ` - **Valor:** ${opp.value}\n`;
|
|
588
|
+
output += ` - **Custo:** \`${opp.cost}\` | **Prioridade:** \`${opp.priority}\`\n\n`;
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (result.syncResult) {
|
|
593
|
+
output += '### 📝 Sincronização com `docs/IDEAS.md`\n';
|
|
594
|
+
output += `- **Aplicado no Disco:** ${result.syncResult.applied ? 'SIM' : 'NÃO (Simulação / Sem novas ideias)'}\n`;
|
|
595
|
+
output += `- **Novas Ideias Adicionadas:** ${result.syncResult.newlyAdded ? result.syncResult.newlyAdded.length : 0}\n`;
|
|
596
|
+
output += `- **Total de Ideias Abertas:** ${result.syncResult.totalOpenCount || 0}/10\n`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return output;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Helper direto para execução programática compatível com toda a suíte.
|
|
604
|
+
* @param {string} [targetDir=process.cwd()]
|
|
605
|
+
* @param {object} [options={}]
|
|
606
|
+
* @returns {object}
|
|
607
|
+
*/
|
|
608
|
+
function mineDomainIdeas(targetDir = process.cwd(), options = {}) {
|
|
609
|
+
const safeTargetDir = (typeof targetDir === 'string' && targetDir) ? targetDir : process.cwd();
|
|
610
|
+
const safeOptions = (options && typeof options === 'object') ? options : {};
|
|
611
|
+
const miner = new DomainIdeaMiner({ rootDir: safeTargetDir, maxOpenIdeas: safeOptions.maxOpenIdeas || 10 });
|
|
612
|
+
const opportunities = miner.mineIdeas(safeTargetDir);
|
|
613
|
+
const ideasFilePath = safeOptions.ideasFilePath || path.join(safeTargetDir, 'docs', 'IDEAS.md');
|
|
614
|
+
|
|
615
|
+
let syncResult = { applied: false, newlyAdded: [] };
|
|
616
|
+
if (safeOptions.autoApply) {
|
|
617
|
+
syncResult = miner.syncWithIdeasFile(opportunities, ideasFilePath, { dryRun: safeOptions.dryRun });
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
scannedFilesCount: miner.collectCodeFiles(safeTargetDir).length,
|
|
622
|
+
opportunitiesFound: opportunities.length,
|
|
623
|
+
opportunities,
|
|
624
|
+
syncResult
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
module.exports = {
|
|
629
|
+
DomainIdeaMiner,
|
|
630
|
+
mineDomainIdeas,
|
|
631
|
+
formatReport,
|
|
632
|
+
IDEA_RULES,
|
|
633
|
+
IGNORED_DIRS,
|
|
634
|
+
CODE_EXTENSIONS
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// Execução CLI direta quando chamado via terminal
|
|
638
|
+
if (require.main === module) {
|
|
639
|
+
const args = process.argv.slice(2);
|
|
640
|
+
const autoApply = args.includes('--apply');
|
|
641
|
+
const dryRun = args.includes('--dry-run');
|
|
642
|
+
const targetDir = process.cwd();
|
|
643
|
+
|
|
644
|
+
console.log(`🔍 [Domain Idea Miner] Iniciando varredura em: ${targetDir}`);
|
|
645
|
+
const result = mineDomainIdeas(targetDir, { autoApply, dryRun });
|
|
646
|
+
|
|
647
|
+
console.log(`📊 Arquivos analisados: ${result.scannedFilesCount}`);
|
|
648
|
+
console.log(`💡 Oportunidades identificadas: ${result.opportunitiesFound}`);
|
|
649
|
+
|
|
650
|
+
result.opportunities.forEach((opp, i) => {
|
|
651
|
+
console.log(` ${i + 1}. [${opp.category}] ${opp.title}`);
|
|
652
|
+
console.log(` Evidência: ${opp.evidence} | Valor: ${opp.value}`);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
if (autoApply) {
|
|
656
|
+
console.log(`📝 Ideias sincronizadas com IDEAS.md: ${result.syncResult.newlyAdded.length} adicionadas.`);
|
|
657
|
+
}
|
|
658
|
+
}
|