agent-master-devsecops 0.0.0-stage → 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 +204 -2
- package/agents/AGENT_DEVSECOPS.md +32 -0
- package/agents/AGENT_INFRA.md +31 -0
- package/agents/AGENT_ORCHESTRATOR.md +39 -0
- package/agents/AGENT_QA.md +58 -0
- package/agents/AGENT_SCRUM.md +23 -0
- package/agents/AGENT_SRE.md +22 -0
- package/agents/AGENT_UIUX.md +26 -0
- package/docs/ARCHITECTURE.md +152 -0
- package/docs/CALCULATIONS.md +17 -0
- package/docs/CHANGELOG.md +112 -0
- package/docs/ENGINEERING_RULES.md +56 -0
- package/docs/IDEAS.md +17 -0
- package/docs/LOCAL_BACKLOG.md +20 -0
- package/docs/STATE.md +111 -0
- package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -0
- package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -0
- package/package.json +57 -4
- 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/bin/bootstrap_cli.js +597 -0
- package/scripts/bin/domain_idea_miner.js +658 -0
- package/scripts/bin/smart_onboarding.js +905 -0
- package/scripts/bin/web_inspector.js +578 -0
- 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/core/circuit_breaker.js +546 -0
- 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/core/magic_prompt_generator.js +250 -0
- package/scripts/core/multi_agent_orchestrator.js +421 -0
- package/scripts/core/otel_tracer.js +335 -0
- package/scripts/core/perf_benchmark.js +212 -0
- package/scripts/core/sarif_engine.js +299 -0
- package/scripts/core/sre_remediation_engine.js +236 -0
- package/scripts/core/universal_packager.js +357 -0
- 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 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/otel_tracer.js
|
|
4
|
+
*
|
|
5
|
+
* Tracer Distribuído OpenTelemetry Nativo (AGDEV-24 / I-8).
|
|
6
|
+
* Suporte a W3C TraceContext (traceparent), hierarquia de spans (parent-child),
|
|
7
|
+
* exportação OTLP JSON e renderização de flamegraphs para playbooks de SRE.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { version: PKG_VERSION } = require('../../package.json');
|
|
12
|
+
const { createSarifReport } = require('./sarif_engine.js');
|
|
13
|
+
|
|
14
|
+
function getOfficialOtelApi() {
|
|
15
|
+
try {
|
|
16
|
+
return require('@opentelemetry/api');
|
|
17
|
+
} catch (_) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isOfficialOtelApiAvailable() {
|
|
23
|
+
return getOfficialOtelApi() !== null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function generateTraceId() {
|
|
27
|
+
return crypto.randomBytes(16).toString('hex');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function generateSpanId() {
|
|
31
|
+
return crypto.randomBytes(8).toString('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatTraceparent(traceId, spanId, sampled = true) {
|
|
35
|
+
const flags = sampled ? '01' : '00';
|
|
36
|
+
return `00-${traceId}-${spanId}-${flags}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseTraceparent(traceparent) {
|
|
40
|
+
if (!traceparent || typeof traceparent !== 'string') return null;
|
|
41
|
+
const parts = traceparent.split('-');
|
|
42
|
+
if (parts.length !== 4) return null;
|
|
43
|
+
return {
|
|
44
|
+
version: parts[0],
|
|
45
|
+
traceId: parts[1],
|
|
46
|
+
spanId: parts[2],
|
|
47
|
+
flags: parts[3],
|
|
48
|
+
sampled: parts[3] === '01'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class Span {
|
|
53
|
+
constructor(name, tracer, options = {}) {
|
|
54
|
+
this.name = name;
|
|
55
|
+
this.tracer = tracer;
|
|
56
|
+
this.traceId = options.traceId || tracer.traceId;
|
|
57
|
+
this.spanId = generateSpanId();
|
|
58
|
+
this.parentSpanId = options.parentSpanId || null;
|
|
59
|
+
this.startTime = options.startTime || Date.now();
|
|
60
|
+
this.endTime = null;
|
|
61
|
+
this.durationMs = 0;
|
|
62
|
+
this.attributes = Object.assign({}, options.attributes || {});
|
|
63
|
+
this.events = [];
|
|
64
|
+
this.status = { code: 'UNSET' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setAttribute(key, value) {
|
|
68
|
+
this.attributes[key] = value;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
addEvent(name, attributes = {}) {
|
|
73
|
+
this.events.push({
|
|
74
|
+
name,
|
|
75
|
+
time: Date.now(),
|
|
76
|
+
attributes
|
|
77
|
+
});
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setStatus(code, message = '') {
|
|
82
|
+
this.status = { code, message };
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
end(endTime = Date.now()) {
|
|
87
|
+
this.endTime = endTime;
|
|
88
|
+
this.durationMs = Math.max(0, this.endTime - this.startTime);
|
|
89
|
+
this.tracer._recordSpan(this);
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class Tracer {
|
|
95
|
+
constructor(serviceName, options = {}) {
|
|
96
|
+
this.serviceName = serviceName;
|
|
97
|
+
this.traceId = options.traceId || generateTraceId();
|
|
98
|
+
this.spans = [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
startSpan(name, options = {}) {
|
|
102
|
+
return new Span(name, this, options);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_recordSpan(span) {
|
|
106
|
+
this.spans.push(span);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getTraceContext(activeSpan = null) {
|
|
110
|
+
const spanId = activeSpan ? activeSpan.spanId : (this.spans[0] ? this.spans[0].spanId : generateSpanId());
|
|
111
|
+
return {
|
|
112
|
+
traceId: this.traceId,
|
|
113
|
+
spanId,
|
|
114
|
+
traceparent: formatTraceparent(this.traceId, spanId)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
analyzeBottlenecks(thresholdMs = 50) {
|
|
119
|
+
const bottlenecks = [];
|
|
120
|
+
const totalDuration = this.spans.reduce((max, s) => Math.max(max, (s.endTime || s.startTime) - this.spans[0].startTime), 0) || 1;
|
|
121
|
+
|
|
122
|
+
for (const span of this.spans) {
|
|
123
|
+
const percentage = Math.round((span.durationMs / totalDuration) * 100);
|
|
124
|
+
if (span.durationMs >= thresholdMs || percentage >= 40) {
|
|
125
|
+
bottlenecks.push({
|
|
126
|
+
name: span.name,
|
|
127
|
+
durationMs: span.durationMs,
|
|
128
|
+
percentageTotal: percentage,
|
|
129
|
+
severity: percentage >= 70 ? 'CRÍTICA' : (percentage >= 40 ? 'ALTA' : 'MÉDIA'),
|
|
130
|
+
spanId: span.spanId
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return bottlenecks;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
renderFlamegraphAscii() {
|
|
139
|
+
if (this.spans.length === 0) return 'Trace vazio.';
|
|
140
|
+
|
|
141
|
+
const sorted = [...this.spans].sort((a, b) => a.startTime - b.startTime);
|
|
142
|
+
const traceStart = sorted[0].startTime;
|
|
143
|
+
const traceEnd = Math.max(...sorted.map(s => s.endTime || s.startTime));
|
|
144
|
+
const totalDuration = Math.max(1, traceEnd - traceStart);
|
|
145
|
+
|
|
146
|
+
let ascii = `=== FLAMEGRAPH DISTRIBUÍDO [Trace: ${this.traceId}] (${totalDuration}ms) ===\n`;
|
|
147
|
+
|
|
148
|
+
sorted.forEach((span, index) => {
|
|
149
|
+
const offsetMs = span.startTime - traceStart;
|
|
150
|
+
const offsetChars = Math.floor((offsetMs / totalDuration) * 30);
|
|
151
|
+
const barLength = Math.max(1, Math.floor((span.durationMs / totalDuration) * 40));
|
|
152
|
+
const indent = ' '.repeat(offsetChars);
|
|
153
|
+
const bar = '█'.repeat(barLength);
|
|
154
|
+
|
|
155
|
+
ascii += `[${String(index + 1).padStart(2, '0')}] ${indent}${bar} ${span.name} (${span.durationMs}ms)\n`;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return ascii;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
exportOtlpJson() {
|
|
162
|
+
return {
|
|
163
|
+
resourceSpans: [
|
|
164
|
+
{
|
|
165
|
+
resource: {
|
|
166
|
+
attributes: [
|
|
167
|
+
{ key: 'service.name', value: { stringValue: this.serviceName } }
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
scopeSpans: [
|
|
171
|
+
{
|
|
172
|
+
scope: { name: 'agent-master-devsecops-tracer', version: PKG_VERSION },
|
|
173
|
+
spans: this.spans.map(s => ({
|
|
174
|
+
traceId: s.traceId,
|
|
175
|
+
spanId: s.spanId,
|
|
176
|
+
parentSpanId: s.parentSpanId,
|
|
177
|
+
name: s.name,
|
|
178
|
+
startTimeUnixNano: String(s.startTime * 1000000),
|
|
179
|
+
endTimeUnixNano: String((s.endTime || s.startTime) * 1000000),
|
|
180
|
+
attributes: Object.entries(s.attributes).map(([k, v]) => ({
|
|
181
|
+
key: k,
|
|
182
|
+
value: { stringValue: String(v) }
|
|
183
|
+
})),
|
|
184
|
+
status: s.status
|
|
185
|
+
}))
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async sendToOtlpEndpoint(endpointUrl, options = {}) {
|
|
194
|
+
const payload = JSON.stringify(this.exportOtlpJson());
|
|
195
|
+
const parsedUrl = new URL(endpointUrl);
|
|
196
|
+
const transport = parsedUrl.protocol === 'https:' ? require('node:https') : require('node:http');
|
|
197
|
+
const timeoutMs = options.timeoutMs || 5000;
|
|
198
|
+
const customHeaders = options.headers || {};
|
|
199
|
+
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
const req = transport.request(parsedUrl, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: Object.assign({
|
|
204
|
+
'Content-Type': 'application/json',
|
|
205
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
206
|
+
'User-Agent': `agent-master-otel-tracer/${PKG_VERSION}`
|
|
207
|
+
}, customHeaders),
|
|
208
|
+
timeout: timeoutMs
|
|
209
|
+
}, (res) => {
|
|
210
|
+
let responseBody = '';
|
|
211
|
+
res.setEncoding('utf8');
|
|
212
|
+
res.on('data', chunk => { responseBody += chunk; });
|
|
213
|
+
res.on('end', () => {
|
|
214
|
+
const success = res.statusCode >= 200 && res.statusCode < 300;
|
|
215
|
+
resolve({
|
|
216
|
+
success,
|
|
217
|
+
statusCode: res.statusCode,
|
|
218
|
+
statusMessage: res.statusMessage,
|
|
219
|
+
body: responseBody
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
req.on('timeout', () => {
|
|
225
|
+
req.destroy();
|
|
226
|
+
reject(new Error(`Timeout ao exportar spans para OTLP endpoint: ${endpointUrl} (${timeoutMs}ms)`));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
req.on('error', (err) => {
|
|
230
|
+
reject(new Error(`Falha de conexão ao exportar OTLP para ${endpointUrl}: ${err.message}`));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
req.write(payload);
|
|
234
|
+
req.end();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
auditToSarif(bottleneckThresholdMs = 50) {
|
|
239
|
+
const bottlenecks = this.analyzeBottlenecks(bottleneckThresholdMs);
|
|
240
|
+
const findings = [];
|
|
241
|
+
|
|
242
|
+
for (const b of bottlenecks) {
|
|
243
|
+
findings.push({
|
|
244
|
+
ruleId: 'RULE-TRACE-001',
|
|
245
|
+
name: 'DistributedSpanBottleneck',
|
|
246
|
+
severity: 'WARNING',
|
|
247
|
+
message: `Span [${b.name}] consumiu ${b.durationMs}ms (${b.pctOfTotalTrace}% do trace total), excedendo o limiar de ${bottleneckThresholdMs}ms.`,
|
|
248
|
+
remediation: 'Otimize chamadas concorrentes ou adicione cache em operações I/O pesadas.'
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return createSarifReport({
|
|
253
|
+
toolName: 'agent-master-otel-tracer',
|
|
254
|
+
rules: [
|
|
255
|
+
{
|
|
256
|
+
id: 'RULE-TRACE-001',
|
|
257
|
+
name: 'DistributedSpanBottleneck',
|
|
258
|
+
description: 'Identifica spans distribuídos que excedem o tempo de resposta aceitável',
|
|
259
|
+
severity: 'MEDIUM',
|
|
260
|
+
cwe: 'CWE-400'
|
|
261
|
+
}
|
|
262
|
+
],
|
|
263
|
+
findings,
|
|
264
|
+
properties: {
|
|
265
|
+
serviceName: this.serviceName,
|
|
266
|
+
totalSpans: this.spans.length,
|
|
267
|
+
bottlenecksCount: bottlenecks.length
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
formatReport() {
|
|
273
|
+
return [
|
|
274
|
+
`### Relatório de Rastreabilidade Distribuída (OpenTelemetry)`,
|
|
275
|
+
`- **Serviço:** \`${this.serviceName}\``,
|
|
276
|
+
`- **Spans Registrados:** ${this.spans.length}`,
|
|
277
|
+
'```text',
|
|
278
|
+
this.renderFlamegraphAscii(),
|
|
279
|
+
'```'
|
|
280
|
+
].join('\n');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
function simulateMultiAgentTrace(serviceName = 'agent-pipeline') {
|
|
286
|
+
const tracer = new Tracer(serviceName);
|
|
287
|
+
|
|
288
|
+
// 1. Root span: Requisição do Usuário
|
|
289
|
+
const root = tracer.startSpan('user_goal_execution').setAttribute('agent.role', 'Orchestrator');
|
|
290
|
+
|
|
291
|
+
// 2. Sub-span: Arquiteto planeja
|
|
292
|
+
const planSpan = tracer.startSpan('architect_plan', { parentSpanId: root.spanId })
|
|
293
|
+
.setAttribute('agent.role', 'Arquiteto')
|
|
294
|
+
.setAttribute('model', 'Claude 3.5 Sonnet');
|
|
295
|
+
// Simula 25ms
|
|
296
|
+
planSpan.end(planSpan.startTime + 25);
|
|
297
|
+
|
|
298
|
+
// 3. Sub-span: DevSecOps implementa
|
|
299
|
+
const devSpan = tracer.startSpan('devsecops_implementation', { parentSpanId: root.spanId })
|
|
300
|
+
.setAttribute('agent.role', 'DevSecOps Lead');
|
|
301
|
+
devSpan.end(devSpan.startTime + 60);
|
|
302
|
+
|
|
303
|
+
// 4. Sub-span: QA audita
|
|
304
|
+
const revSpan = tracer.startSpan('qa_audit', { parentSpanId: root.spanId })
|
|
305
|
+
.setAttribute('agent.role', 'QA Independente');
|
|
306
|
+
revSpan.end(revSpan.startTime + 15);
|
|
307
|
+
|
|
308
|
+
// 5. Finaliza root span
|
|
309
|
+
root.end(root.startTime + 105);
|
|
310
|
+
|
|
311
|
+
return tracer;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function runCli() {
|
|
315
|
+
const tracer = simulateMultiAgentTrace();
|
|
316
|
+
console.log(tracer.renderFlamegraphAscii());
|
|
317
|
+
console.log('\nGargalos Identificados (Threshold 30ms):');
|
|
318
|
+
console.log(JSON.stringify(tracer.analyzeBottlenecks(30), null, 2));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (require.main === module) {
|
|
322
|
+
runCli().catch(console.error);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
generateTraceId,
|
|
327
|
+
generateSpanId,
|
|
328
|
+
formatTraceparent,
|
|
329
|
+
parseTraceparent,
|
|
330
|
+
Span,
|
|
331
|
+
Tracer,
|
|
332
|
+
simulateMultiAgentTrace,
|
|
333
|
+
getOfficialOtelApi,
|
|
334
|
+
isOfficialOtelApiAvailable
|
|
335
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/perf_benchmark.js
|
|
4
|
+
*
|
|
5
|
+
* Scanner de Regressão de Performance em CI (AGDEV-25 / I-9).
|
|
6
|
+
* Executa micro-benchmarks comparativos de latência e consumo de RAM/Heap,
|
|
7
|
+
* detectando e bloqueando regressões superiores a 10% em relação ao baseline de referência.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { createSarifReport } = require('./sarif_engine.js');
|
|
13
|
+
|
|
14
|
+
const ROOT_DIR = path.resolve(__dirname, '../..');
|
|
15
|
+
const BASELINE_FILE = path.join(ROOT_DIR, 'benchmarks', 'baseline.json');
|
|
16
|
+
|
|
17
|
+
function runBenchmark(iterations = 100) {
|
|
18
|
+
const { processMessage } = require('../mcp_server.js');
|
|
19
|
+
const { generateCompactPrompt } = require('./context_compressor.js');
|
|
20
|
+
|
|
21
|
+
const startMem = process.memoryUsage();
|
|
22
|
+
const startTime = process.hrtime.bigint();
|
|
23
|
+
|
|
24
|
+
// Benchmark 1: Processamento de JSON-RPC Initialize e Tools List
|
|
25
|
+
for (let i = 0; i < iterations; i++) {
|
|
26
|
+
processMessage({ jsonrpc: '2.0', id: i, method: 'tools/list' });
|
|
27
|
+
processMessage({ jsonrpc: '2.0', id: i, method: 'tools/call', params: { name: 'agent_full_dev_status', arguments: {} } });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Benchmark 2: Compactação Semântica de Prompt
|
|
31
|
+
for (let i = 0; i < Math.floor(iterations / 2); i++) {
|
|
32
|
+
generateCompactPrompt();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const endTime = process.hrtime.bigint();
|
|
36
|
+
const endMem = process.memoryUsage();
|
|
37
|
+
|
|
38
|
+
const durationNs = Number(endTime - startTime);
|
|
39
|
+
const durationMs = durationNs / 1000000;
|
|
40
|
+
const avgLatencyUs = Math.round((durationNs / (iterations * 2)) / 1000);
|
|
41
|
+
const heapUsedMb = Math.round((endMem.heapUsed / (1024 * 1024)) * 100) / 100;
|
|
42
|
+
const opsPerSec = Math.round(((iterations * 2.5) / (durationMs / 1000)));
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
iterations,
|
|
46
|
+
totalDurationMs: Math.round(durationMs * 100) / 100,
|
|
47
|
+
avgLatencyUs,
|
|
48
|
+
opsPerSec,
|
|
49
|
+
heapUsedMb
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function comparePerformance(current, baseline, maxDegradationPct = 10) {
|
|
54
|
+
const latencyDeltaPct = Math.round(((current.avgLatencyUs - baseline.avgLatencyUs) / baseline.avgLatencyUs) * 100);
|
|
55
|
+
const memoryDeltaPct = Math.round(((current.heapUsedMb - baseline.heapUsedMb) / (baseline.heapUsedMb || 1)) * 100);
|
|
56
|
+
|
|
57
|
+
const hasLatencyRegression = latencyDeltaPct > maxDegradationPct;
|
|
58
|
+
const hasMemoryRegression = memoryDeltaPct > (maxDegradationPct * 2); // 20% tolerância para GC jitter
|
|
59
|
+
const passed = !hasLatencyRegression && !hasMemoryRegression;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
passed,
|
|
63
|
+
verdict: passed ? 'APROVADO' : 'REGRESSÃO_DETECTADA',
|
|
64
|
+
thresholdPct: maxDegradationPct,
|
|
65
|
+
current,
|
|
66
|
+
baseline,
|
|
67
|
+
deltas: {
|
|
68
|
+
latencyDeltaPct,
|
|
69
|
+
memoryDeltaPct
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatPerfReport(comparison) {
|
|
75
|
+
let report = '================================================================================\n';
|
|
76
|
+
report += `RELATÓRIO DE PERFORMANCE & REGRESSÃO DE CI: [ ${comparison.verdict} ]\n`;
|
|
77
|
+
report += '================================================================================\n\n';
|
|
78
|
+
|
|
79
|
+
report += `Limiar Máximo de Degradação Permitido: ${comparison.thresholdPct}%\n\n`;
|
|
80
|
+
|
|
81
|
+
report += 'MÉTRICAS COMPARATIVAS:\n';
|
|
82
|
+
report += `- Latência Média por Operação: ${comparison.current.avgLatencyUs}µs (Baseline: ${comparison.baseline.avgLatencyUs}µs) -> Variação: ${comparison.deltas.latencyDeltaPct > 0 ? '+' : ''}${comparison.deltas.latencyDeltaPct}%\n`;
|
|
83
|
+
report += `- Throughput (Ops/sec): ${comparison.current.opsPerSec} ops/s (Baseline: ${comparison.baseline.opsPerSec} ops/s)\n`;
|
|
84
|
+
report += `- Memória Heap Utilizada: ${comparison.current.heapUsedMb} MB (Baseline: ${comparison.baseline.heapUsedMb} MB) -> Variação: ${comparison.deltas.memoryDeltaPct > 0 ? '+' : ''}${comparison.deltas.memoryDeltaPct}%\n\n`;
|
|
85
|
+
|
|
86
|
+
if (comparison.passed) {
|
|
87
|
+
report += 'STATUS: Nenhuma degradação crítica detectada. Aprovado para merge na esteira de CI.\n';
|
|
88
|
+
} else {
|
|
89
|
+
report += 'ALERTA SRE: Regressão superior a 10% identificada! Merge bloqueado conforme política DevSecOps.\n';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
report += '================================================================================\n';
|
|
93
|
+
return report;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getOrInitBaseline() {
|
|
97
|
+
const benchDir = path.dirname(BASELINE_FILE);
|
|
98
|
+
if (!fs.existsSync(benchDir)) {
|
|
99
|
+
fs.mkdirSync(benchDir, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (fs.existsSync(BASELINE_FILE)) {
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(fs.readFileSync(BASELINE_FILE, 'utf8'));
|
|
105
|
+
} catch (err) {
|
|
106
|
+
void err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Baseline padrão de referência calibrado para Node.js 22
|
|
111
|
+
const initialBaseline = {
|
|
112
|
+
iterations: 100,
|
|
113
|
+
totalDurationMs: 15.0,
|
|
114
|
+
avgLatencyUs: 60,
|
|
115
|
+
opsPerSec: 15000,
|
|
116
|
+
heapUsedMb: 18.5
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
fs.writeFileSync(BASELINE_FILE, JSON.stringify(initialBaseline, null, 2), 'utf8');
|
|
120
|
+
return initialBaseline;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runCli() {
|
|
124
|
+
const args = process.argv.slice(2);
|
|
125
|
+
const baseline = getOrInitBaseline();
|
|
126
|
+
const current = runBenchmark(100);
|
|
127
|
+
|
|
128
|
+
if (args.includes('--save-baseline')) {
|
|
129
|
+
fs.writeFileSync(BASELINE_FILE, JSON.stringify(current, null, 2), 'utf8');
|
|
130
|
+
console.log(`[Perf Scanner] Novo baseline salvo com sucesso em ${BASELINE_FILE}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const comparison = comparePerformance(current, baseline, 15);
|
|
135
|
+
const report = formatPerfReport(comparison);
|
|
136
|
+
console.log(report);
|
|
137
|
+
|
|
138
|
+
if (args.includes('--strict') && !comparison.passed) {
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (require.main === module) {
|
|
144
|
+
runCli().catch(err => {
|
|
145
|
+
console.error(err);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function auditToSarif(iterations = 50, thresholdPct = 15) {
|
|
151
|
+
const baseline = getOrInitBaseline();
|
|
152
|
+
const current = runBenchmark(iterations);
|
|
153
|
+
const comparison = comparePerformance(current, baseline, thresholdPct);
|
|
154
|
+
const findings = [];
|
|
155
|
+
|
|
156
|
+
if (!comparison.passed) {
|
|
157
|
+
findings.push({
|
|
158
|
+
ruleId: 'RULE-PERF-001',
|
|
159
|
+
name: 'PerformanceRegressionDetected',
|
|
160
|
+
severity: 'WARNING',
|
|
161
|
+
message: `Regressão de performance detectada: Latência +${comparison.deltas.latencyDeltaPct}% (Limiar: ${thresholdPct}%).`,
|
|
162
|
+
remediation: 'Investigue o overhead recente ou recalibre o baseline em benchmarks/baseline.json.'
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return createSarifReport({
|
|
167
|
+
toolName: 'agent-master-perf-benchmark',
|
|
168
|
+
rules: [
|
|
169
|
+
{
|
|
170
|
+
id: 'RULE-PERF-001',
|
|
171
|
+
name: 'PerformanceRegressionDetected',
|
|
172
|
+
description: 'Bloqueia merges com regressão de latência ou memória superior ao limiar permitido',
|
|
173
|
+
severity: 'MEDIUM',
|
|
174
|
+
cwe: 'CWE-400'
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
findings,
|
|
178
|
+
properties: {
|
|
179
|
+
passed: comparison.passed,
|
|
180
|
+
avgLatencyUs: current.avgLatencyUs,
|
|
181
|
+
baselineLatencyUs: baseline.avgLatencyUs,
|
|
182
|
+
opsPerSec: current.opsPerSec
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
class PerfBenchmark {
|
|
188
|
+
static runBenchmark(iterations) {
|
|
189
|
+
return runBenchmark(iterations);
|
|
190
|
+
}
|
|
191
|
+
static comparePerformance(current, baseline, threshold) {
|
|
192
|
+
return comparePerformance(current, baseline, threshold);
|
|
193
|
+
}
|
|
194
|
+
static formatReport(comparison) {
|
|
195
|
+
return formatPerfReport(comparison);
|
|
196
|
+
}
|
|
197
|
+
static auditToSarif(iterations, threshold) {
|
|
198
|
+
return auditToSarif(iterations, threshold);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
PerfBenchmark,
|
|
204
|
+
runBenchmark,
|
|
205
|
+
comparePerformance,
|
|
206
|
+
formatPerfReport,
|
|
207
|
+
formatReport: formatPerfReport,
|
|
208
|
+
auditToSarif,
|
|
209
|
+
getOrInitBaseline,
|
|
210
|
+
BASELINE_FILE
|
|
211
|
+
};
|
|
212
|
+
|