@diegovelasquezweb/a11y-engine 0.11.50 → 0.11.52
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 +1 -1
- package/src/fixes/apply-finding-fix.mjs +54 -13
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import { ASSETS } from "../core/asset-loader.mjs";
|
|
|
5
5
|
const ANTHROPIC_API = "https://api.anthropic.com/v1/messages";
|
|
6
6
|
const DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
7
7
|
const MAX_CANDIDATE_FILES = 12;
|
|
8
|
-
const SUPPORTED_EXTENSIONS = new Set([".html", ".htm", ".jsx", ".tsx", ".vue", ".astro", ".liquid"]);
|
|
8
|
+
const SUPPORTED_EXTENSIONS = new Set([".html", ".htm", ".jsx", ".tsx", ".vue", ".astro", ".liquid", ".css", ".scss", ".sass"]);
|
|
9
9
|
|
|
10
10
|
export const FIX_ERROR_CODES = {
|
|
11
11
|
INVALID_INPUT: "invalid-input",
|
|
@@ -277,12 +277,16 @@ function getCandidateFiles(projectDir, finding) {
|
|
|
277
277
|
return allFiles.slice(0, MAX_CANDIDATE_FILES).map((f) => ({ ...f, score: 1 }));
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
const styleFiles = allFiles.filter((f) => /\.(css|scss|sass)$/.test(f.rel));
|
|
281
|
+
|
|
280
282
|
const ranked = allFiles
|
|
283
|
+
.filter((f) => !/\.(css|scss|sass)$/.test(f.rel))
|
|
281
284
|
.map((f) => ({ ...f, score: scoreFile(f.rel, f.content, tokens) }))
|
|
282
285
|
.filter((item) => item.score > 0)
|
|
283
286
|
.sort((a, b) => b.score - a.score)
|
|
284
|
-
.slice(0, MAX_CANDIDATE_FILES);
|
|
285
|
-
|
|
287
|
+
.slice(0, MAX_CANDIDATE_FILES - styleFiles.length);
|
|
288
|
+
|
|
289
|
+
return [...ranked, ...styleFiles.map((f) => ({ ...f, score: 1 }))];
|
|
286
290
|
}
|
|
287
291
|
|
|
288
292
|
function buildExecution(ruleId, intelligenceRule, finding) {
|
|
@@ -326,11 +330,14 @@ function groupFindingsByFile(domFindings, projectDir) {
|
|
|
326
330
|
? layoutCandidates
|
|
327
331
|
: allFiles.slice(0, MAX_CANDIDATE_FILES).map((f) => ({ ...f, score: 1 }));
|
|
328
332
|
} else {
|
|
329
|
-
|
|
333
|
+
const styleFiles = allFiles.filter((f) => /\.(css|scss|sass)$/.test(f.rel));
|
|
334
|
+
const nonStyle = allFiles
|
|
335
|
+
.filter((f) => !/\.(css|scss|sass)$/.test(f.rel))
|
|
330
336
|
.map((f) => ({ ...f, score: scoreFile(f.rel, f.content, tokens) }))
|
|
331
337
|
.filter((f) => f.score > 0)
|
|
332
338
|
.sort((a, b) => b.score - a.score)
|
|
333
|
-
.slice(0, MAX_CANDIDATE_FILES);
|
|
339
|
+
.slice(0, MAX_CANDIDATE_FILES - styleFiles.length);
|
|
340
|
+
ranked = [...nonStyle, ...styleFiles.map((f) => ({ ...f, score: 1 }))];
|
|
334
341
|
}
|
|
335
342
|
|
|
336
343
|
const key = ranked.length > 0 ? ranked[0].rel : `__no_candidates_${finding.id}`;
|
|
@@ -449,8 +456,25 @@ function extractRemediationContext(remediationPath) {
|
|
|
449
456
|
}
|
|
450
457
|
}
|
|
451
458
|
|
|
459
|
+
function detectStylingSystem(aiInput) {
|
|
460
|
+
const files = Array.isArray(aiInput?.files) ? aiInput.files : [];
|
|
461
|
+
const hasTailwind = files.some((f) => /tailwind\.config\.(js|ts|mjs|cjs)$/.test(f.filePath));
|
|
462
|
+
const hasCss = files.some((f) => /\.(css|scss|sass)$/.test(f.filePath));
|
|
463
|
+
if (hasTailwind) return "tailwind";
|
|
464
|
+
if (hasCss) return "css";
|
|
465
|
+
return "inline";
|
|
466
|
+
}
|
|
467
|
+
|
|
452
468
|
async function callClaudeForPatch({ apiKey, model, aiInput, remediationPath }) {
|
|
453
469
|
const remediationContext = extractRemediationContext(remediationPath);
|
|
470
|
+
const stylingSystem = detectStylingSystem(aiInput);
|
|
471
|
+
|
|
472
|
+
const styleInstruction =
|
|
473
|
+
stylingSystem === "tailwind"
|
|
474
|
+
? "This project uses Tailwind CSS. Apply style fixes as Tailwind utility classes in the HTML/JSX file — do not write raw CSS."
|
|
475
|
+
: stylingSystem === "css"
|
|
476
|
+
? "CSS/SCSS files are available in the files array. Prefer fixing visual issues (touch targets, color contrast, focus outlines) in the CSS/SCSS file using proper selectors rather than inline styles."
|
|
477
|
+
: "No CSS file was provided. Apply style fixes using inline style attributes in the HTML file.";
|
|
454
478
|
|
|
455
479
|
const system = [
|
|
456
480
|
"You are an accessibility fix engine.",
|
|
@@ -462,7 +486,7 @@ async function callClaudeForPatch({ apiKey, model, aiInput, remediationPath }) {
|
|
|
462
486
|
"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.",
|
|
463
487
|
"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.",
|
|
464
488
|
"Do not create new files. Only write changes for filePaths listed in the files array.",
|
|
465
|
-
|
|
489
|
+
styleInstruction,
|
|
466
490
|
...(remediationContext ? ["", "## Project Context (from audit report)", remediationContext] : []),
|
|
467
491
|
"Schema:",
|
|
468
492
|
"{\"changes\":[{\"filePath\":\"...\",\"search\":\"...\",\"replace\":\"...\"}],\"verifyRule\":\"...\",\"verifyRoute\":\"...\",\"notes\":\"...\"}",
|
|
@@ -672,19 +696,36 @@ export async function applyFindingFix(input) {
|
|
|
672
696
|
|
|
673
697
|
const validation = validateAiPatchOutput(patchOutput, projectDir, candidateSet);
|
|
674
698
|
if (!validation.ok) {
|
|
675
|
-
// When Claude returns no changes
|
|
676
|
-
// batch) already resolved this issue.
|
|
677
|
-
//
|
|
699
|
+
// When Claude returns no changes OR a no-op patch (search===replace), it may be
|
|
700
|
+
// because a prior fix (e.g. the DOM batch) already resolved this issue.
|
|
701
|
+
// Verify by checking if the pattern's context_reject_regex now matches the
|
|
702
|
+
// current file content around the target element.
|
|
678
703
|
// If it does, the element is already accessible — count as resolved.
|
|
679
|
-
|
|
704
|
+
const isNoChanges = validation.reason === "AI patch output has no changes";
|
|
705
|
+
const isNoop = validation.reason.startsWith("AI generated a no-op patch for ");
|
|
706
|
+
if (isNoChanges || isNoop) {
|
|
680
707
|
const patternId = finding.pattern_id || finding.patternId || "";
|
|
681
708
|
const patternDef = (ASSETS.remediation.codePatterns?.patterns || [])
|
|
682
709
|
.find((p) => p.id === patternId);
|
|
683
710
|
const rejectRegex = patternDef?.context_reject_regex;
|
|
684
711
|
if (rejectRegex) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
712
|
+
// For no-op patches, read the CURRENT file content — the DOM batch may have
|
|
713
|
+
// already resolved this finding by modifying the file after the scan.
|
|
714
|
+
let context;
|
|
715
|
+
if (isNoop) {
|
|
716
|
+
try {
|
|
717
|
+
const currentContent = fs.readFileSync(candidate.abs, "utf8");
|
|
718
|
+
const fileLines = currentContent.split("\n");
|
|
719
|
+
const lineIdx = Math.max(0, (aiInput.finding.line || 1) - 1);
|
|
720
|
+
const start = Math.max(0, lineIdx - 4);
|
|
721
|
+
const end = Math.min(fileLines.length, lineIdx + 5);
|
|
722
|
+
context = fileLines.slice(start, end).join("\n");
|
|
723
|
+
} catch {
|
|
724
|
+
context = [aiInput.finding.surroundingLines, aiInput.finding.matchLine].filter(Boolean).join("\n");
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
context = [aiInput.finding.surroundingLines, aiInput.finding.matchLine].filter(Boolean).join("\n");
|
|
728
|
+
}
|
|
688
729
|
try {
|
|
689
730
|
if (new RegExp(rejectRegex, "i").test(context)) {
|
|
690
731
|
return buildResult({
|