@diegovelasquezweb/a11y-engine 0.11.40 → 0.11.42

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.
Binary file
@@ -1 +1 @@
1
- export default {"patterns":[{"id":"placeholder-only-label","title":"Input uses placeholder as its only label","severity":"Critical","wcag":"WCAG 1.3.1 / 4.1.2 A","wcag_criterion":"1.3.1","wcag_level":"A","type":"structural","fix_description":"Placeholder text is not a label — it disappears on input and is not reliably announced by screen readers. Fix using one of these approaches:\n\n**Option A — Visually hidden label (preferred when no visible label exists):**\n```jsx\n<label htmlFor=\"field-id\" className=\"sr-only\">Field name</label>\n<input id=\"field-id\" placeholder=\"...\" />\n```\n\n**Option B — aria-label (acceptable for icon-only inputs like search):**\n```jsx\n<input aria-label=\"Search\" placeholder=\"Search\" />\n```\n\n**Option C — Visible label already exists nearby (wire it up):**\n```jsx\n<label htmlFor=\"field-id\">Field name</label>\n<input id=\"field-id\" placeholder=\"...\" />\n```\n\nNever rely on `placeholder` alone as the accessible name. Always verify the label is announced correctly with a screen reader.","requires_manual_verification":true,"regex":"\\bplaceholder=[\"']","globs":["**/*.tsx","**/*.jsx","**/*.html","**/*.vue","**/*.svelte","**/*.astro"],"context_reject_regex":"aria-label|aria-labelledby|for=|htmlFor=|<label","context_window":6},{"id":"mouseover-without-focus","title":"Hover handler has no keyboard focus equivalent","severity":"Serious","wcag":"WCAG 2.1.1 A","wcag_criterion":"2.1.1","wcag_level":"A","type":"structural","fix_description":"Add `onFocus`/`onBlur` (or `onFocusIn`/`onFocusOut`) handlers alongside every `onMouseOver`/`onMouseEnter` handler so keyboard users receive the same interaction.","requires_manual_verification":true,"regex":"onMouseOver=|onMouseEnter=","globs":["**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.html"],"context_reject_regex":"onFocus[^I]|onFocusIn","context_window":10},{"id":"new-window-no-warning","title":"Link opens in new tab without warning","severity":"Serious","wcag":"WCAG 3.2.2 A","wcag_criterion":"3.2.2","wcag_level":"A","type":"structural","fix_description":"Add visible text such as `(opens in new tab)` or an `aria-label` containing that warning. Always pair `target=\"_blank\"` with `rel=\"noopener noreferrer\"`.","requires_manual_verification":true,"regex":"target=[\"']_blank[\"']","globs":["**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.html","**/*.astro"],"context_reject_regex":"new.tab|new.window|opens.in|sr-only","context_window":5},{"id":"spa-route-title","title":"SPA navigation does not update document.title","severity":"Serious","wcag":"WCAG 2.4.2 A","wcag_criterion":"2.4.2","wcag_level":"A","type":"structural","fix_description":"Set `document.title` after every router navigation call to reflect the new route's page title. Screen reader users rely on the title to know when the page has changed.","requires_manual_verification":true,"regex":"router\\.push\\(|router\\.replace\\(|navigate\\(|useNavigate\\(","globs":["**/*.tsx","**/*.jsx","**/*.ts","**/*.js","**/*.vue"],"context_reject_regex":"document\\.title","context_window":20},{"id":"focus-outline-suppressed","title":"Focus outline suppressed without replacement","severity":"Serious","wcag":"WCAG 2.4.7 AA","wcag_criterion":"2.4.7","wcag_level":"AA","type":"style","fix_description":"Replace `outline: none` / `outline: 0` with a `:focus-visible` rule that renders a clearly visible custom focus indicator (e.g., a 2px solid high-contrast outline). Never suppress focus outlines globally.","requires_manual_verification":true,"regex":"outline:\\s*none|outline:\\s*0|focus:outline-none","globs":["**/*.css","**/*.scss","**/*.sass","**/*.tsx","**/*.jsx"],"context_reject_regex":":focus-visible","context_window":5},{"id":"orientation-lock","title":"Screen orientation locked programmatically","severity":"Moderate","wcag":"WCAG 1.3.4 AA","wcag_criterion":"1.3.4","wcag_level":"AA","type":"structural","fix_description":"Remove the programmatic orientation lock. If a specific orientation is essential for the content, provide an accessible alternative layout for the other orientation.","requires_manual_verification":false,"regex":"screen\\.orientation\\.lock\\(|lockOrientation\\(","globs":["**/*.ts","**/*.js","**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte"],"context_reject_regex":null,"context_window":0},{"id":"character-key-shortcut","title":"Single-character accesskey shortcut with no override mechanism","severity":"Moderate","wcag":"WCAG 2.1.4 A","wcag_criterion":"2.1.4","wcag_level":"A","type":"structural","fix_description":"Remove the `accesskey` attribute or provide a user-facing mechanism to remap or disable it. Single-character shortcuts conflict with screen reader and speech-input keystroke commands.","requires_manual_verification":false,"regex":"\\baccesskey=","globs":["**/*.html","**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.astro"],"context_reject_regex":null,"context_window":0}]};
1
+ export default {"patterns":[{"id":"placeholder-only-label","title":"Input uses placeholder as its only label","severity":"Critical","wcag":"WCAG 1.3.1 / 4.1.2 A","wcag_criterion":"1.3.1","wcag_level":"A","type":"structural","fix_description":"Placeholder text is not a label — it disappears on input and is not reliably announced by screen readers. Fix using one of these approaches:\n\n**Option A — Visually hidden label (preferred when no visible label exists):**\n```jsx\n<label htmlFor=\"field-id\" className=\"sr-only\">Field name</label>\n<input id=\"field-id\" placeholder=\"...\" />\n```\n\n**Option B — aria-label (acceptable for icon-only inputs like search):**\n```jsx\n<input aria-label=\"Search\" placeholder=\"Search\" />\n```\n\n**Option C — Visible label already exists nearby (wire it up):**\n```jsx\n<label htmlFor=\"field-id\">Field name</label>\n<input id=\"field-id\" placeholder=\"...\" />\n```\n\nNever rely on `placeholder` alone as the accessible name. Always verify the label is announced correctly with a screen reader.","requires_manual_verification":true,"regex":"\\bplaceholder=[\"']","globs":["**/*.tsx","**/*.jsx","**/*.html","**/*.vue","**/*.svelte","**/*.astro"],"context_reject_regex":"aria-label|aria-labelledby|for=|htmlFor=|<label","context_window":6},{"id":"mouseover-without-focus","title":"Hover handler has no keyboard focus equivalent","severity":"Serious","wcag":"WCAG 2.1.1 A","wcag_criterion":"2.1.1","wcag_level":"A","type":"structural","fix_description":"Add `onFocus`/`onBlur` (or `onFocusIn`/`onFocusOut`) handlers alongside every `onMouseOver`/`onMouseEnter` handler so keyboard users receive the same interaction.","requires_manual_verification":true,"regex":"onMouseOver=|onMouseEnter=","globs":["**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.html"],"context_reject_regex":"onFocus[^I]|onFocusIn","context_window":10},{"id":"new-window-no-warning","title":"Link opens in new tab without warning","severity":"Serious","wcag":"WCAG 3.2.2 A","wcag_criterion":"3.2.2","wcag_level":"A","type":"structural","fix_description":"Add visible text such as `(opens in new tab)` or an `aria-label` containing that warning. Always pair `target=\"_blank\"` with `rel=\"noopener noreferrer\"`.","requires_manual_verification":true,"regex":"target=[\"']_blank[\"']","globs":["**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.html","**/*.astro"],"context_reject_regex":"new.tab|new.window|opens.in|sr-only","context_window":5},{"id":"spa-route-title","title":"SPA navigation does not update document.title","severity":"Serious","wcag":"WCAG 2.4.2 A","wcag_criterion":"2.4.2","wcag_level":"A","type":"structural","fix_description":"Set `document.title` after every router navigation call to reflect the new route's page title. Screen reader users rely on the title to know when the page has changed.","requires_manual_verification":true,"regex":"router\\.push\\(|router\\.replace\\(|navigate\\(|useNavigate\\(","globs":["**/*.tsx","**/*.jsx","**/*.ts","**/*.js","**/*.vue"],"context_reject_regex":"document\\.title","context_window":20},{"id":"focus-outline-suppressed","title":"Focus outline suppressed without replacement","severity":"Serious","wcag":"WCAG 2.4.7 AA","wcag_criterion":"2.4.7","wcag_level":"AA","type":"style","fix_description":"Replace `outline: none` / `outline: 0` with a `:focus-visible` rule that renders a clearly visible custom focus indicator (e.g., a 2px solid high-contrast outline). Never suppress focus outlines globally.","requires_manual_verification":true,"regex":"outline:\\s*none|outline:\\s*0|focus:outline-none","globs":["**/*.css","**/*.scss","**/*.sass","**/*.tsx","**/*.jsx","**/*.html","**/*.vue","**/*.svelte","**/*.astro"],"context_reject_regex":":focus-visible","context_window":5},{"id":"orientation-lock","title":"Screen orientation locked programmatically","severity":"Moderate","wcag":"WCAG 1.3.4 AA","wcag_criterion":"1.3.4","wcag_level":"AA","type":"structural","fix_description":"Remove the programmatic orientation lock. If a specific orientation is essential for the content, provide an accessible alternative layout for the other orientation.","requires_manual_verification":false,"regex":"screen\\.orientation\\.lock\\(|lockOrientation\\(","globs":["**/*.ts","**/*.js","**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte"],"context_reject_regex":null,"context_window":0},{"id":"character-key-shortcut","title":"Single-character accesskey shortcut with no override mechanism","severity":"Moderate","wcag":"WCAG 2.1.4 A","wcag_criterion":"2.1.4","wcag_level":"A","type":"structural","fix_description":"Remove the `accesskey` attribute or provide a user-facing mechanism to remap or disable it. Single-character shortcuts conflict with screen reader and speech-input keystroke commands.","requires_manual_verification":false,"regex":"\\baccesskey=","globs":["**/*.html","**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.astro"],"context_reject_regex":null,"context_window":0}]};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegovelasquezweb/a11y-engine",
3
- "version": "0.11.40",
3
+ "version": "0.11.42",
4
4
  "description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,9 +32,6 @@
