@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegovelasquezweb/a11y-engine",
3
- "version": "0.11.50",
3
+ "version": "0.11.52",
4
4
  "description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- return ranked;
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
- ranked = allFiles
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
- "CSS files are never in the files array. Fix visual issues (touch targets, sizing) using inline style attributes or markup changes in the HTML file — never reference or create .css files.",
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, it may be because a prior fix (e.g. the DOM
676
- // batch) already resolved this issue. Verify by checking if the pattern's
677
- // context_reject_regex now matches the surroundingLines of the target element.
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
- if (validation.reason === "AI patch output has no changes") {
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
- const context = [aiInput.finding.surroundingLines, aiInput.finding.matchLine]
686
- .filter(Boolean)
687
- .join("\n");
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({