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,684 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/integrations/confluence_sync.js
|
|
4
|
+
*
|
|
5
|
+
* Sincronizador Automático Universal de ADRs, SPECs e Governança no Confluence em Tempo Real.
|
|
6
|
+
* Lê arquivos Markdown em docs/adr/*.md, docs/specs/*.md e docs/governance/*.md,
|
|
7
|
+
* converte para Confluence Storage Format (XHTML) e cria/atualiza páginas hierarquicamente.
|
|
8
|
+
*
|
|
9
|
+
* Governança: Tríade Git + Jira + Confluence (Regra 11 e Regra 15).
|
|
10
|
+
* Conformidade: OASIS SARIF v2.1.0, Programação Defensiva e Clean Code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
getAtlassianCredentials,
|
|
20
|
+
requestAtlassian,
|
|
21
|
+
AtlassianClient
|
|
22
|
+
} = require('./atlassian_client.js');
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
extractIssueKeys,
|
|
26
|
+
linkConfluencePageToJira
|
|
27
|
+
} = require('./atlassian_sync_action.js');
|
|
28
|
+
|
|
29
|
+
const { createSarifReport } = require('../core/sarif_engine.js');
|
|
30
|
+
|
|
31
|
+
const CONFLUENCE_CONFIG = {
|
|
32
|
+
spaceKey: process.env.CONFLUENCE_SPACE_KEY || process.env.ATLASSIAN_SPACE_KEY || 'DOCS',
|
|
33
|
+
adrParentId: process.env.CONFLUENCE_ADR_PARENT_ID || null,
|
|
34
|
+
specParentId: process.env.CONFLUENCE_SPEC_PARENT_ID || null,
|
|
35
|
+
govParentId: process.env.CONFLUENCE_GOV_PARENT_ID || null
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Escapa caracteres especiais HTML para gerar XML/XHTML válido para Confluence Storage Format.
|
|
40
|
+
* @param {string|any} text
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function escapeHtml(text) {
|
|
44
|
+
if (text === null || text === undefined) return '';
|
|
45
|
+
return String(text)
|
|
46
|
+
.replaceAll('&', '&')
|
|
47
|
+
.replaceAll('<', '<')
|
|
48
|
+
.replaceAll('>', '>')
|
|
49
|
+
.replaceAll('"', '"')
|
|
50
|
+
.replaceAll("'", ''');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Converte links no formato markdown [label](url) para tags HTML <a> seguras.
|
|
55
|
+
* @param {string} text
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function parseLinks(text) {
|
|
59
|
+
if (!text || typeof text !== 'string') return '';
|
|
60
|
+
let result = '';
|
|
61
|
+
let i = 0;
|
|
62
|
+
while (i < text.length) {
|
|
63
|
+
const openBracket = text.indexOf('[', i);
|
|
64
|
+
if (openBracket === -1) {
|
|
65
|
+
result += text.slice(i);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
result += text.slice(i, openBracket);
|
|
69
|
+
const closeBracket = text.indexOf(']', openBracket + 1);
|
|
70
|
+
if (closeBracket !== -1 && text[closeBracket + 1] === '(') {
|
|
71
|
+
const closeParen = text.indexOf(')', closeBracket + 2);
|
|
72
|
+
if (closeParen !== -1) {
|
|
73
|
+
const label = text.slice(openBracket + 1, closeBracket);
|
|
74
|
+
const url = text.slice(closeBracket + 2, closeParen);
|
|
75
|
+
result += `<a href="${url}">${label}</a>`;
|
|
76
|
+
i = closeParen + 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
result += '[';
|
|
81
|
+
i = openBracket + 1;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Converte texto Markdown em XHTML compatível com o Confluence Storage Format v2.
|
|
88
|
+
* Suporta headers, blocos de código com CDATA, blockquotes, listas ordenadas e não ordenadas,
|
|
89
|
+
* tabelas completas (th/td), negrito, itálico, código inline e links.
|
|
90
|
+
* @param {string} markdown
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
function markdownToConfluenceStorage(markdown) {
|
|
94
|
+
if (!markdown || typeof markdown !== 'string') return '';
|
|
95
|
+
|
|
96
|
+
const lines = markdown.split(/\r?\n/);
|
|
97
|
+
const elements = [];
|
|
98
|
+
let inCodeBlock = false;
|
|
99
|
+
let codeBuffer = [];
|
|
100
|
+
let codeLanguage = 'text';
|
|
101
|
+
let inList = false;
|
|
102
|
+
let inOrderedList = false;
|
|
103
|
+
let inTable = false;
|
|
104
|
+
let isFirstTableRow = false;
|
|
105
|
+
|
|
106
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
107
|
+
const rawLine = lines[idx];
|
|
108
|
+
|
|
109
|
+
// Fenced Code Blocks (```lang)
|
|
110
|
+
if (rawLine.trim().startsWith('```')) {
|
|
111
|
+
if (inCodeBlock) {
|
|
112
|
+
inCodeBlock = false;
|
|
113
|
+
const codeContent = codeBuffer.join('\n').replace(/\]\]>/g, ']]]]><![CDATA[>');
|
|
114
|
+
elements.push(
|
|
115
|
+
`<ac:structured-macro ac:name="code" ac:schema-version="1">` +
|
|
116
|
+
`<ac:parameter ac:name="language">${codeLanguage}</ac:parameter>` +
|
|
117
|
+
`<ac:parameter ac:name="linenumbers">true</ac:parameter>` +
|
|
118
|
+
`<ac:plain-text-body><![CDATA[${codeContent}]]></ac:plain-text-body>` +
|
|
119
|
+
`</ac:structured-macro>`
|
|
120
|
+
);
|
|
121
|
+
codeBuffer = [];
|
|
122
|
+
codeLanguage = 'text';
|
|
123
|
+
} else {
|
|
124
|
+
if (inList) { elements.push('</ul>'); inList = false; }
|
|
125
|
+
if (inOrderedList) { elements.push('</ol>'); inOrderedList = false; }
|
|
126
|
+
if (inTable) { elements.push('</tbody></table>'); inTable = false; }
|
|
127
|
+
inCodeBlock = true;
|
|
128
|
+
const match = rawLine.trim().match(/^```([a-zA-Z0-9_-]*)/);
|
|
129
|
+
codeLanguage = match && match[1] ? match[1] : 'text';
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (inCodeBlock) {
|
|
135
|
+
codeBuffer.push(rawLine);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const trimmed = rawLine.trim();
|
|
140
|
+
|
|
141
|
+
// Linha vazia
|
|
142
|
+
if (!trimmed) {
|
|
143
|
+
if (inList) { elements.push('</ul>'); inList = false; }
|
|
144
|
+
if (inOrderedList) { elements.push('</ol>'); inOrderedList = false; }
|
|
145
|
+
if (inTable) { elements.push('</tbody></table>'); inTable = false; }
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Tabelas Markdown (| col1 | col2 |)
|
|
150
|
+
if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
|
|
151
|
+
if (inList) { elements.push('</ul>'); inList = false; }
|
|
152
|
+
if (inOrderedList) { elements.push('</ol>'); inOrderedList = false; }
|
|
153
|
+
if (!inTable) {
|
|
154
|
+
elements.push('<table><tbody>');
|
|
155
|
+
inTable = true;
|
|
156
|
+
isFirstTableRow = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Separador de cabeçalho (| --- | --- |)
|
|
160
|
+
if (/^\|(\s*:?-+:?\s*\|)+$/.test(trimmed)) {
|
|
161
|
+
isFirstTableRow = false;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const rawCells = trimmed.slice(1, -1).split('|').map(c => c.trim());
|
|
166
|
+
const cellTag = isFirstTableRow ? 'th' : 'td';
|
|
167
|
+
const cellsHtml = rawCells
|
|
168
|
+
.map(cell => {
|
|
169
|
+
const formatted = escapeHtml(cell)
|
|
170
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
171
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
172
|
+
return `<${cellTag}>${parseLinks(formatted)}</${cellTag}>`;
|
|
173
|
+
})
|
|
174
|
+
.join('');
|
|
175
|
+
|
|
176
|
+
elements.push(`<tr>${cellsHtml}</tr>`);
|
|
177
|
+
isFirstTableRow = false;
|
|
178
|
+
continue;
|
|
179
|
+
} else if (inTable) {
|
|
180
|
+
elements.push('</tbody></table>');
|
|
181
|
+
inTable = false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Separador horizontal
|
|
185
|
+
if (/^(\-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
186
|
+
if (inList) { elements.push('</ul>'); inList = false; }
|
|
187
|
+
if (inOrderedList) { elements.push('</ol>'); inOrderedList = false; }
|
|
188
|
+
elements.push('<hr />');
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Blockquote
|
|
193
|
+
if (trimmed.startsWith('>')) {
|
|
194
|
+
if (inList) { elements.push('</ul>'); inList = false; }
|
|
195
|
+
if (inOrderedList) { elements.push('</ol>'); inOrderedList = false; }
|
|
196
|
+
const quoteText = escapeHtml(trimmed.replace(/^>\s*/, ''))
|
|
197
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
198
|
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
199
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
200
|
+
elements.push(`<blockquote><p>${parseLinks(quoteText)}</p></blockquote>`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Cabeçalhos (h1 - h6)
|
|
205
|
+
const headerMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
|
|
206
|
+
if (headerMatch) {
|
|
207
|
+
if (inList) { elements.push('</ul>'); inList = false; }
|
|
208
|
+
if (inOrderedList) { elements.push('</ol>'); inOrderedList = false; }
|
|
209
|
+
const level = headerMatch[1].length;
|
|
210
|
+
const text = escapeHtml(headerMatch[2]);
|
|
211
|
+
elements.push(`<h${level}>${parseLinks(text)}</h${level}>`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Itens de lista ordenada (1. item)
|
|
216
|
+
const orderedMatch = trimmed.match(/^(\d+)\.\s+(.*)$/);
|
|
217
|
+
if (orderedMatch) {
|
|
218
|
+
if (inList) { elements.push('</ul>'); inList = false; }
|
|
219
|
+
if (!inOrderedList) {
|
|
220
|
+
elements.push('<ol>');
|
|
221
|
+
inOrderedList = true;
|
|
222
|
+
}
|
|
223
|
+
const itemText = escapeHtml(orderedMatch[2]);
|
|
224
|
+
const formatted = itemText
|
|
225
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
226
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
227
|
+
elements.push(`<li>${parseLinks(formatted)}</li>`);
|
|
228
|
+
continue;
|
|
229
|
+
} else if (inOrderedList) {
|
|
230
|
+
elements.push('</ol>');
|
|
231
|
+
inOrderedList = false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Itens de lista com marcadores (* ou -)
|
|
235
|
+
const listMatch = trimmed.match(/^[-*]\s+(.*)$/);
|
|
236
|
+
if (listMatch) {
|
|
237
|
+
if (!inList) {
|
|
238
|
+
elements.push('<ul>');
|
|
239
|
+
inList = true;
|
|
240
|
+
}
|
|
241
|
+
const itemText = escapeHtml(listMatch[1]);
|
|
242
|
+
const formatted = itemText
|
|
243
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
244
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
245
|
+
elements.push(`<li>${parseLinks(formatted)}</li>`);
|
|
246
|
+
continue;
|
|
247
|
+
} else if (inList) {
|
|
248
|
+
elements.push('</ul>');
|
|
249
|
+
inList = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Parágrafo comum
|
|
253
|
+
const formattedPara = escapeHtml(trimmed)
|
|
254
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
255
|
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
256
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
257
|
+
|
|
258
|
+
elements.push(`<p>${parseLinks(formattedPara)}</p>`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (inList) elements.push('</ul>');
|
|
262
|
+
if (inOrderedList) elements.push('</ol>');
|
|
263
|
+
if (inTable) elements.push('</tbody></table>');
|
|
264
|
+
|
|
265
|
+
return elements.join('');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extrai o título H1 do markdown ou utiliza fallback.
|
|
270
|
+
* @param {string} markdown
|
|
271
|
+
* @param {string} [fallback='Documento Sem Título']
|
|
272
|
+
* @returns {string}
|
|
273
|
+
*/
|
|
274
|
+
function extractTitle(markdown, fallback = 'Documento Sem Título') {
|
|
275
|
+
if (!markdown || typeof markdown !== 'string') return fallback;
|
|
276
|
+
const lines = markdown.split(/\r?\n/);
|
|
277
|
+
for (const line of lines) {
|
|
278
|
+
const match = line.match(/^#\s+(.*)$/);
|
|
279
|
+
if (match) {
|
|
280
|
+
return match[1].trim();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return fallback;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Busca página no Confluence por título ou provisiona pasta estrutural se não existir.
|
|
288
|
+
* @param {object} creds
|
|
289
|
+
* @param {string} spaceKey
|
|
290
|
+
* @param {string} title
|
|
291
|
+
* @param {string|null} [parentId=null]
|
|
292
|
+
* @returns {Promise<object|null>}
|
|
293
|
+
*/
|
|
294
|
+
async function findOrCreatePage(creds, spaceKey, title, parentId = null) {
|
|
295
|
+
const encodedTitle = encodeURIComponent(title);
|
|
296
|
+
const pathUrl = `/wiki/rest/api/content?spaceKey=${spaceKey}&title=${encodedTitle}&expand=version,ancestors`;
|
|
297
|
+
const searchRes = await requestAtlassian(creds, 'GET', pathUrl);
|
|
298
|
+
|
|
299
|
+
if (searchRes.status === 200 && searchRes.body && Array.isArray(searchRes.body.results) && searchRes.body.results.length > 0) {
|
|
300
|
+
return searchRes.body.results[0];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Se não existir e for pasta estrutural, cria
|
|
304
|
+
if (parentId !== undefined) {
|
|
305
|
+
const createPayload = {
|
|
306
|
+
type: 'page',
|
|
307
|
+
title,
|
|
308
|
+
space: { key: spaceKey },
|
|
309
|
+
body: {
|
|
310
|
+
storage: {
|
|
311
|
+
value: `<p>Seção de documentação gerada automaticamente pela Tríade Multiagente.</p>`,
|
|
312
|
+
representation: 'storage'
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
...(parentId ? { ancestors: [{ id: parentId }] } : {})
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const createRes = await requestAtlassian(creds, 'POST', '/wiki/rest/api/content', createPayload);
|
|
319
|
+
if (createRes.status === 200 || createRes.status === 201) {
|
|
320
|
+
return createRes.body;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Sincroniza um documento Markdown específico no Confluence (cria ou atualiza versão).
|
|
329
|
+
* @param {object} creds
|
|
330
|
+
* @param {string} filePath
|
|
331
|
+
* @param {string|null} [parentId=null]
|
|
332
|
+
* @param {object} [options={}]
|
|
333
|
+
* @returns {Promise<object>}
|
|
334
|
+
*/
|
|
335
|
+
async function syncDocument(creds, filePath, parentId = null, options = {}) {
|
|
336
|
+
if (!fs.existsSync(filePath)) {
|
|
337
|
+
return { success: false, error: `Arquivo não encontrado: ${filePath}` };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const rawMarkdown = fs.readFileSync(filePath, 'utf8');
|
|
341
|
+
const filename = path.basename(filePath, '.md');
|
|
342
|
+
const title = extractTitle(rawMarkdown, filename);
|
|
343
|
+
const storageHtml = markdownToConfluenceStorage(rawMarkdown);
|
|
344
|
+
const spaceKey = options.spaceKey || CONFLUENCE_CONFIG.spaceKey;
|
|
345
|
+
|
|
346
|
+
const existingPage = await findOrCreatePage(creds, spaceKey, title);
|
|
347
|
+
let action = 'created';
|
|
348
|
+
let pageId = null;
|
|
349
|
+
let res = null;
|
|
350
|
+
|
|
351
|
+
if (existingPage) {
|
|
352
|
+
action = 'updated';
|
|
353
|
+
pageId = existingPage.id;
|
|
354
|
+
const currentVersion = (existingPage.version && existingPage.version.number) ? existingPage.version.number : 1;
|
|
355
|
+
const nextVersion = currentVersion + 1;
|
|
356
|
+
const updatePayload = {
|
|
357
|
+
version: { number: nextVersion },
|
|
358
|
+
title,
|
|
359
|
+
type: 'page',
|
|
360
|
+
space: { key: spaceKey },
|
|
361
|
+
body: {
|
|
362
|
+
storage: {
|
|
363
|
+
value: storageHtml,
|
|
364
|
+
representation: 'storage'
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
...(parentId ? { ancestors: [{ id: parentId }] } : {})
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
res = await requestAtlassian(creds, 'PUT', `/wiki/rest/api/content/${existingPage.id}`, updatePayload);
|
|
371
|
+
} else {
|
|
372
|
+
action = 'created';
|
|
373
|
+
const createPayload = {
|
|
374
|
+
type: 'page',
|
|
375
|
+
title,
|
|
376
|
+
space: { key: spaceKey },
|
|
377
|
+
body: {
|
|
378
|
+
storage: {
|
|
379
|
+
value: storageHtml,
|
|
380
|
+
representation: 'storage'
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
...(parentId ? { ancestors: [{ id: parentId }] } : {})
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
res = await requestAtlassian(creds, 'POST', '/wiki/rest/api/content', createPayload);
|
|
387
|
+
if (res.body && res.body.id) {
|
|
388
|
+
pageId = res.body.id;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const isSuccess = res && (res.status === 200 || res.status === 201);
|
|
393
|
+
if (isSuccess) {
|
|
394
|
+
const id = pageId || (res.body && res.body.id);
|
|
395
|
+
console.log(`[Confluence Sync] ✔ ${action === 'created' ? '🚀 Criada' : 'Atualizada'}: "${title}" (ID: ${id})`);
|
|
396
|
+
const pageUrl = `https://${creds.domain}/wiki/spaces/${spaceKey}/pages/${id}`;
|
|
397
|
+
const keys = [...new Set([...extractIssueKeys(title), ...extractIssueKeys(rawMarkdown)])];
|
|
398
|
+
for (const key of keys) {
|
|
399
|
+
await linkConfluencePageToJira(key, pageUrl, title, `Página do Confluence ${action}: ${title}`);
|
|
400
|
+
}
|
|
401
|
+
return { success: true, id, action, title, filePath, linkedKeys: keys };
|
|
402
|
+
} else {
|
|
403
|
+
console.error(`[Confluence Sync] ✖ Falha ao salvar "${title}": HTTP ${res ? res.status : 'ERR'}`);
|
|
404
|
+
return { success: false, action, title, filePath, error: res ? res.body : 'Sem resposta' };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Executa a sincronização completa de todos os diretórios padrão de documentação (ADRs, SPECs, Governança).
|
|
410
|
+
* @param {object} [options={}]
|
|
411
|
+
* @returns {Promise<object>}
|
|
412
|
+
*/
|
|
413
|
+
async function runSync(options = {}) {
|
|
414
|
+
const targetDir = options.targetDir || process.cwd();
|
|
415
|
+
const creds = options.credentials || getAtlassianCredentials(targetDir);
|
|
416
|
+
const spaceKey = options.spaceKey || CONFLUENCE_CONFIG.spaceKey;
|
|
417
|
+
|
|
418
|
+
if (!creds || !creds.isConfigured) {
|
|
419
|
+
console.error('[Confluence Sync] ✖ Credenciais Atlassian não configuradas. Sincronização ignorada.');
|
|
420
|
+
return {
|
|
421
|
+
synced: false,
|
|
422
|
+
reason: 'Credenciais Atlassian não configuradas no ambiente.',
|
|
423
|
+
processed: 0,
|
|
424
|
+
results: []
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.log(`[Confluence Sync] Espaço alvo: "${spaceKey}".`);
|
|
429
|
+
|
|
430
|
+
// Descoberta dinâmica de pastas estruturais
|
|
431
|
+
let adrParent = options.adrParentId || CONFLUENCE_CONFIG.adrParentId;
|
|
432
|
+
let specParent = options.specParentId || CONFLUENCE_CONFIG.specParentId;
|
|
433
|
+
let govParent = options.govParentId || CONFLUENCE_CONFIG.govParentId;
|
|
434
|
+
|
|
435
|
+
if (!adrParent) {
|
|
436
|
+
const p = await findOrCreatePage(creds, spaceKey, 'Decisões de Arquitetura (ADRs)');
|
|
437
|
+
if (p) adrParent = p.id;
|
|
438
|
+
}
|
|
439
|
+
if (!specParent) {
|
|
440
|
+
const p = await findOrCreatePage(creds, spaceKey, 'Especificações Técnicas (SPECs)');
|
|
441
|
+
if (p) specParent = p.id;
|
|
442
|
+
}
|
|
443
|
+
if (!govParent) {
|
|
444
|
+
const p = await findOrCreatePage(creds, spaceKey, 'Governança & Padrões Multiagente');
|
|
445
|
+
if (p) govParent = p.id;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const targets = [
|
|
449
|
+
{ dir: path.resolve(targetDir, 'docs/adr'), parentId: adrParent, category: 'ADR' },
|
|
450
|
+
{ dir: path.resolve(targetDir, 'docs/specs'), parentId: specParent, category: 'SPEC' },
|
|
451
|
+
{ dir: path.resolve(targetDir, 'docs/governance'), parentId: govParent, category: 'GOVERNANCE' }
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
const results = [];
|
|
455
|
+
for (const target of targets) {
|
|
456
|
+
if (fs.existsSync(target.dir)) {
|
|
457
|
+
const files = fs.readdirSync(target.dir).filter(f => f.endsWith('.md'));
|
|
458
|
+
for (const f of files) {
|
|
459
|
+
const fullPath = path.join(target.dir, f);
|
|
460
|
+
const res = await syncDocument(creds, fullPath, target.parentId, { spaceKey });
|
|
461
|
+
results.push({
|
|
462
|
+
file: f,
|
|
463
|
+
category: target.category,
|
|
464
|
+
...res
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
console.log(`[Confluence Sync] Finalizado: ${results.length} documentos processados.`);
|
|
471
|
+
return {
|
|
472
|
+
synced: true,
|
|
473
|
+
spaceKey,
|
|
474
|
+
processed: results.length,
|
|
475
|
+
results
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Formata relatório executivo em Markdown sobre o status da sincronização do Confluence.
|
|
481
|
+
* @param {object} [syncOutcome={}]
|
|
482
|
+
* @returns {string}
|
|
483
|
+
*/
|
|
484
|
+
function formatReport(syncOutcome = {}) {
|
|
485
|
+
let md = '# 📘 Confluence Sync — Status Report de Documentação\n\n';
|
|
486
|
+
|
|
487
|
+
const processed = syncOutcome.processed || (syncOutcome.results ? syncOutcome.results.length : 0);
|
|
488
|
+
const spaceKey = syncOutcome.spaceKey || 'DOCS';
|
|
489
|
+
const results = syncOutcome.results || [];
|
|
490
|
+
|
|
491
|
+
md += `**Espaço Confluence:** \`${spaceKey}\`\n`;
|
|
492
|
+
md += `**Documentos Processados:** \`${processed}\`\n\n`;
|
|
493
|
+
|
|
494
|
+
if (results.length === 0) {
|
|
495
|
+
md += syncOutcome.reason
|
|
496
|
+
? `*Observação:* ${syncOutcome.reason}\n`
|
|
497
|
+
: '*Nenhum documento encontrado para sincronização.*\n';
|
|
498
|
+
return md;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
md += '| Categoria | Arquivo | Título | Ação | Status |\n';
|
|
502
|
+
md += '| :--- | :--- | :--- | :--- | :--- |\n';
|
|
503
|
+
for (const r of results) {
|
|
504
|
+
const statusIcon = r.success ? '✅' : '❌';
|
|
505
|
+
const actionText = r.action || (r.success ? 'synced' : 'failed');
|
|
506
|
+
md += `| \`${r.category || 'DOC'}\` | \`${r.file || 'unknown'}\` | ${r.title || 'Sem título'} | \`${actionText}\` | ${statusIcon} |\n`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return md;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Audita a prontidão e resultado da sincronização do Confluence emitindo relatório OASIS SARIF v2.1.0.
|
|
514
|
+
* @param {object} [syncOutcome={}]
|
|
515
|
+
* @returns {object} SARIF v2.1.0 JSON object
|
|
516
|
+
*/
|
|
517
|
+
function auditSyncToSarif(syncOutcome = {}) {
|
|
518
|
+
const rules = [
|
|
519
|
+
{
|
|
520
|
+
id: 'RULE-CONF-001',
|
|
521
|
+
name: 'ConfluenceCredentialsMissing',
|
|
522
|
+
severity: 'WARNING',
|
|
523
|
+
message: 'Credenciais Atlassian ou espaço Confluence não configurados.',
|
|
524
|
+
remediation: 'Configure ATLASSIAN_API_TOKEN, ATLASSIAN_EMAIL, ATLASSIAN_DOMAIN e CONFLUENCE_SPACE_KEY.',
|
|
525
|
+
cwe: 'CWE-200'
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: 'RULE-CONF-002',
|
|
529
|
+
name: 'ConfluencePageSyncFailed',
|
|
530
|
+
severity: 'ERROR',
|
|
531
|
+
message: 'Falha ao sincronizar documento Markdown para o Confluence Storage Format.',
|
|
532
|
+
remediation: 'Verifique permissões de escrita no espaço Confluence e validade do conteúdo XHTML.',
|
|
533
|
+
cwe: 'CWE-287'
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
id: 'RULE-CONF-003',
|
|
537
|
+
name: 'ConfluenceDocsDirNotFound',
|
|
538
|
+
severity: 'NOTE',
|
|
539
|
+
message: 'Diretório padrão de documentação (docs/adr, docs/specs, docs/governance) não encontrado.',
|
|
540
|
+
remediation: 'Crie a pasta de documentação e versione arquivos Markdown.',
|
|
541
|
+
cwe: 'CWE-Unknown'
|
|
542
|
+
}
|
|
543
|
+
];
|
|
544
|
+
|
|
545
|
+
const findings = [];
|
|
546
|
+
const results = syncOutcome.results || [];
|
|
547
|
+
|
|
548
|
+
if (syncOutcome.synced === false) {
|
|
549
|
+
findings.push({
|
|
550
|
+
ruleId: 'RULE-CONF-001',
|
|
551
|
+
severity: 'WARNING',
|
|
552
|
+
message: syncOutcome.reason || 'Credenciais Confluence não configuradas no ambiente.',
|
|
553
|
+
filename: '.env',
|
|
554
|
+
line: 1,
|
|
555
|
+
column: 1
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
for (const r of results) {
|
|
560
|
+
if (!r.success) {
|
|
561
|
+
findings.push({
|
|
562
|
+
ruleId: 'RULE-CONF-002',
|
|
563
|
+
severity: 'ERROR',
|
|
564
|
+
message: `Falha na sincronização do documento ${r.file || r.title}: ${JSON.stringify(r.error || 'Erro desconhecido')}`,
|
|
565
|
+
filename: r.file || 'docs',
|
|
566
|
+
line: 1,
|
|
567
|
+
column: 1
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return createSarifReport({
|
|
573
|
+
toolName: 'agent-master-confluence-sync',
|
|
574
|
+
toolVersion: '5.1.0',
|
|
575
|
+
rules,
|
|
576
|
+
findings,
|
|
577
|
+
properties: {
|
|
578
|
+
category: 'Governance & Documentation Sync',
|
|
579
|
+
spaceKey: syncOutcome.spaceKey || 'DOCS',
|
|
580
|
+
processedCount: results.length
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Classe Canônica ConfluenceSync.
|
|
587
|
+
* Provedor orientado a objetos para conversão de Markdown e sincronização com Confluence Cloud.
|
|
588
|
+
*/
|
|
589
|
+
class ConfluenceSync {
|
|
590
|
+
constructor(options = {}) {
|
|
591
|
+
const safeOptions = (options && typeof options === 'object') ? options : {};
|
|
592
|
+
this.targetDir = safeOptions.targetDir || process.cwd();
|
|
593
|
+
this.spaceKey = safeOptions.spaceKey || process.env.CONFLUENCE_SPACE_KEY || process.env.ATLASSIAN_SPACE_KEY || 'DOCS';
|
|
594
|
+
this.adrParentId = safeOptions.adrParentId || process.env.CONFLUENCE_ADR_PARENT_ID || null;
|
|
595
|
+
this.specParentId = safeOptions.specParentId || process.env.CONFLUENCE_SPEC_PARENT_ID || null;
|
|
596
|
+
this.govParentId = safeOptions.govParentId || process.env.CONFLUENCE_GOV_PARENT_ID || null;
|
|
597
|
+
this.atlassianClient = new AtlassianClient(this.targetDir, { timeout: safeOptions.timeout || 30000 });
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
convertMarkdown(markdown) {
|
|
601
|
+
return markdownToConfluenceStorage(markdown);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
extractDocTitle(markdown, fallback) {
|
|
605
|
+
return extractTitle(markdown, fallback);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async syncDoc(filePath, parentId = null) {
|
|
609
|
+
const creds = this.atlassianClient.getCredentials();
|
|
610
|
+
return syncDocument(creds, filePath, parentId, { spaceKey: this.spaceKey });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async syncAll() {
|
|
614
|
+
return runSync({
|
|
615
|
+
targetDir: this.targetDir,
|
|
616
|
+
spaceKey: this.spaceKey,
|
|
617
|
+
adrParentId: this.adrParentId,
|
|
618
|
+
specParentId: this.specParentId,
|
|
619
|
+
govParentId: this.govParentId,
|
|
620
|
+
credentials: this.atlassianClient.getCredentials()
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
formatReport(syncOutcome = {}) {
|
|
625
|
+
return formatReport(syncOutcome);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
auditToSarif(syncOutcome = {}) {
|
|
629
|
+
return auditSyncToSarif(syncOutcome);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Métodos Estáticos Canônicos
|
|
633
|
+
static escapeHtml(text) {
|
|
634
|
+
return escapeHtml(text);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
static parseLinks(text) {
|
|
638
|
+
return parseLinks(text);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
static markdownToConfluenceStorage(markdown) {
|
|
642
|
+
return markdownToConfluenceStorage(markdown);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
static extractTitle(markdown, fallback) {
|
|
646
|
+
return extractTitle(markdown, fallback);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
static async syncDocument(creds, filePath, parentId, options) {
|
|
650
|
+
return syncDocument(creds, filePath, parentId, options);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
static async runSync(options) {
|
|
654
|
+
return runSync(options);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
static formatReport(syncOutcome) {
|
|
658
|
+
return formatReport(syncOutcome);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
static auditToSarif(syncOutcome) {
|
|
662
|
+
return auditSyncToSarif(syncOutcome);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (require.main === module) {
|
|
667
|
+
runSync().catch(err => {
|
|
668
|
+
console.error('[Confluence Sync] Erro fatal:', err);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
module.exports = {
|
|
674
|
+
ConfluenceSync,
|
|
675
|
+
escapeHtml,
|
|
676
|
+
parseLinks,
|
|
677
|
+
markdownToConfluenceStorage,
|
|
678
|
+
extractTitle,
|
|
679
|
+
findOrCreatePage,
|
|
680
|
+
syncDocument,
|
|
681
|
+
runSync,
|
|
682
|
+
formatReport,
|
|
683
|
+
auditSyncToSarif
|
|
684
|
+
};
|