@wcag-audit/cli 1.0.0-alpha.11

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.
Files changed (79) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +110 -0
  3. package/package.json +73 -0
  4. package/patches/@guidepup+guidepup+0.24.1.patch +30 -0
  5. package/src/__tests__/sanity.test.js +7 -0
  6. package/src/ai-fix-json.js +321 -0
  7. package/src/audit.js +199 -0
  8. package/src/cache/route-cache.js +46 -0
  9. package/src/cache/route-cache.test.js +96 -0
  10. package/src/checkers/ai-vision.js +102 -0
  11. package/src/checkers/auth.js +111 -0
  12. package/src/checkers/axe.js +65 -0
  13. package/src/checkers/consistency.js +222 -0
  14. package/src/checkers/forms.js +149 -0
  15. package/src/checkers/interaction.js +142 -0
  16. package/src/checkers/keyboard.js +351 -0
  17. package/src/checkers/media.js +102 -0
  18. package/src/checkers/motion.js +155 -0
  19. package/src/checkers/pointer.js +128 -0
  20. package/src/checkers/screen-reader.js +522 -0
  21. package/src/checkers/util/consistency-match.js +53 -0
  22. package/src/checkers/util/consistency-match.test.js +54 -0
  23. package/src/checkers/viewport.js +214 -0
  24. package/src/cli.js +169 -0
  25. package/src/commands/ci.js +63 -0
  26. package/src/commands/ci.test.js +55 -0
  27. package/src/commands/doctor.js +105 -0
  28. package/src/commands/doctor.test.js +81 -0
  29. package/src/commands/init.js +162 -0
  30. package/src/commands/init.test.js +83 -0
  31. package/src/commands/scan.js +362 -0
  32. package/src/commands/scan.test.js +139 -0
  33. package/src/commands/watch.js +89 -0
  34. package/src/config/global.js +60 -0
  35. package/src/config/global.test.js +58 -0
  36. package/src/config/project.js +35 -0
  37. package/src/config/project.test.js +44 -0
  38. package/src/devserver/spawn.js +82 -0
  39. package/src/devserver/spawn.test.js +58 -0
  40. package/src/discovery/astro.js +86 -0
  41. package/src/discovery/astro.test.js +76 -0
  42. package/src/discovery/crawl.js +93 -0
  43. package/src/discovery/crawl.test.js +93 -0
  44. package/src/discovery/dynamic-samples.js +44 -0
  45. package/src/discovery/dynamic-samples.test.js +66 -0
  46. package/src/discovery/manual.js +38 -0
  47. package/src/discovery/manual.test.js +52 -0
  48. package/src/discovery/nextjs.js +136 -0
  49. package/src/discovery/nextjs.test.js +141 -0
  50. package/src/discovery/registry.js +80 -0
  51. package/src/discovery/registry.test.js +33 -0
  52. package/src/discovery/remix.js +82 -0
  53. package/src/discovery/remix.test.js +77 -0
  54. package/src/discovery/sitemap.js +73 -0
  55. package/src/discovery/sitemap.test.js +69 -0
  56. package/src/discovery/sveltekit.js +85 -0
  57. package/src/discovery/sveltekit.test.js +76 -0
  58. package/src/discovery/vite.js +94 -0
  59. package/src/discovery/vite.test.js +144 -0
  60. package/src/license/log-usage.js +23 -0
  61. package/src/license/log-usage.test.js +45 -0
  62. package/src/license/request-free.js +46 -0
  63. package/src/license/request-free.test.js +57 -0
  64. package/src/license/validate.js +58 -0
  65. package/src/license/validate.test.js +58 -0
  66. package/src/output/agents-md.js +58 -0
  67. package/src/output/agents-md.test.js +62 -0
  68. package/src/output/cursor-rules.js +57 -0
  69. package/src/output/cursor-rules.test.js +62 -0
  70. package/src/output/excel-project.js +263 -0
  71. package/src/output/excel-project.test.js +165 -0
  72. package/src/output/markdown.js +119 -0
  73. package/src/output/markdown.test.js +95 -0
  74. package/src/report.js +239 -0
  75. package/src/util/anthropic.js +25 -0
  76. package/src/util/llm.js +159 -0
  77. package/src/util/screenshot.js +131 -0
  78. package/src/wcag-criteria.js +256 -0
  79. package/src/wcag-manual-steps.js +114 -0
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WCAG Audit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ Note: This package is a client for the WCAG Audit SaaS. A valid license key
24
+ from https://wcagaudit.io is required to run audits. See the website for
25
+ pricing and terms.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # @wcagaudit/cli
2
+
3
+ Project-aware WCAG 2.1/2.2 auditor. Scans every route in your Next.js app, generates an Excel report AND AI-ready fix prompts your coding agent can read directly.
4
+
5
+ ## Install
6
+
7
+ No install needed — use `npx`:
8
+
9
+ ```bash
10
+ npx wcag-audit init # one-time: license key + optional AI config
11
+ cd my-nextjs-app
12
+ npx wcag-audit scan # audits every route, writes WCAG_FIXES.md + wcag-report.xlsx
13
+ ```
14
+
15
+ ## Commands
16
+
17
+ | Command | What it does |
18
+ |---|---|
19
+ | `init` | Interactive setup. Saves license + AI config to `~/.wcagauditrc` (chmod 600). |
20
+ | `scan` | Audit every route in the current project. Next.js App + Pages Router supported. |
21
+ | `scan --dry-run` | Preview credit cost only. No browser launches, no credits consumed. |
22
+ | `scan --no-ai` | Skip AI vision review (faster, no LLM cost). |
23
+ | `scan --no-cache` | Force a full scan even if cached findings are available (they're valid for 24h). |
24
+ | `ci --fail-on=<level>` | CI-optimized scan. Exit 1 when findings at `critical` (default), `serious`, `moderate`, or `minor` threshold are found. |
25
+ | `doctor` | Diagnose setup (license, AI key, framework detection, dev script). |
26
+ | `scan --url=<url>` | Crawl a deployed site instead of the local project. Combine with `--crawl-depth`. |
27
+ | `scan --routes=<file>` | Load routes from a plain text file (one per line, `#` for comments). |
28
+ | `scan --crawl-depth=<n>` | Max BFS depth when using `--url` (default 2). |
29
+ | `audit <url>` | Legacy single-URL audit. Prefer `scan` for local projects. |
30
+ | `config` | Print current config (secrets masked). |
31
+
32
+ ## Config
33
+
34
+ ### Global: `~/.wcagauditrc` (secrets, never committed)
35
+
36
+ Created by `init`. Contains license key + AI API key.
37
+
38
+ ### Project: `.wcagauditrc` (committed)
39
+
40
+ Optional. Overrides scan behavior per project:
41
+
42
+ ```json
43
+ {
44
+ "routes": "auto",
45
+ "excludePaths": ["/api/*", "/admin/*"],
46
+ "failOn": "critical",
47
+ "outputs": ["excel", "markdown"],
48
+ "devServer": {
49
+ "command": null,
50
+ "port": null,
51
+ "healthCheck": "/"
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## What Gets Generated
57
+
58
+ - **`WCAG_FIXES.md`** — AI-ready markdown with file paths, route context, WCAG criteria, code snippets, and fix hints. Hand to Cursor / Claude Code / Windsurf.
59
+ - **`wcag-report.xlsx`** — Full compliance report with embedded screenshots.
60
+
61
+ ## Supported Frameworks
62
+
63
+ - Next.js App Router and Pages Router
64
+ - Vite + React Router v6+
65
+ - SvelteKit
66
+ - Remix v2
67
+ - Astro
68
+ - Fallbacks: sitemap.xml, manual routes file, BFS crawl for deployed sites
69
+
70
+ ## Dynamic Routes
71
+
72
+ Routes like `/blog/[slug]` or `/users/:id` are skipped by default. To include them, add samples to `.wcagauditrc`:
73
+
74
+ ```json
75
+ {
76
+ "dynamicRouteSamples": {
77
+ "/blog/[slug]": ["hello-world", "another-post"],
78
+ "/users/[id]": ["1", "42"]
79
+ }
80
+ }
81
+ ```
82
+
83
+ Each sample value generates one concrete route.
84
+
85
+ ## Credits
86
+
87
+ - Pro / Business: 1 credit per route audited.
88
+ - Enterprise: postpaid monthly invoice.
89
+ - AI vision review is BYOK (your own API key, your own LLM spend).
90
+
91
+ ## Cursor / Claude Code Integration
92
+
93
+ Set `outputs` in `.wcagauditrc` to include one or both of:
94
+
95
+ - `"cursor-rules"` — writes `.cursor/rules/wcag-fixes.mdc`. Cursor auto-attaches the rule as context when you edit matching files.
96
+ - `"agents-md"` — upserts a `<!-- wcag-audit:start -->` / `<!-- wcag-audit:end -->` section in your root `AGENTS.md`. Safe to re-run; it replaces the section in place without disturbing surrounding content.
97
+
98
+ Example:
99
+
100
+ ```json
101
+ {
102
+ "outputs": ["markdown", "cursor-rules", "agents-md"]
103
+ }
104
+ ```
105
+
106
+ ## Incremental Scans (Cache)
107
+
108
+ Scan results are cached per route for 24h in `.wcag-audit/cache/`. Re-running `scan` within that window skips unchanged routes at zero credit cost. Gitignore `.wcag-audit/` to keep the cache local.
109
+
110
+ Pass `--no-cache` to force a fresh scan.
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@wcag-audit/cli",
3
+ "version": "1.0.0-alpha.11",
4
+ "description": "Project-aware WCAG 2.1/2.2 auditor with AI-ready fix prompts for vibe-coding tools (Cursor, Claude Code, Windsurf).",
5
+ "type": "module",
6
+ "bin": {
7
+ "wcag-audit": "src/cli.js"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "patches",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": [
16
+ "wcag",
17
+ "accessibility",
18
+ "a11y",
19
+ "audit",
20
+ "cli",
21
+ "cursor",
22
+ "claude-code",
23
+ "nextjs",
24
+ "vite",
25
+ "sveltekit",
26
+ "remix",
27
+ "astro"
28
+ ],
29
+ "homepage": "https://wcagaudit.io",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/wcagauditdev-commits/wcag-documentation-creator.git",
33
+ "directory": "cli"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/wcagauditdev-commits/wcag-documentation-creator/issues"
37
+ },
38
+ "license": "MIT",
39
+ "scripts": {
40
+ "audit": "node src/cli.js",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "postinstall": "playwright install chromium && patch-package"
44
+ },
45
+ "dependencies": {
46
+ "@anthropic-ai/sdk": "^0.32.1",
47
+ "@axe-core/playwright": "^4.10.1",
48
+ "@guidepup/guidepup": "^0.24.1",
49
+ "@guidepup/playwright": "^0.15.0",
50
+ "axe-core": "^4.10.2",
51
+ "commander": "^12.1.0",
52
+ "enquirer": "^2.4.1",
53
+ "exceljs": "^4.4.0",
54
+ "execa": "^9.5.2",
55
+ "fast-xml-parser": "^4.5.0",
56
+ "get-port": "^7.1.0",
57
+ "p-limit": "^6.1.0",
58
+ "patch-package": "^8.0.1",
59
+ "pixelmatch": "^6.0.0",
60
+ "playwright": "^1.49.0",
61
+ "pngjs": "^7.0.0"
62
+ },
63
+ "devDependencies": {
64
+ "vite": "^8.0.8",
65
+ "vitest": "^2.1.8"
66
+ },
67
+ "engines": {
68
+ "node": ">=20"
69
+ },
70
+ "publishConfig": {
71
+ "access": "public"
72
+ }
73
+ }
@@ -0,0 +1,30 @@
1
+ diff --git a/node_modules/@guidepup/guidepup/lib/macOS/VoiceOver/supportsAppleScriptControl/enabledDefaults.js b/node_modules/@guidepup/guidepup/lib/macOS/VoiceOver/supportsAppleScriptControl/enabledDefaults.js
2
+ index 19cbf41..af60107 100644
3
+ --- a/node_modules/@guidepup/guidepup/lib/macOS/VoiceOver/supportsAppleScriptControl/enabledDefaults.js
4
+ +++ b/node_modules/@guidepup/guidepup/lib/macOS/VoiceOver/supportsAppleScriptControl/enabledDefaults.js
5
+ @@ -5,13 +5,21 @@ const child_process_1 = require("child_process");
6
+ const VOICE_OVER_APPLESCRIPT_ENABLED_DEFAULTS = "defaults read com.apple.VoiceOver4/default SCREnableAppleScript";
7
+ async function enabledDefaults() {
8
+ return await new Promise((resolve) => {
9
+ - (0, child_process_1.exec)(VOICE_OVER_APPLESCRIPT_ENABLED_DEFAULTS, (err, stdout) => {
10
+ + (0, child_process_1.exec)(VOICE_OVER_APPLESCRIPT_ENABLED_DEFAULTS, (err, stdout, stderr) => {
11
+ + // Sequoia-and-later patch: macOS 15 stopped writing the
12
+ + // SCREnableAppleScript key into com.apple.VoiceOver4/default.
13
+ + // If the key simply doesn't exist, treat it as a soft pass and
14
+ + // let the .VoiceOverAppleScriptEnabled DB file be authoritative.
15
+ if (err) {
16
+ + const msg = ((stderr || "") + (err.message || "")).toString();
17
+ + if (/does not exist/i.test(msg)) {
18
+ + resolve(true);
19
+ + return;
20
+ + }
21
+ resolve(false);
22
+ + return;
23
+ }
24
+ - else {
25
+ - resolve(stdout.trim() === "1");
26
+ - }
27
+ + resolve(stdout.trim() === "1");
28
+ });
29
+ });
30
+ }
@@ -0,0 +1,7 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ describe("sanity", () => {
4
+ it("runs vitest", () => {
5
+ expect(1 + 1).toBe(2);
6
+ });
7
+ });
@@ -0,0 +1,321 @@
1
+ // Generates an AI-fixable JSON report from audit findings.
2
+ // Each issue includes a specific prompt that an AI coding agent can use
3
+ // to fix the violation in the source code.
4
+ //
5
+ // Output format:
6
+ // {
7
+ // meta: { url, timestamp, wcagVersion, levels, totalIssues },
8
+ // issues: [
9
+ // {
10
+ // issueNumber, ruleId, criteria, level, severity, source,
11
+ // description, selector, evidence, helpUrl,
12
+ // fixPrompt: "...", // Natural language instruction for AI
13
+ // fixContext: { ... }, // Structured context for the fix
14
+ // }
15
+ // ]
16
+ // }
17
+
18
+ import fs from "node:fs/promises";
19
+ import path from "node:path";
20
+
21
+ // ─── Per-criterion fix prompt templates ─────────────────────────────
22
+ // Maps WCAG criteria to specific, actionable fix instructions.
23
+ // Each returns a function that takes the finding and returns a prompt.
24
+
25
+ const CRITERION_FIX_TEMPLATES = {
26
+ // 1.1.1 Non-text Content
27
+ "1.1.1": (f) =>
28
+ `Add descriptive alt text to the image/non-text element at "${f.selector}". ` +
29
+ `The alt text should convey the same information as the visual content. ` +
30
+ `If the image is decorative, use alt="" or aria-hidden="true". ` +
31
+ `If it's a complex image (chart/diagram), provide a longer description via aria-describedby.`,
32
+
33
+ // 1.2.1 Audio/Video-only
34
+ "1.2.1": (f) =>
35
+ `Provide a text transcript for the audio/video-only content at "${f.selector}". ` +
36
+ `Add a <track> element with kind="captions" for video, or a visible text transcript link nearby.`,
37
+
38
+ // 1.2.2 Captions
39
+ "1.2.2": (f) =>
40
+ `Add captions to the video at "${f.selector}". ` +
41
+ `Include a <track kind="captions" src="captions.vtt" srclang="en" label="English"> element inside the <video> tag.`,
42
+
43
+ // 1.3.1 Info and Relationships
44
+ "1.3.1": (f) =>
45
+ `Fix the semantic structure at "${f.selector}". ` +
46
+ `Ensure visual relationships are expressed in markup: use proper heading levels (h1-h6), ` +
47
+ `<table> with <th>/<td> for tabular data, <fieldset>/<legend> for form groups, ` +
48
+ `and <ul>/<ol>/<li> for lists. Don't use visual-only styling to convey meaning.`,
49
+
50
+ // 1.3.2 Meaningful Sequence
51
+ "1.3.2": (f) =>
52
+ `Fix the reading order at "${f.selector}". ` +
53
+ `Remove positive tabindex values (use tabindex="0" or no tabindex instead). ` +
54
+ `Ensure the DOM order matches the visual reading order. ` +
55
+ `Use CSS flexbox/grid order only when it doesn't change the logical meaning.`,
56
+
57
+ // 1.3.4 Orientation
58
+ "1.3.4": (f) =>
59
+ `Remove the CSS orientation lock at "${f.selector}". ` +
60
+ `Delete any @media (orientation: portrait/landscape) rules that hide or restrict content. ` +
61
+ `The page must be usable in both portrait and landscape modes.`,
62
+
63
+ // 1.3.5 Identify Input Purpose
64
+ "1.3.5": (f) =>
65
+ `Add the correct autocomplete attribute to the input at "${f.selector}". ` +
66
+ `Use standard values like autocomplete="name", "email", "tel", "street-address", etc. ` +
67
+ `This helps browsers and assistive technologies auto-fill user information.`,
68
+
69
+ // 1.4.1 Use of Color
70
+ "1.4.1": (f) =>
71
+ `Don't use color alone to convey information at "${f.selector}". ` +
72
+ `Add a secondary visual indicator: an icon, underline, pattern, or text label. ` +
73
+ `For links in text blocks, add an underline or other non-color distinction.`,
74
+
75
+ // 1.4.3 Contrast (Minimum)
76
+ "1.4.3": (f) => {
77
+ const ratioMatch = f.failureSummary?.match(/(\d+\.?\d*:\d+)/);
78
+ const ratio = ratioMatch ? ratioMatch[1] : "unknown";
79
+ return (
80
+ `Fix the color contrast at "${f.selector}". ` +
81
+ `Current contrast ratio is ${ratio} — the minimum required is 4.5:1 for normal text and 3:1 for large text (18px+ or 14px+ bold). ` +
82
+ `Darken the text color or lighten the background. Use a contrast checker to verify.`
83
+ );
84
+ },
85
+
86
+ // 1.4.4 Resize Text
87
+ "1.4.4": (f) =>
88
+ `Fix text resizing at "${f.selector}". ` +
89
+ `Remove any user-scalable=no or maximum-scale restrictions from the viewport meta tag. ` +
90
+ `Use relative units (rem, em, %) instead of px for font sizes. ` +
91
+ `Content must be readable when text is resized to 200%.`,
92
+
93
+ // 1.4.5 Images of Text
94
+ "1.4.5": (f) =>
95
+ `Replace the image of text at "${f.selector}" with actual styled HTML text. ` +
96
+ `Use CSS for visual presentation (font-family, font-size, color, etc.). ` +
97
+ `Images of text are only acceptable for logotypes.`,
98
+
99
+ // 1.4.10 Reflow
100
+ "1.4.10": (f) =>
101
+ `Fix content reflow at "${f.selector}". ` +
102
+ `At 320px viewport width (or 400% zoom), content must not require horizontal scrolling. ` +
103
+ `Use responsive CSS (flexbox, grid, media queries). Avoid fixed-width containers.`,
104
+
105
+ // 1.4.11 Non-text Contrast
106
+ "1.4.11": (f) =>
107
+ `Increase the contrast of the UI component or graphic at "${f.selector}". ` +
108
+ `Non-text elements (borders, icons, focus indicators, chart segments) need at least 3:1 contrast ratio against adjacent colors.`,
109
+
110
+ // 1.4.12 Text Spacing
111
+ "1.4.12": (f) =>
112
+ `Fix text spacing handling at "${f.selector}". ` +
113
+ `Content must not be clipped or overlap when users override: ` +
114
+ `line-height to 1.5×, letter-spacing to 0.12em, word-spacing to 0.16em, paragraph spacing to 2×. ` +
115
+ `Avoid fixed heights on text containers.`,
116
+
117
+ // 1.4.13 Content on Hover or Focus
118
+ "1.4.13": (f) =>
119
+ `Fix the hover/focus content at "${f.selector}". ` +
120
+ `Tooltip or popup content must: (1) be dismissible via Escape without moving focus, ` +
121
+ `(2) remain visible while hovering over it, and (3) persist until dismissed or no longer relevant.`,
122
+
123
+ // 2.1.1 Keyboard
124
+ "2.1.1": (f) =>
125
+ `Make the element at "${f.selector}" keyboard accessible. ` +
126
+ `If it's a custom interactive control, add tabindex="0" and handle keydown events for Enter and Space. ` +
127
+ `If using onClick on a div/span, change it to a <button> or <a>. ` +
128
+ `Ensure all functionality available via mouse is also available via keyboard.`,
129
+
130
+ // 2.1.2 No Keyboard Trap
131
+ "2.1.2": (f) =>
132
+ `Fix the keyboard trap at "${f.selector}". ` +
133
+ `Users must be able to Tab away from any focusable element. ` +
134
+ `For modals, implement a focus trap that allows Escape to close the modal and restore focus. ` +
135
+ `Check for tabindex="-1" on wrapper elements that might steal focus.`,
136
+
137
+ // 2.4.1 Bypass Blocks
138
+ "2.4.1": (f) =>
139
+ `Add a skip navigation link. Insert a visually-hidden-until-focused link as the first element in <body>: ` +
140
+ `<a href="#main-content" class="sr-only focus:not-sr-only">Skip to main content</a>. ` +
141
+ `Add id="main-content" to the <main> element. Also add ARIA landmark roles (nav, main, complementary).`,
142
+
143
+ // 2.4.2 Page Titled
144
+ "2.4.2": (f) =>
145
+ `Add a descriptive <title> to this page. ` +
146
+ `The title should be unique and describe the page's topic or purpose. ` +
147
+ `Format: "Page Name — Site Name" or "Page Name | Site Name".`,
148
+
149
+ // 2.4.3 Focus Order
150
+ "2.4.3": (f) =>
151
+ `Fix the focus order at "${f.selector}". ` +
152
+ `Remove positive tabindex values (tabindex="2", etc.) — use tabindex="0" for natural order. ` +
153
+ `Rearrange DOM order to match the visual layout. ` +
154
+ `Focus should follow a logical, predictable sequence.`,
155
+
156
+ // 2.4.4 Link Purpose
157
+ "2.4.4": (f) =>
158
+ `Fix the link at "${f.selector}" — it needs a descriptive accessible name. ` +
159
+ `Add meaningful link text (not "click here" or "read more"). ` +
160
+ `For icon-only links, add aria-label="Description of destination". ` +
161
+ `For image-only links, add alt text to the image.`,
162
+
163
+ // 2.4.7 Focus Visible
164
+ "2.4.7": (f) =>
165
+ `Add a visible focus indicator to "${f.selector}". ` +
166
+ `Don't use outline: none without providing an alternative focus style. ` +
167
+ `Use CSS: :focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; }. ` +
168
+ `The focus indicator must have at least 3:1 contrast.`,
169
+
170
+ // 2.5.3 Label in Name
171
+ "2.5.3": (f) =>
172
+ `Fix the accessible name at "${f.selector}". ` +
173
+ `The accessible name (aria-label or text content) must contain the visible label text. ` +
174
+ `If the button shows "Submit", don't set aria-label="Send form" — use aria-label="Submit form" instead.`,
175
+
176
+ // 2.5.8 Target Size (Minimum)
177
+ "2.5.8": (f) =>
178
+ `Increase the touch target size at "${f.selector}". ` +
179
+ `Interactive elements must be at least 24×24 CSS pixels. ` +
180
+ `Add padding, min-width/min-height, or use the CSS target-size property.`,
181
+
182
+ // 3.1.1 Language of Page
183
+ "3.1.1": (f) =>
184
+ `Add the lang attribute to the <html> element: <html lang="en">. ` +
185
+ `Use the correct ISO 639-1 language code for the page's primary language.`,
186
+
187
+ // 3.1.2 Language of Parts
188
+ "3.1.2": (f) =>
189
+ `Add lang attribute to the element at "${f.selector}" that contains text in a different language. ` +
190
+ `Example: <span lang="fr">Bonjour</span>. Use ISO 639-1 codes.`,
191
+
192
+ // 3.3.1 Error Identification
193
+ "3.3.1": (f) =>
194
+ `Add error identification to the form at "${f.selector}". ` +
195
+ `When validation fails: (1) describe the error in text near the field, ` +
196
+ `(2) set aria-invalid="true" on the field, ` +
197
+ `(3) use aria-describedby to associate error text, ` +
198
+ `(4) announce errors via aria-live="polite" region.`,
199
+
200
+ // 3.3.2 Labels or Instructions
201
+ "3.3.2": (f) =>
202
+ `Add a label to the form field at "${f.selector}". ` +
203
+ `Use <label for="field-id">Label text</label> associated via matching id. ` +
204
+ `Or use aria-label or aria-labelledby for custom controls.`,
205
+
206
+ // 4.1.1 Parsing (removed in 2.2 but still flagged)
207
+ "4.1.1": (f) =>
208
+ `Fix duplicate IDs at "${f.selector}". ` +
209
+ `Every id attribute must be unique within the document. ` +
210
+ `Search for duplicate id values and rename them.`,
211
+
212
+ // 4.1.2 Name, Role, Value
213
+ "4.1.2": (f) =>
214
+ `Fix the accessible name, role, or state at "${f.selector}". ` +
215
+ `Custom controls need: (1) an accessible name via aria-label or aria-labelledby, ` +
216
+ `(2) an appropriate role (button, link, checkbox, etc.), ` +
217
+ `(3) correct state attributes (aria-expanded, aria-checked, aria-selected). ` +
218
+ `Or use native HTML elements which have these built-in.`,
219
+ };
220
+
221
+ // ─── Generic fix prompt for criteria without specific templates ─────
222
+ function genericFixPrompt(f) {
223
+ const parts = [
224
+ `Fix WCAG ${f.criteria} (${f.level}) violation at "${f.selector}".`,
225
+ ];
226
+ if (f.description) parts.push(`Issue: ${f.description}`);
227
+ if (f.help) parts.push(`Suggested fix: ${f.help}`);
228
+ if (f.helpUrl) parts.push(`Reference: ${f.helpUrl}`);
229
+ return parts.join("\n");
230
+ }
231
+
232
+ // ─── Build the fix prompt for a single finding ──────────────────────
233
+ function buildFixPrompt(f) {
234
+ // Try each criterion in the comma-separated list
235
+ const criteria = String(f.criteria || "")
236
+ .split(",")
237
+ .map((s) => s.trim());
238
+ for (const c of criteria) {
239
+ if (CRITERION_FIX_TEMPLATES[c]) {
240
+ return CRITERION_FIX_TEMPLATES[c](f);
241
+ }
242
+ }
243
+ return genericFixPrompt(f);
244
+ }
245
+
246
+ // ─── Build structured fix context ───────────────────────────────────
247
+ function buildFixContext(f) {
248
+ return {
249
+ element: f.selector || null,
250
+ htmlSnippet: (f.evidence || f.htmlSnippet || "").slice(0, 1000) || null,
251
+ failureSummary: f.failureSummary || null,
252
+ wcagCriterion: f.criteria || null,
253
+ wcagLevel: f.level || null,
254
+ severity: f.impact || null,
255
+ source: f.source || null,
256
+ ruleId: f.ruleId || null,
257
+ referenceUrl: f.helpUrl || null,
258
+ };
259
+ }
260
+
261
+ // ─── Generate the AI fix JSON ───────────────────────────────────────
262
+ export function generateAiFixJson({ findings, meta }) {
263
+ const issues = findings.map((f) => ({
264
+ issueNumber: f.issueNumber,
265
+ ruleId: f.ruleId,
266
+ criteria: f.criteria,
267
+ level: f.level,
268
+ severity: f.impact,
269
+ source: f.source,
270
+ description: f.description,
271
+ selector: f.selector || "",
272
+ evidence: (f.evidence || f.htmlSnippet || "").slice(0, 1000),
273
+ helpUrl: f.helpUrl || "",
274
+ fixPrompt: buildFixPrompt(f),
275
+ fixContext: buildFixContext(f),
276
+ }));
277
+
278
+ return {
279
+ $schema: "https://wcagaudit.io/schemas/ai-fix-v1.json",
280
+ version: "1.0.0",
281
+ generatedAt: new Date().toISOString(),
282
+ meta: {
283
+ url: meta.url,
284
+ timestamp: meta.startedAt || meta.timestamp || new Date().toISOString(),
285
+ wcagVersion: meta.wcagVersion || meta.version || "2.2",
286
+ levels: meta.levels || [],
287
+ totalIssues: findings.length,
288
+ bySeverity: {
289
+ critical: findings.filter((f) => f.impact === "critical").length,
290
+ serious: findings.filter((f) => f.impact === "serious").length,
291
+ moderate: findings.filter((f) => f.impact === "moderate").length,
292
+ minor: findings.filter((f) => f.impact === "minor").length,
293
+ },
294
+ bySource: {
295
+ axe: findings.filter((f) => f.source === "axe").length,
296
+ playwright: findings.filter(
297
+ (f) => f.source === "playwright" || f.source === "playwright-eq"
298
+ ).length,
299
+ ai: findings.filter((f) => f.source === "ai").length,
300
+ cloud: findings.filter((f) =>
301
+ String(f.source || "").startsWith("cloud")
302
+ ).length,
303
+ },
304
+ },
305
+ systemPrompt:
306
+ "You are an accessibility remediation expert. You will receive a list of WCAG violations " +
307
+ "found on a web page. For each issue, use the fixPrompt and fixContext to generate " +
308
+ "the exact code changes needed to fix the violation. Output your fixes as a JSON array " +
309
+ "of objects with: { issueNumber, filePath (if known), originalCode, fixedCode, explanation }. " +
310
+ "Prioritize critical and serious issues first. Always use semantic HTML over ARIA when possible.",
311
+ issues,
312
+ };
313
+ }
314
+
315
+ // ─── Write to disk ──────────────────────────────────────────────────
316
+ export async function writeAiFixJson({ findings, meta, outPath }) {
317
+ const json = generateAiFixJson({ findings, meta });
318
+ await fs.mkdir(path.dirname(path.resolve(outPath)), { recursive: true });
319
+ await fs.writeFile(outPath, JSON.stringify(json, null, 2), "utf-8");
320
+ return json;
321
+ }