@diegovelasquezweb/a11y-engine 0.11.39 → 0.11.41
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.
|
@@ -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
|
@@ -154,14 +154,29 @@ 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
|
|
158
|
-
const
|
|
159
|
-
|
|
157
|
+
const originalLine = typeof finding.line === "number" ? finding.line : null;
|
|
158
|
+
const rawMatch = typeof finding.match === "string" ? finding.match.trim() : "";
|
|
159
|
+
|
|
160
|
+
// The file may have been modified by a previous sequential fix (lines shifted).
|
|
161
|
+
// Search for the actual current line containing the match string instead of
|
|
162
|
+
// relying solely on the original line number from the scan.
|
|
163
|
+
let effectiveLineIndex = originalLine !== null ? originalLine - 1 : -1;
|
|
164
|
+
if (rawMatch && effectiveLineIndex >= 0) {
|
|
165
|
+
const lineAtOriginal = (fileLines[effectiveLineIndex] || "").trim();
|
|
166
|
+
if (!lineAtOriginal.includes(rawMatch.slice(0, 30))) {
|
|
167
|
+
const found = fileLines.findIndex((l) => l.trim().includes(rawMatch.slice(0, 30)));
|
|
168
|
+
if (found !== -1) effectiveLineIndex = found;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const lineNumber = effectiveLineIndex >= 0 ? effectiveLineIndex + 1 : originalLine;
|
|
173
|
+
const matchLine = effectiveLineIndex >= 0 && effectiveLineIndex < fileLines.length
|
|
174
|
+
? fileLines[effectiveLineIndex]
|
|
160
175
|
: "";
|
|
161
176
|
|
|
162
177
|
// Widen context: include ±4 lines around the match line for multi-line elements.
|
|
163
|
-
const contextStart =
|
|
164
|
-
const contextEnd =
|
|
178
|
+
const contextStart = effectiveLineIndex >= 0 ? Math.max(0, effectiveLineIndex - 4) : 0;
|
|
179
|
+
const contextEnd = effectiveLineIndex >= 0 ? Math.min(fileLines.length, effectiveLineIndex + 5) : 0;
|
|
165
180
|
const surroundingLines = contextStart < contextEnd
|
|
166
181
|
? fileLines.slice(contextStart, contextEnd).join("\n")
|
|
167
182
|
: (finding.context || "");
|