@diegovelasquezweb/a11y-engine 0.6.5 → 0.7.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/package.json +2 -2
- package/src/ai/claude.mjs +249 -0
- package/src/core/github-api.mjs +150 -0
- package/src/enrichment/analyzer.mjs +12 -1
- package/src/index.d.mts +10 -0
- package/src/index.mjs +102 -16
- package/src/pipeline/dom-scanner.mjs +54 -18
- package/src/reports/checklist.mjs +1 -1
- package/src/reports/renderers/md.mjs +1 -1
- package/src/source-patterns/source-scanner.mjs +87 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegovelasquezweb/a11y-engine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "WCAG 2.2
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file claude.mjs
|
|
3
|
+
* @description Claude AI enrichment layer for the a11y-engine.
|
|
4
|
+
* Enhances Critical/Serious findings with context-aware fix suggestions,
|
|
5
|
+
* leveraging repository source code when available via GitHub API.
|
|
6
|
+
*
|
|
7
|
+
* This module is a passthrough when:
|
|
8
|
+
* - options.enabled is false
|
|
9
|
+
* - no apiKey is provided
|
|
10
|
+
*
|
|
11
|
+
* Requires: ANTHROPIC_API_KEY in options.apiKey
|
|
12
|
+
* Optional: githubToken for reading private repo files
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ANTHROPIC_API = "https://api.anthropic.com/v1/messages";
|
|
16
|
+
const DEFAULT_MODEL = "claude-sonnet-4-5";
|
|
17
|
+
const MAX_AI_FINDINGS = 20; // cap to control cost
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {import('../index.d.mts').EnrichedFinding} EnrichedFinding
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Builds the system prompt for the AI enrichment task.
|
|
25
|
+
* @param {object} context
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function buildSystemPrompt(context) {
|
|
29
|
+
const { framework, cms, uiLibraries } = context.stack || {};
|
|
30
|
+
|
|
31
|
+
let stackInfo = "";
|
|
32
|
+
if (framework) stackInfo += `Framework: ${framework}\n`;
|
|
33
|
+
if (cms) stackInfo += `CMS: ${cms}\n`;
|
|
34
|
+
if (uiLibraries?.length) stackInfo += `UI Libraries: ${uiLibraries.join(", ")}\n`;
|
|
35
|
+
|
|
36
|
+
return `You are an expert web accessibility engineer specializing in WCAG 2.2 AA remediation.
|
|
37
|
+
|
|
38
|
+
Your task is to improve the fix guidance for accessibility findings from an automated scan.
|
|
39
|
+
${stackInfo ? `\nProject context:\n${stackInfo}` : ""}
|
|
40
|
+
For each finding you receive, provide:
|
|
41
|
+
1. A clear, specific fix description (1-2 sentences, actionable, no jargon)
|
|
42
|
+
2. Ready-to-use fix code in the correct language for the stack${context.hasSourceCode ? "\n3. The exact line/component to change if you can identify it from the source code" : ""}
|
|
43
|
+
|
|
44
|
+
Rules:
|
|
45
|
+
- Keep fix code minimal and focused — only the changed element, not the whole file
|
|
46
|
+
- Use the detected framework syntax (JSX for React/Next.js, template syntax for Vue, etc.)
|
|
47
|
+
- Do not change component logic, only accessibility attributes
|
|
48
|
+
- If the fix requires multiple changes, show the most important one
|
|
49
|
+
- Respond in JSON only — no markdown, no explanation outside the JSON structure`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Builds the user message for a batch of findings.
|
|
54
|
+
* @param {EnrichedFinding[]} findings
|
|
55
|
+
* @param {Record<string, string>} sourceFiles - map of filePath -> content
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function buildUserMessage(findings, sourceFiles) {
|
|
59
|
+
const items = findings.map((f, i) => ({
|
|
60
|
+
index: i,
|
|
61
|
+
ruleId: f.ruleId,
|
|
62
|
+
severity: f.severity,
|
|
63
|
+
wcag: f.wcag,
|
|
64
|
+
title: f.title,
|
|
65
|
+
selector: f.primarySelector || f.selector,
|
|
66
|
+
actual: f.actual,
|
|
67
|
+
currentFix: f.fixDescription,
|
|
68
|
+
currentCode: f.fixCode,
|
|
69
|
+
fileSearchPattern: f.fileSearchPattern || null,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
let message = `Improve the fix guidance for these ${findings.length} accessibility finding(s).\n\n`;
|
|
73
|
+
message += `FINDINGS:\n${JSON.stringify(items, null, 2)}\n`;
|
|
74
|
+
|
|
75
|
+
if (Object.keys(sourceFiles).length > 0) {
|
|
76
|
+
message += `\nSOURCE FILES (for context):\n`;
|
|
77
|
+
for (const [filePath, content] of Object.entries(sourceFiles)) {
|
|
78
|
+
const truncated = content.length > 4000 ? content.slice(0, 4000) + "\n... (truncated)" : content;
|
|
79
|
+
message += `\n--- ${filePath} ---\n${truncated}\n`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
message += `\nRespond with a JSON array where each item has:
|
|
84
|
+
{
|
|
85
|
+
"index": <number>,
|
|
86
|
+
"fixDescription": "<improved description>",
|
|
87
|
+
"fixCode": "<improved code snippet>",
|
|
88
|
+
"fixCodeLang": "<language: html|jsx|tsx|vue|svelte|astro|liquid|php|css>"
|
|
89
|
+
}`;
|
|
90
|
+
|
|
91
|
+
return message;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Calls the Claude API with the given messages.
|
|
96
|
+
* @param {string} apiKey
|
|
97
|
+
* @param {string} model
|
|
98
|
+
* @param {string} systemPrompt
|
|
99
|
+
* @param {string} userMessage
|
|
100
|
+
* @returns {Promise<string>}
|
|
101
|
+
*/
|
|
102
|
+
async function callClaude(apiKey, model, systemPrompt, userMessage) {
|
|
103
|
+
const res = await fetch(ANTHROPIC_API, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
"x-api-key": apiKey,
|
|
108
|
+
"anthropic-version": "2023-06-01",
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
model,
|
|
112
|
+
max_tokens: 4096,
|
|
113
|
+
system: systemPrompt,
|
|
114
|
+
messages: [{ role: "user", content: userMessage }],
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
const text = await res.text();
|
|
120
|
+
throw new Error(`Claude API error ${res.status}: ${text}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const data = await res.json();
|
|
124
|
+
return data.content?.[0]?.text ?? "";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Tries to fetch source files for findings that have a fileSearchPattern.
|
|
129
|
+
* @param {EnrichedFinding[]} findings
|
|
130
|
+
* @param {string} repoUrl
|
|
131
|
+
* @param {string|undefined} githubToken
|
|
132
|
+
* @returns {Promise<Record<string, string>>}
|
|
133
|
+
*/
|
|
134
|
+
async function fetchSourceFilesForFindings(findings, repoUrl, githubToken) {
|
|
135
|
+
const sourceFiles = {};
|
|
136
|
+
if (!repoUrl) return sourceFiles;
|
|
137
|
+
|
|
138
|
+
const { fetchRepoFile, listRepoFiles, parseRepoUrl } = await import("../core/github-api.mjs");
|
|
139
|
+
if (!parseRepoUrl(repoUrl)) return sourceFiles;
|
|
140
|
+
|
|
141
|
+
const patterns = new Set(
|
|
142
|
+
findings
|
|
143
|
+
.filter((f) => f.fileSearchPattern)
|
|
144
|
+
.map((f) => f.fileSearchPattern)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
for (const pattern of patterns) {
|
|
148
|
+
try {
|
|
149
|
+
// Extract extension from pattern (e.g. "src/components/*.tsx" -> ".tsx")
|
|
150
|
+
const extMatch = pattern.match(/\*\.(\w+)$/);
|
|
151
|
+
if (!extMatch) continue;
|
|
152
|
+
const ext = `.${extMatch[1]}`;
|
|
153
|
+
|
|
154
|
+
const files = await listRepoFiles(repoUrl, [ext], githubToken);
|
|
155
|
+
// Pick up to 3 most relevant files per pattern
|
|
156
|
+
const relevant = files.slice(0, 3);
|
|
157
|
+
for (const filePath of relevant) {
|
|
158
|
+
if (!sourceFiles[filePath]) {
|
|
159
|
+
const content = await fetchRepoFile(repoUrl, filePath, githubToken);
|
|
160
|
+
if (content) sourceFiles[filePath] = content;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// non-fatal
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return sourceFiles;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Enriches Critical and Serious findings using Claude AI.
|
|
173
|
+
* Returns findings with improved fixDescription, fixCode, and fixCodeLang.
|
|
174
|
+
* Passthrough if AI is disabled or no apiKey is provided.
|
|
175
|
+
*
|
|
176
|
+
* @param {EnrichedFinding[]} findings
|
|
177
|
+
* @param {{
|
|
178
|
+
* stack?: { framework?: string, cms?: string, uiLibraries?: string[] },
|
|
179
|
+
* repoUrl?: string,
|
|
180
|
+
* }} context
|
|
181
|
+
* @param {{
|
|
182
|
+
* enabled?: boolean,
|
|
183
|
+
* apiKey?: string,
|
|
184
|
+
* githubToken?: string,
|
|
185
|
+
* model?: string,
|
|
186
|
+
* }} options
|
|
187
|
+
* @returns {Promise<EnrichedFinding[]>}
|
|
188
|
+
*/
|
|
189
|
+
export async function enrichWithAI(findings, context = {}, options = {}) {
|
|
190
|
+
const enabled = options.enabled !== false && !!options.apiKey;
|
|
191
|
+
if (!enabled) return findings;
|
|
192
|
+
|
|
193
|
+
const model = options.model || DEFAULT_MODEL;
|
|
194
|
+
|
|
195
|
+
// Only enrich Critical and Serious findings, cap total
|
|
196
|
+
const targets = findings
|
|
197
|
+
.filter((f) => f.severity === "Critical" || f.severity === "Serious")
|
|
198
|
+
.slice(0, MAX_AI_FINDINGS);
|
|
199
|
+
|
|
200
|
+
if (targets.length === 0) return findings;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Fetch source files if repo is available
|
|
204
|
+
const sourceFiles = context.repoUrl
|
|
205
|
+
? await fetchSourceFilesForFindings(targets, context.repoUrl, options.githubToken)
|
|
206
|
+
: {};
|
|
207
|
+
|
|
208
|
+
const systemPrompt = buildSystemPrompt({
|
|
209
|
+
stack: context.stack,
|
|
210
|
+
hasSourceCode: Object.keys(sourceFiles).length > 0,
|
|
211
|
+
});
|
|
212
|
+
const userMessage = buildUserMessage(targets, sourceFiles);
|
|
213
|
+
|
|
214
|
+
const responseText = await callClaude(options.apiKey, model, systemPrompt, userMessage);
|
|
215
|
+
|
|
216
|
+
// Parse Claude's JSON response
|
|
217
|
+
let improvements = [];
|
|
218
|
+
try {
|
|
219
|
+
const jsonMatch = responseText.match(/\[[\s\S]*\]/);
|
|
220
|
+
if (jsonMatch) improvements = JSON.parse(jsonMatch[0]);
|
|
221
|
+
} catch {
|
|
222
|
+
console.warn("[a11y-engine] Could not parse Claude response as JSON, skipping AI enrichment");
|
|
223
|
+
return findings;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Build a map by target index for fast lookup
|
|
227
|
+
const targetIds = new Set(targets.map((f) => f.id));
|
|
228
|
+
const targetList = findings.filter((f) => targetIds.has(f.id));
|
|
229
|
+
const improvementMap = new Map(improvements.map((imp) => [imp.index, imp]));
|
|
230
|
+
|
|
231
|
+
// Apply improvements to findings
|
|
232
|
+
return findings.map((finding) => {
|
|
233
|
+
const targetIdx = targetList.findIndex((t) => t.id === finding.id);
|
|
234
|
+
if (targetIdx === -1) return finding;
|
|
235
|
+
const imp = improvementMap.get(targetIdx);
|
|
236
|
+
if (!imp) return finding;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
...finding,
|
|
240
|
+
fixDescription: imp.fixDescription || finding.fixDescription,
|
|
241
|
+
fixCode: imp.fixCode || finding.fixCode,
|
|
242
|
+
fixCodeLang: imp.fixCodeLang || finding.fixCodeLang,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.warn(`[a11y-engine] AI enrichment failed (non-fatal): ${err.message}`);
|
|
247
|
+
return findings;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file github-api.mjs
|
|
3
|
+
* @description Minimal GitHub API client for reading repository content without cloning.
|
|
4
|
+
* Used by the engine to fetch package.json for stack detection and source files for
|
|
5
|
+
* pattern scanning and AI enrichment.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const GITHUB_API = "https://api.github.com";
|
|
9
|
+
const GITHUB_RAW = "https://raw.githubusercontent.com";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parses a GitHub URL into owner and repo components.
|
|
13
|
+
* @param {string} repoUrl
|
|
14
|
+
* @returns {{ owner: string, repo: string, branch: string } | null}
|
|
15
|
+
*/
|
|
16
|
+
export function parseRepoUrl(repoUrl) {
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(repoUrl);
|
|
19
|
+
if (url.hostname !== "github.com") return null;
|
|
20
|
+
const parts = url.pathname.replace(/^\//, "").replace(/\.git$/, "").split("/");
|
|
21
|
+
if (parts.length < 2) return null;
|
|
22
|
+
const [owner, repo, , branch] = parts;
|
|
23
|
+
return { owner, repo, branch: branch || "main" };
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Builds Authorization header if token is present.
|
|
31
|
+
* @param {string|undefined} token
|
|
32
|
+
* @returns {Record<string, string>}
|
|
33
|
+
*/
|
|
34
|
+
function authHeaders(token) {
|
|
35
|
+
const headers = {
|
|
36
|
+
Accept: "application/vnd.github+json",
|
|
37
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
38
|
+
};
|
|
39
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
40
|
+
return headers;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fetches and parses package.json from a GitHub repository.
|
|
45
|
+
* Tries the default branch first, then falls back to 'master'.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} repoUrl
|
|
48
|
+
* @param {string|undefined} token
|
|
49
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchPackageJson(repoUrl, token) {
|
|
52
|
+
const parsed = parseRepoUrl(repoUrl);
|
|
53
|
+
if (!parsed) return null;
|
|
54
|
+
|
|
55
|
+
const { owner, repo, branch } = parsed;
|
|
56
|
+
const branches = [branch, branch === "main" ? "master" : "main"];
|
|
57
|
+
|
|
58
|
+
for (const b of branches) {
|
|
59
|
+
try {
|
|
60
|
+
const url = `${GITHUB_RAW}/${owner}/${repo}/${b}/package.json`;
|
|
61
|
+
const res = await fetch(url, {
|
|
62
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
63
|
+
});
|
|
64
|
+
if (res.ok) {
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
return JSON.parse(text);
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// try next branch
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Fetches a file from a GitHub repository.
|
|
78
|
+
* Returns the raw file content as a string.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} repoUrl
|
|
81
|
+
* @param {string} filePath - Relative path within the repo (e.g. "src/components/Header.tsx")
|
|
82
|
+
* @param {string|undefined} token
|
|
83
|
+
* @returns {Promise<string | null>}
|
|
84
|
+
*/
|
|
85
|
+
export async function fetchRepoFile(repoUrl, filePath, token) {
|
|
86
|
+
const parsed = parseRepoUrl(repoUrl);
|
|
87
|
+
if (!parsed) return null;
|
|
88
|
+
|
|
89
|
+
const { owner, repo, branch } = parsed;
|
|
90
|
+
const branches = [branch, branch === "main" ? "master" : "main"];
|
|
91
|
+
|
|
92
|
+
for (const b of branches) {
|
|
93
|
+
try {
|
|
94
|
+
const url = `${GITHUB_RAW}/${owner}/${repo}/${b}/${filePath}`;
|
|
95
|
+
const res = await fetch(url, {
|
|
96
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
97
|
+
});
|
|
98
|
+
if (res.ok) return await res.text();
|
|
99
|
+
} catch {
|
|
100
|
+
// try next branch
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Lists files in a repository directory using the GitHub Trees API.
|
|
109
|
+
* Returns file paths matching the given extensions.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} repoUrl
|
|
112
|
+
* @param {string[]} extensions - e.g. [".tsx", ".jsx", ".html"]
|
|
113
|
+
* @param {string|undefined} token
|
|
114
|
+
* @returns {Promise<string[]>}
|
|
115
|
+
*/
|
|
116
|
+
export async function listRepoFiles(repoUrl, extensions, token) {
|
|
117
|
+
const parsed = parseRepoUrl(repoUrl);
|
|
118
|
+
if (!parsed) return [];
|
|
119
|
+
|
|
120
|
+
const { owner, repo, branch } = parsed;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const url = `${GITHUB_API}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
124
|
+
const res = await fetch(url, { headers: authHeaders(token) });
|
|
125
|
+
if (!res.ok) return [];
|
|
126
|
+
|
|
127
|
+
const { tree } = await res.json();
|
|
128
|
+
if (!Array.isArray(tree)) return [];
|
|
129
|
+
|
|
130
|
+
const extSet = new Set(extensions.map((e) => e.toLowerCase()));
|
|
131
|
+
const skipDirs = new Set([
|
|
132
|
+
"node_modules", ".git", "dist", "build", ".next", ".nuxt",
|
|
133
|
+
"coverage", ".cache", "out", ".turbo", ".vercel", ".netlify",
|
|
134
|
+
"public", "static", "wp-includes", "wp-admin",
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
return tree
|
|
138
|
+
.filter((item) => {
|
|
139
|
+
if (item.type !== "blob") return false;
|
|
140
|
+
const path = item.path;
|
|
141
|
+
const parts = path.split("/");
|
|
142
|
+
if (parts.some((p) => skipDirs.has(p) || p.startsWith("."))) return false;
|
|
143
|
+
const ext = path.slice(path.lastIndexOf(".")).toLowerCase();
|
|
144
|
+
return extSet.has(ext);
|
|
145
|
+
})
|
|
146
|
+
.map((item) => item.path);
|
|
147
|
+
} catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -744,6 +744,14 @@ function computeTestingMethodology(payload) {
|
|
|
744
744
|
const routes = payload.routes || [];
|
|
745
745
|
const scanned = routes.filter((r) => !r.error).length;
|
|
746
746
|
const errored = routes.filter((r) => r.error).length;
|
|
747
|
+
const tags = payload.axeTags || [];
|
|
748
|
+
const conformanceLevel = tags.includes("wcag2aaa")
|
|
749
|
+
? "AAA"
|
|
750
|
+
: tags.includes("wcag2aa") || tags.includes("wcag21aa") || tags.includes("wcag22aa")
|
|
751
|
+
? "AA"
|
|
752
|
+
: tags.includes("wcag2a") || tags.includes("wcag21a") || tags.includes("wcag22a")
|
|
753
|
+
? "A"
|
|
754
|
+
: null;
|
|
747
755
|
return {
|
|
748
756
|
automated_tools: [
|
|
749
757
|
"axe-core (via @axe-core/playwright)",
|
|
@@ -751,7 +759,10 @@ function computeTestingMethodology(payload) {
|
|
|
751
759
|
"pa11y (HTML CodeSniffer via Puppeteer)",
|
|
752
760
|
"Playwright + Chromium",
|
|
753
761
|
],
|
|
754
|
-
compliance_target: "WCAG 2.2
|
|
762
|
+
compliance_target: "WCAG 2.2",
|
|
763
|
+
conformance_level: conformanceLevel,
|
|
764
|
+
best_practices: tags.includes("best-practice"),
|
|
765
|
+
axe_tags: tags.length > 0 ? tags : null,
|
|
755
766
|
pages_scanned: scanned,
|
|
756
767
|
pages_errored: errored,
|
|
757
768
|
framework_detected: payload.projectContext?.framework || "Not detected",
|
package/src/index.d.mts
CHANGED
|
@@ -394,12 +394,22 @@ export interface RunAuditOptions {
|
|
|
394
394
|
ignoreFindings?: string[];
|
|
395
395
|
framework?: string;
|
|
396
396
|
projectDir?: string;
|
|
397
|
+
repoUrl?: string;
|
|
398
|
+
githubToken?: string;
|
|
397
399
|
skipPatterns?: boolean;
|
|
398
400
|
screenshotsDir?: string;
|
|
399
401
|
engines?: EngineSelection;
|
|
402
|
+
ai?: AiOptions;
|
|
400
403
|
onProgress?: (step: string, status: string, extra?: Record<string, unknown>) => void;
|
|
401
404
|
}
|
|
402
405
|
|
|
406
|
+
export interface AiOptions {
|
|
407
|
+
enabled?: boolean;
|
|
408
|
+
apiKey?: string;
|
|
409
|
+
githubToken?: string;
|
|
410
|
+
model?: string;
|
|
411
|
+
}
|
|
412
|
+
|
|
403
413
|
|
|
404
414
|
|
|
405
415
|
|
package/src/index.mjs
CHANGED
|
@@ -188,6 +188,11 @@ export function getFindings(input, options = {}) {
|
|
|
188
188
|
const { screenshotUrlBuilder = null } = options;
|
|
189
189
|
const rules = getIntelligence().rules || {};
|
|
190
190
|
|
|
191
|
+
// If AI enrichment ran, return those findings directly (already normalized + enriched)
|
|
192
|
+
if (input?.ai_enriched_findings?.length > 0 && !screenshotUrlBuilder) {
|
|
193
|
+
return input.ai_enriched_findings;
|
|
194
|
+
}
|
|
195
|
+
|
|
191
196
|
const rawFindings = input?.findings || [];
|
|
192
197
|
|
|
193
198
|
// Normalize raw findings
|
|
@@ -652,6 +657,23 @@ export async function runAudit(options) {
|
|
|
652
657
|
pa11y: options.engines?.pa11y !== false,
|
|
653
658
|
};
|
|
654
659
|
|
|
660
|
+
// Fetch remote package.json via GitHub API if repoUrl is provided
|
|
661
|
+
let remotePackageJson = null;
|
|
662
|
+
if (options.repoUrl && !options.projectDir) {
|
|
663
|
+
if (onProgress) onProgress("repo", "running");
|
|
664
|
+
try {
|
|
665
|
+
const { fetchPackageJson } = await import("./core/github-api.mjs");
|
|
666
|
+
remotePackageJson = await fetchPackageJson(options.repoUrl, options.githubToken);
|
|
667
|
+
if (remotePackageJson) {
|
|
668
|
+
if (onProgress) onProgress("repo", "done", { packageJson: true });
|
|
669
|
+
} else {
|
|
670
|
+
if (onProgress) onProgress("repo", "skipped", { reason: "Could not read package.json" });
|
|
671
|
+
}
|
|
672
|
+
} catch (err) {
|
|
673
|
+
if (onProgress) onProgress("repo", "skipped", { reason: err.message });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
655
677
|
// Step 1: DOM scan (selected engines)
|
|
656
678
|
if (onProgress) onProgress("page", "running");
|
|
657
679
|
|
|
@@ -672,6 +694,7 @@ export async function runAudit(options) {
|
|
|
672
694
|
excludeSelectors: options.excludeSelectors,
|
|
673
695
|
screenshotsDir: options.screenshotsDir,
|
|
674
696
|
projectDir: options.projectDir,
|
|
697
|
+
remotePackageJson,
|
|
675
698
|
engines,
|
|
676
699
|
},
|
|
677
700
|
{ onProgress },
|
|
@@ -685,10 +708,11 @@ export async function runAudit(options) {
|
|
|
685
708
|
framework: options.framework,
|
|
686
709
|
});
|
|
687
710
|
|
|
688
|
-
// Step 3: Source patterns (optional)
|
|
689
|
-
|
|
711
|
+
// Step 3: Source patterns (optional) — works with local projectDir or remote repoUrl
|
|
712
|
+
const hasSourceContext = (options.projectDir || options.repoUrl) && !options.skipPatterns;
|
|
713
|
+
if (hasSourceContext) {
|
|
714
|
+
if (onProgress) onProgress("patterns", "running");
|
|
690
715
|
try {
|
|
691
|
-
const { resolveScanDirs, scanPattern } = await import("./source-patterns/source-scanner.mjs");
|
|
692
716
|
const { patterns } = loadAssetJson(ASSET_PATHS.remediation.codePatterns, "code-patterns.json");
|
|
693
717
|
|
|
694
718
|
let resolvedFramework = options.framework;
|
|
@@ -696,35 +720,97 @@ export async function runAudit(options) {
|
|
|
696
720
|
resolvedFramework = findingsPayload.metadata.projectContext.framework;
|
|
697
721
|
}
|
|
698
722
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
723
|
+
let allPatternFindings = [];
|
|
724
|
+
|
|
725
|
+
if (options.projectDir) {
|
|
726
|
+
// Local filesystem scan
|
|
727
|
+
const { resolveScanDirs, scanPattern } = await import("./source-patterns/source-scanner.mjs");
|
|
728
|
+
const scanDirs = resolveScanDirs(resolvedFramework || null, options.projectDir);
|
|
729
|
+
for (const pattern of patterns) {
|
|
730
|
+
for (const scanDir of scanDirs) {
|
|
731
|
+
allPatternFindings.push(...scanPattern(pattern, scanDir, options.projectDir));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
} else if (options.repoUrl) {
|
|
735
|
+
// Remote GitHub API scan
|
|
736
|
+
const { scanPatternRemote } = await import("./source-patterns/source-scanner.mjs");
|
|
737
|
+
for (const pattern of patterns) {
|
|
738
|
+
const remoteFindings = await scanPatternRemote(
|
|
739
|
+
pattern,
|
|
740
|
+
options.repoUrl,
|
|
741
|
+
options.githubToken,
|
|
742
|
+
resolvedFramework || null,
|
|
743
|
+
);
|
|
744
|
+
allPatternFindings.push(...remoteFindings);
|
|
704
745
|
}
|
|
705
746
|
}
|
|
706
747
|
|
|
748
|
+
const confirmed = allPatternFindings.filter((f) => f.status === "confirmed").length;
|
|
749
|
+
const potential = allPatternFindings.filter((f) => f.status === "potential").length;
|
|
750
|
+
|
|
707
751
|
if (allPatternFindings.length > 0) {
|
|
708
752
|
findingsPayload.patternFindings = {
|
|
709
753
|
generated_at: new Date().toISOString(),
|
|
710
|
-
project_dir: options.projectDir,
|
|
754
|
+
project_dir: options.projectDir || options.repoUrl,
|
|
711
755
|
findings: allPatternFindings,
|
|
712
|
-
summary: {
|
|
713
|
-
total: allPatternFindings.length,
|
|
714
|
-
confirmed: allPatternFindings.filter((f) => f.status === "confirmed").length,
|
|
715
|
-
potential: allPatternFindings.filter((f) => f.status === "potential").length,
|
|
716
|
-
},
|
|
756
|
+
summary: { total: allPatternFindings.length, confirmed, potential },
|
|
717
757
|
};
|
|
718
758
|
}
|
|
759
|
+
|
|
760
|
+
if (onProgress) onProgress("patterns", "done", {
|
|
761
|
+
total: allPatternFindings.length,
|
|
762
|
+
confirmed,
|
|
763
|
+
potential,
|
|
764
|
+
});
|
|
719
765
|
} catch (err) {
|
|
720
|
-
// Non-fatal: source scanning is optional
|
|
721
766
|
const msg = err instanceof Error ? err.message : String(err);
|
|
767
|
+
if (onProgress) onProgress("patterns", "skipped", { reason: msg });
|
|
722
768
|
console.warn(`Source pattern scan failed (non-fatal): ${msg}`);
|
|
723
769
|
}
|
|
724
770
|
}
|
|
725
771
|
|
|
726
772
|
if (onProgress) onProgress("intelligence", "done");
|
|
727
773
|
|
|
774
|
+
// Step 4: AI enrichment (optional) — requires ANTHROPIC_API_KEY
|
|
775
|
+
const aiOptions = options.ai || {};
|
|
776
|
+
const aiEnabled = aiOptions.enabled !== false && !!aiOptions.apiKey;
|
|
777
|
+
if (!aiEnabled && onProgress && options.ai !== undefined) {
|
|
778
|
+
onProgress("ai", "skipped", { reason: "No API key configured" });
|
|
779
|
+
}
|
|
780
|
+
if (aiEnabled) {
|
|
781
|
+
try {
|
|
782
|
+
if (onProgress) onProgress("ai", "running");
|
|
783
|
+
const { enrichWithAI } = await import("./ai/claude.mjs");
|
|
784
|
+
|
|
785
|
+
const projectContext = findingsPayload.metadata?.projectContext || {};
|
|
786
|
+
const rawFindings = getFindings(findingsPayload);
|
|
787
|
+
|
|
788
|
+
const enrichedFindings = await enrichWithAI(
|
|
789
|
+
rawFindings,
|
|
790
|
+
{
|
|
791
|
+
stack: {
|
|
792
|
+
framework: projectContext.framework || null,
|
|
793
|
+
cms: projectContext.cms || null,
|
|
794
|
+
uiLibraries: projectContext.uiLibraries || [],
|
|
795
|
+
},
|
|
796
|
+
repoUrl: options.repoUrl,
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
enabled: true,
|
|
800
|
+
apiKey: aiOptions.apiKey,
|
|
801
|
+
githubToken: aiOptions.githubToken || options.githubToken,
|
|
802
|
+
model: aiOptions.model,
|
|
803
|
+
}
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
// Store enriched findings back into the payload
|
|
807
|
+
findingsPayload.ai_enriched_findings = enrichedFindings;
|
|
808
|
+
if (onProgress) onProgress("ai", "done");
|
|
809
|
+
} catch (err) {
|
|
810
|
+
console.warn(`[a11y-engine] AI step failed (non-fatal): ${err.message}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
728
814
|
// Attach active engines to metadata so consumers know which ran
|
|
729
815
|
findingsPayload.metadata = findingsPayload.metadata || {};
|
|
730
816
|
findingsPayload.metadata.engines = engines;
|
|
@@ -1029,7 +1115,7 @@ export async function getHTMLReport(payload, options = {}) {
|
|
|
1029
1115
|
</div>
|
|
1030
1116
|
<div class="bg-white rounded-xl p-4 border border-slate-200 shadow-sm">
|
|
1031
1117
|
<div class="text-3xl font-black ${wcagStatus === 'Pass' ? 'text-emerald-600' : 'text-rose-600'}">${wcagStatus}</div>
|
|
1032
|
-
<div class="text-xs font-bold text-slate-500 uppercase">WCAG 2.2
|
|
1118
|
+
<div class="text-xs font-bold text-slate-500 uppercase">WCAG 2.2</div>
|
|
1033
1119
|
</div>
|
|
1034
1120
|
<div class="bg-white rounded-xl p-4 border border-slate-200 shadow-sm">
|
|
1035
1121
|
<div class="text-3xl font-black">${Object.keys(pageGroups).length}</div>
|
|
@@ -341,16 +341,62 @@ export async function discoverRoutes(page, baseUrl, maxRoutes, crawlDepth = 2) {
|
|
|
341
341
|
return [...routes].slice(0, maxRoutes);
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Extracts framework and UI library info from a parsed package.json object.
|
|
346
|
+
* Used both for local file reads and remote GitHub API reads.
|
|
347
|
+
* @param {Record<string, unknown>} pkg
|
|
348
|
+
* @returns {{ framework: string|null, uiLibraries: string[] }}
|
|
349
|
+
*/
|
|
350
|
+
function detectFromPackageJson(pkg) {
|
|
351
|
+
const uiLibraries = [];
|
|
352
|
+
let pkgFramework = null;
|
|
353
|
+
|
|
354
|
+
const allDeps = Object.keys({
|
|
355
|
+
...(pkg.dependencies || {}),
|
|
356
|
+
...(pkg.devDependencies || {}),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
for (const [dep, fw] of STACK_DETECTION.frameworkPackageDetectors) {
|
|
360
|
+
if (allDeps.some((d) => d === dep || d.startsWith(`${dep}/`))) {
|
|
361
|
+
pkgFramework = fw;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
for (const [prefix, name] of STACK_DETECTION.uiLibraryPackageDetectors) {
|
|
366
|
+
if (allDeps.some((d) => d === prefix || d.startsWith(`${prefix}/`))) {
|
|
367
|
+
uiLibraries.push(name);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { framework: pkgFramework, uiLibraries };
|
|
372
|
+
}
|
|
373
|
+
|
|
344
374
|
/**
|
|
345
375
|
* Detects the web framework and UI libraries used by analyzing package.json and file structure.
|
|
376
|
+
* Accepts either a local project directory path or a pre-parsed package.json object
|
|
377
|
+
* (useful when the package.json was fetched remotely via GitHub API).
|
|
378
|
+
*
|
|
346
379
|
* @param {string|null} [explicitProjectDir=null] - Explicit project directory. Falls back to env/cwd.
|
|
380
|
+
* @param {Record<string, unknown>|null} [remotePackageJson=null] - Pre-parsed package.json from GitHub API.
|
|
347
381
|
* @returns {Object} An object containing detected framework and UI libraries.
|
|
348
382
|
*/
|
|
349
|
-
function detectProjectContext(explicitProjectDir = null) {
|
|
383
|
+
function detectProjectContext(explicitProjectDir = null, remotePackageJson = null) {
|
|
350
384
|
const uiLibraries = [];
|
|
351
385
|
let pkgFramework = null;
|
|
352
386
|
let fileFramework = null;
|
|
353
387
|
|
|
388
|
+
// If a remote package.json was provided (from GitHub API), use it directly
|
|
389
|
+
if (remotePackageJson) {
|
|
390
|
+
const result = detectFromPackageJson(remotePackageJson);
|
|
391
|
+
if (result.framework) {
|
|
392
|
+
log.info(`Detected framework: ${result.framework} (from remote package.json)`);
|
|
393
|
+
}
|
|
394
|
+
if (result.uiLibraries.length) {
|
|
395
|
+
log.info(`Detected UI libraries: ${result.uiLibraries.join(", ")}`);
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
|
|
354
400
|
const projectDir = explicitProjectDir || process.env.A11Y_PROJECT_DIR || null;
|
|
355
401
|
if (!projectDir) {
|
|
356
402
|
return { framework: null, uiLibraries: [] };
|
|
@@ -360,21 +406,9 @@ function detectProjectContext(explicitProjectDir = null) {
|
|
|
360
406
|
const pkgPath = path.join(projectDir, "package.json");
|
|
361
407
|
if (fs.existsSync(pkgPath)) {
|
|
362
408
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
});
|
|
367
|
-
for (const [dep, fw] of STACK_DETECTION.frameworkPackageDetectors) {
|
|
368
|
-
if (allDeps.some((d) => d === dep || d.startsWith(`${dep}/`))) {
|
|
369
|
-
pkgFramework = fw;
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
for (const [prefix, name] of STACK_DETECTION.uiLibraryPackageDetectors) {
|
|
374
|
-
if (allDeps.some((d) => d === prefix || d.startsWith(`${prefix}/`))) {
|
|
375
|
-
uiLibraries.push(name);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
409
|
+
const result = detectFromPackageJson(pkg);
|
|
410
|
+
pkgFramework = result.framework;
|
|
411
|
+
uiLibraries.push(...result.uiLibraries);
|
|
378
412
|
}
|
|
379
413
|
} catch { /* package.json unreadable */ }
|
|
380
414
|
|
|
@@ -951,6 +985,7 @@ export async function runDomScanner(options = {}, callbacks = {}) {
|
|
|
951
985
|
viewport: options.viewport || null,
|
|
952
986
|
axeTags: options.axeTags || null,
|
|
953
987
|
projectDir: options.projectDir || null,
|
|
988
|
+
remotePackageJson: options.remotePackageJson || null,
|
|
954
989
|
engines: {
|
|
955
990
|
axe: options.engines?.axe !== false,
|
|
956
991
|
cdp: options.engines?.cdp !== false,
|
|
@@ -1002,8 +1037,8 @@ async function _runDomScannerInternal(args) {
|
|
|
1002
1037
|
timeout: args.timeoutMs,
|
|
1003
1038
|
});
|
|
1004
1039
|
|
|
1005
|
-
// 1. File-system / package.json detection (
|
|
1006
|
-
const repoCtx = detectProjectContext(args.projectDir || null);
|
|
1040
|
+
// 1. File-system / package.json detection (local projectDir) or remote package.json
|
|
1041
|
+
const repoCtx = detectProjectContext(args.projectDir || null, args.remotePackageJson || null);
|
|
1007
1042
|
|
|
1008
1043
|
// 2. DOM/runtime detection (always works for any remote URL)
|
|
1009
1044
|
let domCtx = { framework: null, cms: null, uiLibraries: [] };
|
|
@@ -1253,6 +1288,7 @@ async function _runDomScannerInternal(args) {
|
|
|
1253
1288
|
base_url: baseUrl,
|
|
1254
1289
|
onlyRule: args.onlyRule || null,
|
|
1255
1290
|
engines: args.engines,
|
|
1291
|
+
axeTags: args.axeTags || null,
|
|
1256
1292
|
projectContext,
|
|
1257
1293
|
routes: results,
|
|
1258
1294
|
};
|
|
@@ -152,7 +152,7 @@ function buildHtml(args) {
|
|
|
152
152
|
</div>
|
|
153
153
|
|
|
154
154
|
<footer class="mt-12 py-6 border-t border-slate-200 text-center">
|
|
155
|
-
<p class="text-slate-600 text-sm font-medium">Generated by <a href="https://github.com/diegovelasquezweb/a11y" target="_blank" class="text-slate-700 hover:text-amber-700 font-semibold transition-colors">a11y</a> • <span class="text-slate-700">WCAG 2.2
|
|
155
|
+
<p class="text-slate-600 text-sm font-medium">Generated by <a href="https://github.com/diegovelasquezweb/a11y" target="_blank" class="text-slate-700 hover:text-amber-700 font-semibold transition-colors">a11y</a> • <span class="text-slate-700">WCAG 2.2</span></p>
|
|
156
156
|
</footer>
|
|
157
157
|
|
|
158
158
|
</main>
|
|
@@ -564,7 +564,7 @@ ${rows.join("\n")}
|
|
|
564
564
|
const sourceBoundariesSection = buildSourceBoundariesSection(framework);
|
|
565
565
|
|
|
566
566
|
return (
|
|
567
|
-
`# Accessibility Remediation Guide — WCAG 2.2
|
|
567
|
+
`# Accessibility Remediation Guide — WCAG 2.2
|
|
568
568
|
> **Base URL:** ${args.baseUrl || "N/A"}
|
|
569
569
|
|
|
570
570
|
| Severity | Count |
|
|
@@ -230,6 +230,93 @@ export function scanPattern(pattern, scanDir, projectDir = scanDir) {
|
|
|
230
230
|
return findings;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Scans files in a remote GitHub repository for a pattern.
|
|
235
|
+
* Functionally equivalent to scanPattern but reads files via GitHub API.
|
|
236
|
+
*
|
|
237
|
+
* @param {Object} pattern
|
|
238
|
+
* @param {string} repoUrl
|
|
239
|
+
* @param {string|undefined} githubToken
|
|
240
|
+
* @param {string|null} framework
|
|
241
|
+
* @returns {Promise<Object[]>}
|
|
242
|
+
*/
|
|
243
|
+
export async function scanPatternRemote(pattern, repoUrl, githubToken, framework = null) {
|
|
244
|
+
const { fetchRepoFile, listRepoFiles, parseRepoUrl } = await import("../core/github-api.mjs");
|
|
245
|
+
const parsed = parseRepoUrl(repoUrl);
|
|
246
|
+
if (!parsed) return [];
|
|
247
|
+
|
|
248
|
+
const extensions = [...parseExtensions(pattern.globs)];
|
|
249
|
+
if (extensions.length === 0) return [];
|
|
250
|
+
|
|
251
|
+
const allFiles = await listRepoFiles(repoUrl, extensions, githubToken);
|
|
252
|
+
|
|
253
|
+
// Scope to framework boundaries if known
|
|
254
|
+
const boundaries = framework ? SOURCE_BOUNDARIES?.[framework] : null;
|
|
255
|
+
let scopedFiles = allFiles;
|
|
256
|
+
if (boundaries) {
|
|
257
|
+
const allGlobs = [boundaries.components, boundaries.styles]
|
|
258
|
+
.filter(Boolean)
|
|
259
|
+
.flatMap((g) => g.split(",").map((s) => s.trim()));
|
|
260
|
+
|
|
261
|
+
const prefixes = new Set();
|
|
262
|
+
for (const glob of allGlobs) {
|
|
263
|
+
const prefix = glob.split(/[*?{]/)[0].replace(/\/$/, "");
|
|
264
|
+
if (prefix) prefixes.add(prefix);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (prefixes.size > 0) {
|
|
268
|
+
scopedFiles = allFiles.filter((f) =>
|
|
269
|
+
[...prefixes].some((p) => f.startsWith(p))
|
|
270
|
+
);
|
|
271
|
+
if (scopedFiles.length === 0) scopedFiles = allFiles; // fallback
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const regex = new RegExp(pattern.regex, "gi");
|
|
276
|
+
const findings = [];
|
|
277
|
+
|
|
278
|
+
for (const filePath of scopedFiles) {
|
|
279
|
+
const content = await fetchRepoFile(repoUrl, filePath, githubToken);
|
|
280
|
+
if (!content) continue;
|
|
281
|
+
|
|
282
|
+
const lines = content.split("\n");
|
|
283
|
+
|
|
284
|
+
for (let i = 0; i < lines.length; i++) {
|
|
285
|
+
regex.lastIndex = 0;
|
|
286
|
+
if (!regex.test(lines[i])) continue;
|
|
287
|
+
|
|
288
|
+
const contextStart = Math.max(0, i - 3);
|
|
289
|
+
const contextEnd = Math.min(lines.length - 1, i + 3);
|
|
290
|
+
const context = lines
|
|
291
|
+
.slice(contextStart, contextEnd + 1)
|
|
292
|
+
.map((l, idx) => `${contextStart + idx + 1} ${l}`)
|
|
293
|
+
.join("\n");
|
|
294
|
+
|
|
295
|
+
const confirmed = isConfirmedByContext(pattern, lines, i);
|
|
296
|
+
|
|
297
|
+
findings.push({
|
|
298
|
+
id: makeFindingId(pattern.id, filePath, i + 1),
|
|
299
|
+
pattern_id: pattern.id,
|
|
300
|
+
title: pattern.title,
|
|
301
|
+
severity: pattern.severity,
|
|
302
|
+
wcag: pattern.wcag,
|
|
303
|
+
wcag_criterion: pattern.wcag_criterion,
|
|
304
|
+
wcag_level: pattern.wcag_level,
|
|
305
|
+
type: pattern.type,
|
|
306
|
+
fix_description: pattern.fix_description ?? null,
|
|
307
|
+
status: confirmed ? "confirmed" : "potential",
|
|
308
|
+
file: filePath,
|
|
309
|
+
line: i + 1,
|
|
310
|
+
match: lines[i].trim(),
|
|
311
|
+
context,
|
|
312
|
+
source: "code-pattern",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return findings;
|
|
318
|
+
}
|
|
319
|
+
|
|
233
320
|
/**
|
|
234
321
|
* Main execution function for the pattern scanner.
|
|
235
322
|
*/
|