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.
Files changed (61) hide show
  1. package/README.md +204 -2
  2. package/agents/AGENT_DEVSECOPS.md +32 -0
  3. package/agents/AGENT_INFRA.md +31 -0
  4. package/agents/AGENT_ORCHESTRATOR.md +39 -0
  5. package/agents/AGENT_QA.md +58 -0
  6. package/agents/AGENT_SCRUM.md +23 -0
  7. package/agents/AGENT_SRE.md +22 -0
  8. package/agents/AGENT_UIUX.md +26 -0
  9. package/docs/ARCHITECTURE.md +152 -0
  10. package/docs/CALCULATIONS.md +17 -0
  11. package/docs/CHANGELOG.md +112 -0
  12. package/docs/ENGINEERING_RULES.md +56 -0
  13. package/docs/IDEAS.md +17 -0
  14. package/docs/LOCAL_BACKLOG.md +20 -0
  15. package/docs/STATE.md +111 -0
  16. package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -0
  17. package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -0
  18. package/package.json +57 -4
  19. package/scripts/auditors/a11y_auditor.js +462 -0
  20. package/scripts/auditors/code_quality_auditor.js +444 -0
  21. package/scripts/auditors/contract_test_validator.js +396 -0
  22. package/scripts/auditors/eval_harness.js +213 -0
  23. package/scripts/auditors/git_hygiene_linter.js +391 -0
  24. package/scripts/auditors/pr_reviewer.js +627 -0
  25. package/scripts/auditors/sast_scanner.js +442 -0
  26. package/scripts/auditors/sca_dependency_auditor.js +673 -0
  27. package/scripts/bin/bootstrap_cli.js +597 -0
  28. package/scripts/bin/domain_idea_miner.js +658 -0
  29. package/scripts/bin/smart_onboarding.js +905 -0
  30. package/scripts/bin/web_inspector.js +578 -0
  31. package/scripts/core/architecture_advisor.js +378 -0
  32. package/scripts/core/ast_skeletonizer.js +413 -0
  33. package/scripts/core/autonomous_remediation_engine.js +305 -0
  34. package/scripts/core/circuit_breaker.js +546 -0
  35. package/scripts/core/context_compressor.js +237 -0
  36. package/scripts/core/fast_feature_scaffold.js +840 -0
  37. package/scripts/core/hot_state_cache.js +238 -0
  38. package/scripts/core/knowledge_gateway.js +888 -0
  39. package/scripts/core/magic_prompt_generator.js +250 -0
  40. package/scripts/core/multi_agent_orchestrator.js +421 -0
  41. package/scripts/core/otel_tracer.js +335 -0
  42. package/scripts/core/perf_benchmark.js +212 -0
  43. package/scripts/core/sarif_engine.js +299 -0
  44. package/scripts/core/sre_remediation_engine.js +236 -0
  45. package/scripts/core/universal_packager.js +357 -0
  46. package/scripts/integrations/atlassian_client.js +600 -0
  47. package/scripts/integrations/atlassian_sync_action.js +576 -0
  48. package/scripts/integrations/confluence_sync.js +684 -0
  49. package/scripts/integrations/credentials_guard.js +500 -0
  50. package/scripts/integrations/jira_manager.js +722 -0
  51. package/scripts/mcp/handlers/atlassian_handlers.js +300 -0
  52. package/scripts/mcp/handlers/core_handlers.js +419 -0
  53. package/scripts/mcp/handlers/qa_handlers.js +502 -0
  54. package/scripts/mcp/handlers/rules_handlers.js +379 -0
  55. package/scripts/mcp/handlers/scaffold_handlers.js +334 -0
  56. package/scripts/mcp/handlers/sre_handlers.js +443 -0
  57. package/scripts/mcp/resources_registry.js +453 -0
  58. package/scripts/mcp/server.js +408 -0
  59. package/scripts/mcp/tool_handlers.js +444 -0
  60. package/scripts/mcp/tools_registry.js +1123 -0
  61. 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('<', '&lt;')
48
+ .replaceAll('>', '&gt;')
49
+ .replaceAll('"', '&quot;')
50
+ .replaceAll("'", '&#39;');
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
+ };