32
32
  "CHANGELOG.md",
33
33
  "LICENSE"
34
34
  ],
35
- "scripts": {
36
- "test": "vitest run"
37
- },
38
35
  "dependencies": {
39
36
  "@axe-core/playwright": "^4.11.1",
40
37
  "axe-core": "^4.11.1",
@@ -44,5 +41,7 @@
44
41
  "devDependencies": {
45
42
  "vitest": "^4.0.18"
46
43
  },
47
- "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
48
- }
44
+ "scripts": {
45
+ "test": "vitest run"
46
+ }
47
+ }
File without changes
@@ -154,14 +154,43 @@ function buildPatternAiInput({ finding, candidate, projectHints }) {
154
154
  // Extract the exact line(s) containing the pattern match so Claude has an
155
155
  // unambiguous search anchor instead of inferring it from the full file.
156
156
  const fileLines = candidate.content.split("\n");
157
- const lineNumber = typeof finding.line === "number" ? finding.line : null;
158
- const matchLine = lineNumber !== null && lineNumber >= 1 && lineNumber <= fileLines.length
159
- ? fileLines[lineNumber - 1]
157
+ const originalLine = typeof finding.line === "number" ? finding.line : null;
158
+ const rawMatch = typeof finding.match === "string" ? finding.match.trim() : "";
159
+
160
+ // rawMatch is often just the regex prefix (e.g. 'placeholder="') and is not unique
161
+ // when multiple elements match the same pattern in the same file.
162
+ // Use finding.context to build a more specific anchor so sequential fixes on the
163
+ // same file don't end up targeting an already-patched sibling element.
164
+ const contextLines = typeof finding.context === "string"
165
+ ? finding.context.split("\n").map((l) => l.trim()).filter(Boolean)
166
+ : [];
167
+ const matchPrefix = rawMatch.slice(0, 30);
168
+ const contextAnchor = contextLines.reduce((best, line) => {
169
+ if (!matchPrefix || !line.includes(matchPrefix)) return best;
170
+ return !best || line.length > best.length ? line : best;
171
+ }, null);
172
+ const anchor = contextAnchor || matchPrefix;
173
+
174
+ // The file may have been modified by a previous sequential fix (lines shifted).
175
+ // Search for the actual current line containing the anchor instead of
176
+ // relying solely on the original line number from the scan.
177
+ let effectiveLineIndex = originalLine !== null ? originalLine - 1 : -1;
178
+ if (anchor && effectiveLineIndex >= 0) {
179
+ const lineAtOriginal = (fileLines[effectiveLineIndex] || "").trim();
180
+ if (!lineAtOriginal.includes(anchor)) {
181
+ const found = fileLines.findIndex((l) => l.trim().includes(anchor));
182
+ if (found !== -1) effectiveLineIndex = found;
183
+ }
184
+ }
185
+
186
+ const lineNumber = effectiveLineIndex >= 0 ? effectiveLineIndex + 1 : originalLine;
187
+ const matchLine = effectiveLineIndex >= 0 && effectiveLineIndex < fileLines.length
188
+ ? fileLines[effectiveLineIndex]
160
189
  : "";
161
190
 
162
191
  // Widen context: include ±4 lines around the match line for multi-line elements.
163
- const contextStart = lineNumber !== null ? Math.max(0, lineNumber - 5) : 0;
164
- const contextEnd = lineNumber !== null ? Math.min(fileLines.length, lineNumber + 4) : 0;
192
+ const contextStart = effectiveLineIndex >= 0 ? Math.max(0, effectiveLineIndex - 4) : 0;
193
+ const contextEnd = effectiveLineIndex >= 0 ? Math.min(fileLines.length, effectiveLineIndex + 5) : 0;
165
194
  const surroundingLines = contextStart < contextEnd
166
195
  ? fileLines.slice(contextStart, contextEnd).join("\n")
167
196
  : (finding.context || "");
File without changes
File without changes