@diegovelasquezweb/a11y-engine 0.11.31 → 0.11.33

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.31",
3
+ "version": "0.11.33",
4
4
  "description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -90,7 +90,7 @@ function getFindings(input) {
90
90
 
91
91
  function getIntelligenceForRule(ruleId) {
92
92
  const rules = ASSETS.remediation.intelligence?.rules || {};
93
- return isObject(rules[ruleId]) ? rules[ruleId] : {};
93
+ return isObject(rules[ruleId]) ? rules[ruleId] : isObject(rules[`cdp-${ruleId}`]) ? rules[`cdp-${ruleId}`] : {};
94
94
  }
95
95
 
96
96
  function listFilesRecursive(dir) {
@@ -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 files = listFilesRecursive(projectDir);
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
- const ranked = files
174
- .map((abs) => {
175
- const content = fs.readFileSync(abs, "utf8");
176
- const rel = path.relative(projectDir, abs);
177
- return { abs, rel, content, score: scoreFile(rel, content, tokens) };
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);
@@ -183,7 +224,11 @@ function getCandidateFiles(projectDir, finding) {
183
224
  }
184
225
 
185
226
  function buildExecution(ruleId, intelligenceRule, finding) {
186
- const ruleVerify = finding.rule_id || ruleId || "";
227
+ const raw = finding.rule_id || ruleId || "";
228
+ const axeEquivalent = intelligenceRule.related_rules?.find((r) =>
229
+ r.reason?.toLowerCase().includes("axe"),
230
+ )?.id;
231
+ const ruleVerify = axeEquivalent || raw;
187
232
  const route = normalizeRoute(finding.area);
188
233
  return {
189
234
  strategy: "ai-dom-patch",
@@ -212,11 +257,19 @@ function groupFindingsByFile(domFindings, projectDir) {
212
257
 
213
258
  for (const finding of domFindings) {
214
259
  const tokens = selectorTokens(finding.selector);
215
- const ranked = allFiles
216
- .map((f) => ({ ...f, score: scoreFile(f.rel, f.content, tokens) }))
217
- .filter((f) => f.score > 0)
218
- .sort((a, b) => b.score - a.score)
219
- .slice(0, MAX_CANDIDATE_FILES);
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
+ }
220
273
 
221
274
  const key = ranked.length > 0 ? ranked[0].rel : `__no_candidates_${finding.id}`;
222
275
  if (!initialGroups.has(key)) {