@diegovelasquezweb/a11y-engine 0.11.32 → 0.11.34
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegovelasquezweb/a11y-engine",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.34",
|
|
4
4
|
"description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
"CHANGELOG.md",
|
|
33
33
|
"LICENSE"
|
|
34
34
|
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "vitest run"
|
|
37
|
+
},
|
|
35
38
|
"dependencies": {
|
|
36
39
|
"@axe-core/playwright": "^4.11.1",
|
|
37
40
|
"axe-core": "^4.11.1",
|
|
@@ -41,7 +44,5 @@
|
|
|
41
44
|
"devDependencies": {
|
|
42
45
|
"vitest": "^4.0.18"
|
|
43
46
|
},
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
}
|
|
47
|
+
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
|
|
48
|
+
}
|
|
File without changes
|
|
@@ -167,15 +167,56 @@ function buildPatternAiInput({ finding, candidate }) {
|
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// Layout file name patterns for page-level fixes (e.g. bypass/skip-link).
|
|
171
|
+
// Ordered by priority: more specific names first.
|
|
172
|
+
const LAYOUT_FILE_PATTERNS = [
|
|
173
|
+
/^app[\/\\]layout\.[jt]sx?$/i,
|
|
174
|
+
/^src[\/\\]app[\/\\]layout\.[jt]sx?$/i,
|
|
175
|
+
/^pages[\/\\]_app\.[jt]sx?$/i,
|
|
176
|
+
/^pages[\/\\]_document\.[jt]sx?$/i,
|
|
177
|
+
/^src[\/\\]pages[\/\\]_app\.[jt]sx?$/i,
|
|
178
|
+
/^src[\/\\]pages[\/\\]_document\.[jt]sx?$/i,
|
|
179
|
+
/layouts[\/\\]default\.vue$/i,
|
|
180
|
+
/layouts[\/\\][^\/\\]+\.vue$/i,
|
|
181
|
+
/\+layout\.svelte$/i,
|
|
182
|
+
/app\.component\.html$/i,
|
|
183
|
+
/layouts[\/\\][^\/\\]+\.astro$/i,
|
|
184
|
+
/layout[\/\\]theme\.liquid$/i,
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
function getLayoutCandidates(projectDir, files) {
|
|
188
|
+
const byPattern = [];
|
|
189
|
+
for (const { abs, rel, content } of files) {
|
|
190
|
+
const normalRel = rel.replace(/\\/g, "/");
|
|
191
|
+
for (let i = 0; i < LAYOUT_FILE_PATTERNS.length; i++) {
|
|
192
|
+
if (LAYOUT_FILE_PATTERNS[i].test(normalRel)) {
|
|
193
|
+
byPattern.push({ abs, rel, content, score: LAYOUT_FILE_PATTERNS.length - i });
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return byPattern.sort((a, b) => b.score - a.score).slice(0, MAX_CANDIDATE_FILES);
|
|
199
|
+
}
|
|
200
|
+
|
|
170
201
|
function getCandidateFiles(projectDir, finding) {
|
|
171
|
-
const
|
|
202
|
+
const allFiles = listFilesRecursive(projectDir).map((abs) => {
|
|
203
|
+
const content = fs.readFileSync(abs, "utf8");
|
|
204
|
+
const rel = path.relative(projectDir, abs);
|
|
205
|
+
return { abs, rel, content };
|
|
206
|
+
});
|
|
207
|
+
|
|
172
208
|
const tokens = selectorTokens(finding.selector);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
209
|
+
|
|
210
|
+
if (tokens.length === 0) {
|
|
211
|
+
// Selector has no useful tokens (e.g. "html", "body") — target layout files directly.
|
|
212
|
+
const layoutCandidates = getLayoutCandidates(projectDir, allFiles);
|
|
213
|
+
if (layoutCandidates.length > 0) return layoutCandidates;
|
|
214
|
+
// Fallback: return all files with equal weight so Claude gets the full picture.
|
|
215
|
+
return allFiles.slice(0, MAX_CANDIDATE_FILES).map((f) => ({ ...f, score: 1 }));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const ranked = allFiles
|
|
219
|
+
.map((f) => ({ ...f, score: scoreFile(f.rel, f.content, tokens) }))
|
|
179
220
|
.filter((item) => item.score > 0)
|
|
180
221
|
.sort((a, b) => b.score - a.score)
|
|
181
222
|
.slice(0, MAX_CANDIDATE_FILES);
|
|
@@ -216,11 +257,19 @@ function groupFindingsByFile(domFindings, projectDir) {
|
|
|
216
257
|
|
|
217
258
|
for (const finding of domFindings) {
|
|
218
259
|
const tokens = selectorTokens(finding.selector);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
260
|
+
let ranked;
|
|
261
|
+
if (tokens.length === 0) {
|
|
262
|
+
const layoutCandidates = getLayoutCandidates(projectDir, allFiles);
|
|
263
|
+
ranked = layoutCandidates.length > 0
|
|
264
|
+
? layoutCandidates
|
|
265
|
+
: allFiles.slice(0, MAX_CANDIDATE_FILES).map((f) => ({ ...f, score: 1 }));
|
|
266
|
+
} else {
|
|
267
|
+
ranked = allFiles
|
|
268
|
+
.map((f) => ({ ...f, score: scoreFile(f.rel, f.content, tokens) }))
|
|
269
|
+
.filter((f) => f.score > 0)
|
|
270
|
+
.sort((a, b) => b.score - a.score)
|
|
271
|
+
.slice(0, MAX_CANDIDATE_FILES);
|
|
272
|
+
}
|
|
224
273
|
|
|
225
274
|
const key = ranked.length > 0 ? ranked[0].rel : `__no_candidates_${finding.id}`;
|
|
226
275
|
if (!initialGroups.has(key)) {
|
|
@@ -322,11 +371,14 @@ function parseJsonBlock(text) {
|
|
|
322
371
|
async function callClaudeForPatch({ apiKey, model, aiInput }) {
|
|
323
372
|
const system = [
|
|
324
373
|
"You are an accessibility fix engine.",
|
|
325
|
-
"Return JSON only.",
|
|
326
|
-
"
|
|
327
|
-
"
|
|
328
|
-
"
|
|
329
|
-
"
|
|
374
|
+
"Return JSON only — no markdown fences, no extra text.",
|
|
375
|
+
"The input contains either a single 'finding' object or a 'findings' array. Fix EVERY finding present.",
|
|
376
|
+
"For each finding, read its fixDescription and constraints.must fields to understand the required fix.",
|
|
377
|
+
"Generate text-replacement changes in the provided source files.",
|
|
378
|
+
"CRITICAL — filePath rules: use the EXACT filePath string from the files array. Never derive filePath from area, url, selector, or any other field. Never add or remove leading slashes or file extensions.",
|
|
379
|
+
"CRITICAL — search accuracy: the 'search' value must be a verbatim copy of a substring from the file content. Do not paraphrase, reformat, or reconstruct it — copy it character-for-character.",
|
|
380
|
+
"For insertions (new element not yet in the file), anchor the search on the nearest existing surrounding element and include it in both search and replace.",
|
|
381
|
+
"Do not create new files. Only write changes for filePaths listed in the files array.",
|
|
330
382
|
"Schema:",
|
|
331
383
|
"{\"changes\":[{\"filePath\":\"...\",\"search\":\"...\",\"replace\":\"...\"}],\"verifyRule\":\"...\",\"verifyRoute\":\"...\",\"notes\":\"...\"}",
|
|
332
384
|
].join("\n");
|
|
@@ -364,6 +416,11 @@ async function callClaudeForPatch({ apiKey, model, aiInput }) {
|
|
|
364
416
|
return { patch: parsed, usage };
|
|
365
417
|
}
|
|
366
418
|
|
|
419
|
+
function normalizeFilePath(filePath) {
|
|
420
|
+
// Strip leading slashes Claude may copy from finding.area (e.g. "/contact.html" → "contact.html")
|
|
421
|
+
return typeof filePath === "string" ? filePath.replace(/^\/+/, "") : filePath;
|
|
422
|
+
}
|
|
423
|
+
|
|
367
424
|
function validateAiPatchOutput(output, projectDir, fileSet) {
|
|
368
425
|
if (!isObject(output)) return { ok: false, reason: "AI patch output is empty" };
|
|
369
426
|
if (!Array.isArray(output.changes) || output.changes.length === 0) {
|
|
@@ -372,18 +429,22 @@ function validateAiPatchOutput(output, projectDir, fileSet) {
|
|
|
372
429
|
|
|
373
430
|
for (const change of output.changes) {
|
|
374
431
|
if (!isObject(change)) return { ok: false, reason: "Invalid change item" };
|
|
375
|
-
const
|
|
432
|
+
const rawPath = typeof change.filePath === "string" ? change.filePath.trim() : "";
|
|
433
|
+
const filePath = normalizeFilePath(rawPath);
|
|
376
434
|
const search = typeof change.search === "string" ? change.search : "";
|
|
377
435
|
const replace = typeof change.replace === "string" ? change.replace : "";
|
|
378
436
|
if (!filePath || !search) return { ok: false, reason: "Change is missing filePath/search" };
|
|
379
|
-
if (!fileSet.has(filePath)) return { ok: false, reason: `Change file not in candidate set: ${
|
|
437
|
+
if (!fileSet.has(filePath)) return { ok: false, reason: `Change file not in candidate set: ${rawPath}` };
|
|
380
438
|
if (search === replace) return { ok: false, reason: `AI generated a no-op patch for ${filePath} — search and replace are identical` };
|
|
381
439
|
|
|
382
440
|
const abs = path.resolve(projectDir, filePath);
|
|
383
|
-
if (!isWithin(projectDir, abs)
|
|
441
|
+
if (!isWithin(projectDir, abs)) {
|
|
384
442
|
return { ok: false, reason: `Change path escapes projectDir: ${filePath}` };
|
|
385
443
|
}
|
|
386
444
|
if (replace.length > 20000) return { ok: false, reason: `Replacement too large for ${filePath}` };
|
|
445
|
+
|
|
446
|
+
// Normalize in-place so downstream applyChanges uses the clean path
|
|
447
|
+
change.filePath = filePath;
|
|
387
448
|
}
|
|
388
449
|
|
|
389
450
|
return { ok: true };
|
|
File without changes
|
package/src/reports/html.mjs
CHANGED
|
File without changes
|
package/assets/.DS_Store
DELETED
|
Binary file
|