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,462 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/auditors/a11y_auditor.js
|
|
4
|
+
*
|
|
5
|
+
* Auditor Funcional de Acessibilidade Digital & Design Tokens — WCAG 2.1 AA (AGENT_UIUX / AGENT_QA)
|
|
6
|
+
* Inspeciona componentes HTML, JSX e TSX através de um Parser DOM/AST defensivo e determinístico,
|
|
7
|
+
* executando regras como predicados funcionais puros e gerando relatórios Markdown e OASIS SARIF v2.1.0.
|
|
8
|
+
*
|
|
9
|
+
* Em conformidade com os 10 Princípios Canônicos, NIST SSDF e diretrizes de inclusão digital da W3C.
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { getKnowledgeGateway } = require('../core/knowledge_gateway.js');
|
|
16
|
+
const { createSarifReport } = require('../core/sarif_engine.js');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Representação imutável de nó na árvore sintática (DOM/AST).
|
|
20
|
+
*/
|
|
21
|
+
class DomAstNode {
|
|
22
|
+
constructor(tag = '#root', attrs = {}, line = 1, raw = '') {
|
|
23
|
+
this.tag = String(tag || '').toLowerCase();
|
|
24
|
+
this.rawTag = tag;
|
|
25
|
+
const normalizedAttrs = {};
|
|
26
|
+
for (const [k, v] of Object.entries(attrs || {})) {
|
|
27
|
+
normalizedAttrs[k.toLowerCase()] = v;
|
|
28
|
+
}
|
|
29
|
+
this.attrs = Object.freeze(normalizedAttrs);
|
|
30
|
+
this.children = [];
|
|
31
|
+
this.parent = null;
|
|
32
|
+
this.line = Math.max(1, Number(line) || 1);
|
|
33
|
+
this.raw = raw || '';
|
|
34
|
+
this.textContent = '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
hasAttr(name) {
|
|
38
|
+
if (!name) return false;
|
|
39
|
+
const lower = name.toLowerCase();
|
|
40
|
+
return Object.prototype.hasOwnProperty.call(this.attrs, lower) ||
|
|
41
|
+
(lower === 'class' && Object.prototype.hasOwnProperty.call(this.attrs, 'classname')) ||
|
|
42
|
+
(lower === 'for' && Object.prototype.hasOwnProperty.call(this.attrs, 'htmlfor'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getAttr(name) {
|
|
46
|
+
if (!name) return null;
|
|
47
|
+
const lower = name.toLowerCase();
|
|
48
|
+
if (Object.prototype.hasOwnProperty.call(this.attrs, lower)) {
|
|
49
|
+
return this.attrs[lower];
|
|
50
|
+
}
|
|
51
|
+
if (lower === 'class' && this.attrs.classname) return this.attrs.classname;
|
|
52
|
+
if (lower === 'for' && this.attrs.htmlfor) return this.attrs.htmlfor;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const VOID_TAGS = new Set([
|
|
58
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
59
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const EXEMPT_INPUT_TYPES = new Set(['hidden', 'submit', 'button', 'image', 'reset']);
|
|
63
|
+
|
|
64
|
+
const VAGUE_LINK_TEXTS = new Set([
|
|
65
|
+
'clique aqui', 'click here', 'saiba mais', 'read more', 'veja mais', 'link', 'aqui', 'here'
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Remove blocos de comentários e código executável de apoio sem alterar o alinhamento de linhas.
|
|
70
|
+
* @param {string} rawCode
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function sanitizeMarkupPreservingLines(rawCode) {
|
|
74
|
+
if (typeof rawCode !== 'string') return '';
|
|
75
|
+
return rawCode
|
|
76
|
+
// Remove comentários HTML: <!-- ... --> preservando \n
|
|
77
|
+
.replace(/<!--[\s\S]*?-->/g, match => match.replace(/[^\r\n]/g, ' '))
|
|
78
|
+
// Remove comentários de bloco JSX: {/* ... */} preservando \n
|
|
79
|
+
.replace(/\{\/\*[\s\S]*?\*\/\}/g, match => match.replace(/[^\r\n]/g, ' '))
|
|
80
|
+
// Remove blocos de script e style internos preservando \n
|
|
81
|
+
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, match => match.replace(/[^\r\n]/g, ' '));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Tokenizador linear O(N) imune a ReDoS (CWE-1333).
|
|
86
|
+
* Suporta arrow functions, URLs com barras e expressões JSX em atributos.
|
|
87
|
+
* @param {string} code
|
|
88
|
+
* @returns {Array<{ tag: string, start: number, end: number }>}
|
|
89
|
+
*/
|
|
90
|
+
function tokenizeHtmlTags(code) {
|
|
91
|
+
const tags = [];
|
|
92
|
+
let i = 0;
|
|
93
|
+
const len = code.length;
|
|
94
|
+
|
|
95
|
+
while (i < len) {
|
|
96
|
+
if (code[i] === '<' && i + 1 < len) {
|
|
97
|
+
const nextChar = code[i + 1];
|
|
98
|
+
if (nextChar === '/' || /[a-zA-Z]/.test(nextChar)) {
|
|
99
|
+
const startIdx = i;
|
|
100
|
+
i++;
|
|
101
|
+
let inDoubleQuote = false;
|
|
102
|
+
let inSingleQuote = false;
|
|
103
|
+
let braceDepth = 0;
|
|
104
|
+
|
|
105
|
+
while (i < len) {
|
|
106
|
+
const char = code[i];
|
|
107
|
+
if (char === '"' && !inSingleQuote) {
|
|
108
|
+
inDoubleQuote = !inDoubleQuote;
|
|
109
|
+
} else if (char === "'" && !inDoubleQuote) {
|
|
110
|
+
inSingleQuote = !inSingleQuote;
|
|
111
|
+
} else if (char === '{' && !inDoubleQuote && !inSingleQuote) {
|
|
112
|
+
braceDepth++;
|
|
113
|
+
} else if (char === '}' && !inDoubleQuote && !inSingleQuote && braceDepth > 0) {
|
|
114
|
+
braceDepth--;
|
|
115
|
+
} else if (char === '>' && !inDoubleQuote && !inSingleQuote && braceDepth === 0) {
|
|
116
|
+
const fullTag = code.slice(startIdx, i + 1);
|
|
117
|
+
tags.push({ tag: fullTag, start: startIdx, end: i + 1 });
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
return tags;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parser de Árvore Sintática (AST / DOM Tree Lexer) para HTML, JSX e TSX.
|
|
131
|
+
* Executa tokenização linear defensiva imune a ReDoS (CWE-1333).
|
|
132
|
+
* @param {string} code
|
|
133
|
+
* @returns {DomAstNode} Raiz da árvore DOM
|
|
134
|
+
*/
|
|
135
|
+
function parseHtmlAst(code) {
|
|
136
|
+
const safeCode = sanitizeMarkupPreservingLines(code);
|
|
137
|
+
const root = new DomAstNode('#root', {}, 1, '');
|
|
138
|
+
let currentParent = root;
|
|
139
|
+
const stack = [root];
|
|
140
|
+
|
|
141
|
+
const tagTokens = tokenizeHtmlTags(safeCode);
|
|
142
|
+
let lastIndex = 0;
|
|
143
|
+
|
|
144
|
+
for (const token of tagTokens) {
|
|
145
|
+
const textBefore = safeCode.slice(lastIndex, token.start);
|
|
146
|
+
if (textBefore.trim().length > 0 && currentParent) {
|
|
147
|
+
currentParent.textContent += (currentParent.textContent ? ' ' : '') + textBefore.trim();
|
|
148
|
+
}
|
|
149
|
+
lastIndex = token.end;
|
|
150
|
+
|
|
151
|
+
const fullTag = token.tag;
|
|
152
|
+
const isClosing = fullTag.startsWith('</');
|
|
153
|
+
const lineNum = safeCode.slice(0, token.start).split('\n').length;
|
|
154
|
+
|
|
155
|
+
if (isClosing) {
|
|
156
|
+
const tagNameMatch = fullTag.match(/^<\/\s*([a-zA-Z0-9_\-:]+)/);
|
|
157
|
+
if (tagNameMatch) {
|
|
158
|
+
const targetTag = tagNameMatch[1].toLowerCase();
|
|
159
|
+
for (let i = stack.length - 1; i > 0; i--) {
|
|
160
|
+
if (stack[i].tag === targetTag) {
|
|
161
|
+
stack.length = i;
|
|
162
|
+
currentParent = stack[stack.length - 1];
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
let tagInner = fullTag.slice(1, -1).trim();
|
|
169
|
+
let isSelfClosing = false;
|
|
170
|
+
if (tagInner.endsWith('/')) {
|
|
171
|
+
isSelfClosing = true;
|
|
172
|
+
tagInner = tagInner.slice(0, -1).trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const tagNameMatch = tagInner.match(/^([a-zA-Z0-9_\-:]+)/);
|
|
176
|
+
if (!tagNameMatch) continue;
|
|
177
|
+
|
|
178
|
+
const tagName = tagNameMatch[1];
|
|
179
|
+
const rawAttrs = tagInner.slice(tagName.length).trim();
|
|
180
|
+
if (VOID_TAGS.has(tagName.toLowerCase())) {
|
|
181
|
+
isSelfClosing = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const attrs = {};
|
|
185
|
+
const attrRegex = /([a-zA-Z0-9_\-:@]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([^}]*)\}|([^>\s]+)))?/g;
|
|
186
|
+
let attrMatch;
|
|
187
|
+
while ((attrMatch = attrRegex.exec(rawAttrs)) !== null) {
|
|
188
|
+
const attrKey = attrMatch[1].toLowerCase();
|
|
189
|
+
const attrVal = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? attrMatch[5] ?? 'true';
|
|
190
|
+
attrs[attrKey] = attrVal;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const node = new DomAstNode(tagName, attrs, lineNum, fullTag);
|
|
194
|
+
node.parent = currentParent;
|
|
195
|
+
currentParent.children.push(node);
|
|
196
|
+
|
|
197
|
+
if (!isSelfClosing) {
|
|
198
|
+
stack.push(node);
|
|
199
|
+
currentParent = node;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (lastIndex < safeCode.length && currentParent) {
|
|
205
|
+
const trailingText = safeCode.slice(lastIndex).trim();
|
|
206
|
+
if (trailingText) {
|
|
207
|
+
currentParent.textContent += (currentParent.textContent ? ' ' : '') + trailingText;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Agregação funcional de texto recursivo
|
|
212
|
+
function aggregateText(node) {
|
|
213
|
+
for (const child of node.children) {
|
|
214
|
+
aggregateText(child);
|
|
215
|
+
if (child.textContent) {
|
|
216
|
+
node.textContent += (node.textContent ? ' ' : '') + child.textContent;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
node.textContent = node.textContent.trim();
|
|
220
|
+
}
|
|
221
|
+
aggregateText(root);
|
|
222
|
+
|
|
223
|
+
return root;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Catálogo Canônico de Regras WCAG 2.1 Nível AA com Predicados Puros.
|
|
228
|
+
*/
|
|
229
|
+
const WCAG_RULES = Object.freeze([
|
|
230
|
+
{
|
|
231
|
+
id: 'WCAG-1.1.1',
|
|
232
|
+
criterion: 'Non-text Content (Nível A)',
|
|
233
|
+
name: 'Imagem sem texto alternativo (alt)',
|
|
234
|
+
severity: 'CRÍTICA',
|
|
235
|
+
issue: 'Elemento de imagem não possui o atributo "alt" para tecnologias assistivas.',
|
|
236
|
+
remediation: 'Adicione alt="Descrição da imagem" ou alt="" acompanhado de role="presentation" para imagens puramente decorativas.',
|
|
237
|
+
check: (node) => (node.tag === 'img' || node.tag === 'image') && !node.hasAttr('alt')
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'WCAG-4.1.2-BTN',
|
|
241
|
+
criterion: 'Name, Role, Value (Nível A)',
|
|
242
|
+
name: 'Botão interativo sem rótulo acessível',
|
|
243
|
+
severity: 'ALTA',
|
|
244
|
+
issue: 'Botão contendo apenas ícone sem aria-label, aria-labelledby ou texto acessível.',
|
|
245
|
+
remediation: 'Inclua aria-label="Ação do botão" ou texto acessível visível/oculto via classe sr-only.',
|
|
246
|
+
check: (node) => {
|
|
247
|
+
if (node.tag !== 'button') return false;
|
|
248
|
+
const hasAccessibleLabel = node.hasAttr('aria-label') || node.hasAttr('aria-labelledby') || node.hasAttr('title');
|
|
249
|
+
const hasVisibleText = (node.textContent || '').trim().length > 0;
|
|
250
|
+
return !hasAccessibleLabel && !hasVisibleText;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: 'WCAG-1.3.1-INPUT',
|
|
255
|
+
criterion: 'Info and Relationships (Nível A)',
|
|
256
|
+
name: 'Campo de entrada (input) sem rótulo associado',
|
|
257
|
+
severity: 'CRÍTICA',
|
|
258
|
+
issue: 'Elemento <input> não possui id para pareamento com <label htmlFor="..."> nem aria-label.',
|
|
259
|
+
remediation: 'Adicione um id único ao input pareado com <label htmlFor="id"> ou declare aria-label.',
|
|
260
|
+
check: (node) => {
|
|
261
|
+
if (node.tag !== 'input') return false;
|
|
262
|
+
const inputType = (node.getAttr('type') || 'text').toLowerCase();
|
|
263
|
+
if (EXEMPT_INPUT_TYPES.has(inputType)) return false;
|
|
264
|
+
const hasId = node.hasAttr('id');
|
|
265
|
+
const hasAria = node.hasAttr('aria-label') || node.hasAttr('aria-labelledby');
|
|
266
|
+
return !hasId && !hasAria;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: 'WCAG-2.4.4-LINK',
|
|
271
|
+
criterion: 'Link Purpose (In Context) (Nível A)',
|
|
272
|
+
name: 'Link com texto vago ou não descritivo',
|
|
273
|
+
severity: 'MÉDIA',
|
|
274
|
+
issue: 'Texto de link ambíguo ("clique aqui", "saiba mais") que prejudica a navegação cega por links.',
|
|
275
|
+
remediation: 'Substitua por texto descritivo do destino (ex: "Consulte o catálogo de livros").',
|
|
276
|
+
check: (node) => {
|
|
277
|
+
if (node.tag !== 'a') return false;
|
|
278
|
+
const linkText = (node.textContent || '').trim().toLowerCase();
|
|
279
|
+
return VAGUE_LINK_TEXTS.has(linkText);
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: 'WCAG-2.4.7-FOCUS',
|
|
284
|
+
criterion: 'Focus Visible (Nível AA)',
|
|
285
|
+
name: 'Supressão de contorno de foco sem anel acessível',
|
|
286
|
+
severity: 'ALTA',
|
|
287
|
+
issue: 'Uso de outline-none sem definição de anel alternativo de foco (focus-visible / focus:ring).',
|
|
288
|
+
remediation: 'Mantenha o foco visível para usuários de teclado adicionando classes como "focus-visible:ring-2 focus-visible:ring-blue-500".',
|
|
289
|
+
check: (node) => {
|
|
290
|
+
const classNames = node.getAttr('class') || '';
|
|
291
|
+
if (!/\boutline-none\b/i.test(classNames)) return false;
|
|
292
|
+
const hasFocusRing = /\b(focus-visible:|focus:ring|focus:border|focus:outline)\b/i.test(classNames);
|
|
293
|
+
return !hasFocusRing;
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'WCAG-2.4.3-TABINDEX',
|
|
298
|
+
criterion: 'Focus Order (Nível A)',
|
|
299
|
+
name: 'Uso de tabindex positivo (anti-pattern)',
|
|
300
|
+
severity: 'MÉDIA',
|
|
301
|
+
issue: 'tabindex maior que zero altera a ordem natural de tabulação do DOM.',
|
|
302
|
+
remediation: 'Utilize tabindex="0" para incluir o elemento no fluxo natural ou reorganize a ordem dos elementos no DOM.',
|
|
303
|
+
check: (node) => {
|
|
304
|
+
if (!node.hasAttr('tabindex')) return false;
|
|
305
|
+
const tabVal = parseInt(node.getAttr('tabindex'), 10);
|
|
306
|
+
return !Number.isNaN(tabVal) && tabVal > 0;
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: 'WCAG-3.1.1-LANG',
|
|
311
|
+
criterion: 'Language of Page (Nível A)',
|
|
312
|
+
name: 'Documento HTML sem atributo de idioma (lang)',
|
|
313
|
+
severity: 'ALTA',
|
|
314
|
+
issue: 'Tag <html> sem atributo lang, impedindo sintetizadores de voz de carregar a pronúncia correta.',
|
|
315
|
+
remediation: 'Declare o idioma principal do documento: <html lang="pt-BR">.',
|
|
316
|
+
check: (node) => node.tag === 'html' && !node.hasAttr('lang')
|
|
317
|
+
}
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Coleta recursiva de todos os nós em array plano (Pipeline Funcional).
|
|
322
|
+
* @param {DomAstNode} rootNode
|
|
323
|
+
* @returns {Array<DomAstNode>}
|
|
324
|
+
*/
|
|
325
|
+
function flattenDomTree(rootNode) {
|
|
326
|
+
const nodes = [];
|
|
327
|
+
const collect = (node) => {
|
|
328
|
+
if (node.tag !== '#root') nodes.push(node);
|
|
329
|
+
for (const child of node.children) {
|
|
330
|
+
collect(child);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
collect(rootNode);
|
|
334
|
+
return nodes;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
class A11yAuditor {
|
|
338
|
+
constructor(rules = WCAG_RULES, options = {}) {
|
|
339
|
+
this.rules = rules;
|
|
340
|
+
this.gateway = options.gateway || getKnowledgeGateway(options);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Executa a auditoria funcional sobre a árvore sintática (DOM/AST).
|
|
345
|
+
* @param {string} code
|
|
346
|
+
* @param {string} [filename='component.tsx']
|
|
347
|
+
* @returns {object} Relatório canônico de acessibilidade
|
|
348
|
+
*/
|
|
349
|
+
auditCode(code, filename = 'component.tsx') {
|
|
350
|
+
const ast = parseHtmlAst(code);
|
|
351
|
+
const nodes = flattenDomTree(ast);
|
|
352
|
+
|
|
353
|
+
// Avaliação Funcional Pura via FlatMap
|
|
354
|
+
const violations = nodes.flatMap(node => {
|
|
355
|
+
return this.rules
|
|
356
|
+
.filter(rule => typeof rule.check === 'function' && rule.check(node))
|
|
357
|
+
.map(rule => {
|
|
358
|
+
const spec = this.gateway ? this.gateway.getWcagSpec(rule.id) : null;
|
|
359
|
+
return {
|
|
360
|
+
ruleId: rule.id,
|
|
361
|
+
wcagCriterion: rule.criterion,
|
|
362
|
+
name: rule.name,
|
|
363
|
+
severity: rule.severity,
|
|
364
|
+
line: node.line,
|
|
365
|
+
snippet: (node.raw || `<${node.tag}>`).slice(0, 100),
|
|
366
|
+
issue: rule.issue,
|
|
367
|
+
remediation: rule.remediation,
|
|
368
|
+
authoritativeRef: spec?.authoritativeRef || null
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const criticalCount = violations.filter(v => v.severity === 'CRÍTICA').length;
|
|
374
|
+
const highCount = violations.filter(v => v.severity === 'ALTA').length;
|
|
375
|
+
const mediumCount = violations.filter(v => v.severity === 'MÉDIA').length;
|
|
376
|
+
|
|
377
|
+
let verdict = 'APROVADO';
|
|
378
|
+
if (criticalCount > 0 || highCount > 0) {
|
|
379
|
+
verdict = 'REPROVADO';
|
|
380
|
+
} else if (mediumCount > 0) {
|
|
381
|
+
verdict = 'COM RESSALVAS';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
filename,
|
|
386
|
+
wcagTarget: 'WCAG 2.1 Nível AA',
|
|
387
|
+
verdict,
|
|
388
|
+
status: verdict === 'APROVADO' ? 'PASSED' : 'FAILED',
|
|
389
|
+
summary: {
|
|
390
|
+
totalViolations: violations.length,
|
|
391
|
+
criticalCount,
|
|
392
|
+
highCount,
|
|
393
|
+
mediumCount
|
|
394
|
+
},
|
|
395
|
+
violations
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Exporta os apontamentos de acessibilidade em SARIF v2.1.0 oficial.
|
|
401
|
+
* @param {string} code
|
|
402
|
+
* @param {string} [filename='component.tsx']
|
|
403
|
+
* @returns {object} SARIF v2.1.0 JSON object
|
|
404
|
+
*/
|
|
405
|
+
auditToSarif(code, filename = 'component.tsx') {
|
|
406
|
+
const auditRes = this.auditCode(code, filename);
|
|
407
|
+
const findings = auditRes.violations.map(v => ({
|
|
408
|
+
ruleId: v.ruleId,
|
|
409
|
+
name: v.name,
|
|
410
|
+
severity: v.severity,
|
|
411
|
+
filename: auditRes.filename,
|
|
412
|
+
line: v.line,
|
|
413
|
+
column: 1,
|
|
414
|
+
snippet: v.snippet,
|
|
415
|
+
message: `${v.wcagCriterion}: ${v.issue}`,
|
|
416
|
+
remediation: v.remediation
|
|
417
|
+
}));
|
|
418
|
+
|
|
419
|
+
return createSarifReport({
|
|
420
|
+
toolName: 'agent-master-a11y-auditor',
|
|
421
|
+
toolVersion: '5.1.0',
|
|
422
|
+
findings,
|
|
423
|
+
properties: {
|
|
424
|
+
wcagTarget: auditRes.wcagTarget,
|
|
425
|
+
verdict: auditRes.verdict,
|
|
426
|
+
status: auditRes.status,
|
|
427
|
+
summary: auditRes.summary
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
formatReport(audit) {
|
|
433
|
+
let md = `## ♿ Relatório de Auditoria de Acessibilidade (WCAG 2.1 AA — AGENT UI/UX)\n\n`;
|
|
434
|
+
md += `**Alvo:** \`${audit.filename}\` | **Veredito:** ${audit.verdict === 'APROVADO' ? '✅ APROVADO' : audit.verdict === 'REPROVADO' ? '❌ REPROVADO' : '⚠️ COM RESSALVAS'}\n\n`;
|
|
435
|
+
md += `### 📊 Sumário Executivo de Inclusão (DOM/AST Analysis)\n`;
|
|
436
|
+
md += `- **Violações Críticas (Impede Leitor de Tela):** ${audit.summary.criticalCount}\n`;
|
|
437
|
+
md += `- **Violações Altas (Navegação/Foco):** ${audit.summary.highCount}\n`;
|
|
438
|
+
md += `- **Ressalvas Médias:** ${audit.summary.mediumCount}\n\n`;
|
|
439
|
+
|
|
440
|
+
if (audit.violations.length > 0) {
|
|
441
|
+
md += `### ⚠️ Apontamentos de Acessibilidade Detectados\n\n`;
|
|
442
|
+
md += `| Linha | Critério WCAG | Severidade | Problema | Remediação Recomendada |\n`;
|
|
443
|
+
md += `| :---: | :--- | :---: | :--- | :--- |\n`;
|
|
444
|
+
for (const v of audit.violations) {
|
|
445
|
+
md += `| ${v.line} | **${v.wcagCriterion}** (${v.ruleId}) | **${v.severity}** | ${v.issue} | ${v.remediation} |\n`;
|
|
446
|
+
}
|
|
447
|
+
md += '\n';
|
|
448
|
+
} else {
|
|
449
|
+
md += `> 🌟 **Componente Acessível:** Nenhuma violação detectada. O componente atende às diretrizes WCAG 2.1 AA.\n\n`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return md;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
module.exports = {
|
|
457
|
+
A11yAuditor,
|
|
458
|
+
DomAstNode,
|
|
459
|
+
parseHtmlAst,
|
|
460
|
+
flattenDomTree,
|
|
461
|
+
WCAG_RULES
|
|
462
|
+
};
|