wcag-a11y 0.3.5 → 0.4.0
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/README.md +86 -26
- package/dist/ai/anthropic.js +59 -0
- package/dist/ai/azure-openai.js +17 -0
- package/dist/ai/base.js +39 -0
- package/dist/ai/cohere.js +58 -0
- package/dist/ai/deepseek.js +6 -0
- package/dist/ai/gemini.js +8 -39
- package/dist/ai/groq.js +6 -0
- package/dist/ai/index.js +78 -14
- package/dist/ai/mistral.js +6 -0
- package/dist/ai/ollama.js +3 -23
- package/dist/ai/openai-compat.js +65 -0
- package/dist/ai/openai.js +3 -84
- package/dist/ai/perplexity.js +6 -0
- package/dist/ai/together.js +6 -0
- package/dist/ai/xai.js +6 -0
- package/dist/cli.js +14 -5
- package/dist/config.js +51 -0
- package/dist/fixer.js +76 -8
- package/package.json +20 -2
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Generating AI fixes for 7 violations...
|
|
|
56
56
|
|
|
57
57
|
The **prompt** at the end of each fix is what you copy into Cursor, Copilot, or Claude. It includes the affected selectors, the WCAG rule, and exactly what needs to change — no rewriting needed.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
The full output is saved to `a11y-report.md` by default. Pass `--no-report` to skip.
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
@@ -69,17 +69,22 @@ npm install -g wcag-a11y
|
|
|
69
69
|
## Setup
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
wcag-a11y init
|
|
73
|
-
wcag-a11y init --provider openai
|
|
74
|
-
wcag-a11y init --provider
|
|
72
|
+
wcag-a11y init # Gemini (free, default)
|
|
73
|
+
wcag-a11y init --provider openai # OpenAI
|
|
74
|
+
wcag-a11y init --provider anthropic # Anthropic Claude
|
|
75
|
+
wcag-a11y init --provider mistral # Mistral
|
|
76
|
+
wcag-a11y init --provider groq # Groq (fast inference)
|
|
77
|
+
wcag-a11y init --provider cohere # Cohere
|
|
78
|
+
wcag-a11y init --provider xai # xAI Grok
|
|
79
|
+
wcag-a11y init --provider deepseek # DeepSeek
|
|
80
|
+
wcag-a11y init --provider together # Together AI (open-source models)
|
|
81
|
+
wcag-a11y init --provider perplexity # Perplexity
|
|
82
|
+
wcag-a11y init --provider azure-openai # Azure OpenAI
|
|
83
|
+
wcag-a11y init --provider ollama # Local — no API key needed
|
|
75
84
|
```
|
|
76
85
|
|
|
77
86
|
Each command creates an `a11y.config.json` pre-wired for that provider. Fill in your API key, then scan.
|
|
78
87
|
|
|
79
|
-
**Get a free Gemini key:** https://aistudio.google.com
|
|
80
|
-
**Get an OpenAI key:** https://platform.openai.com/api-keys
|
|
81
|
-
**Ollama (local):** install from https://ollama.com, then run `ollama serve`
|
|
82
|
-
|
|
83
88
|
---
|
|
84
89
|
|
|
85
90
|
## Commands
|
|
@@ -91,13 +96,13 @@ Scan a built-in page with 10 intentional violations. No dev server or config req
|
|
|
91
96
|
```bash
|
|
92
97
|
wcag-a11y demo # violations + AI fixes (default, requires config)
|
|
93
98
|
wcag-a11y demo --no-ai # violations only, no AI — faster
|
|
94
|
-
wcag-a11y demo --report
|
|
99
|
+
wcag-a11y demo --no-report # skip saving a11y-report.md
|
|
95
100
|
```
|
|
96
101
|
|
|
97
102
|
| Flag | Description |
|
|
98
103
|
|---|---|
|
|
99
104
|
| `--no-ai` | Skip AI fix generation — prints violations only, no prompts |
|
|
100
|
-
|
|
|
105
|
+
| `--no-report` | Skip saving report to `a11y-report.md` (report is saved by default) |
|
|
101
106
|
|
|
102
107
|
---
|
|
103
108
|
|
|
@@ -113,7 +118,7 @@ wcag-a11y init --provider ollama # Ollama (local)
|
|
|
113
118
|
|
|
114
119
|
| Flag | Description |
|
|
115
120
|
|---|---|
|
|
116
|
-
| `--provider <name>` | Which provider to configure: `gemini` (default), `openai`,
|
|
121
|
+
| `--provider <name>` | Which provider to configure. Valid values: `gemini` (default), `openai`, `anthropic`, `mistral`, `groq`, `cohere`, `xai`, `deepseek`, `together`, `perplexity`, `azure-openai`, `ollama`. Determines which fields are written to the config file. |
|
|
117
122
|
|
|
118
123
|
---
|
|
119
124
|
|
|
@@ -124,8 +129,9 @@ Scan a running dev server for accessibility violations.
|
|
|
124
129
|
```bash
|
|
125
130
|
wcag-a11y scan -u http://localhost:3000
|
|
126
131
|
wcag-a11y scan -u http://localhost:3000 --pages / /about /contact
|
|
127
|
-
wcag-a11y scan -u http://localhost:3000 --crawl
|
|
132
|
+
wcag-a11y scan -u http://localhost:3000 --crawl
|
|
128
133
|
wcag-a11y scan -u http://localhost:3000 --no-ai --ci
|
|
134
|
+
wcag-a11y scan -u http://localhost:3000 --terminal --fast-mode
|
|
129
135
|
```
|
|
130
136
|
|
|
131
137
|
| Flag | Default | Description |
|
|
@@ -133,12 +139,14 @@ wcag-a11y scan -u http://localhost:3000 --no-ai --ci
|
|
|
133
139
|
| `-u, --url <url>` | required | Base URL of your running dev server |
|
|
134
140
|
| `-p, --pages <pages...>` | `/` | One or more paths to scan. Separate with spaces: `--pages / /about /contact` |
|
|
135
141
|
| `-c, --crawl` | off | Follow same-origin links and scan all reachable pages automatically |
|
|
136
|
-
|
|
|
142
|
+
| `--no-report` | on | Skip saving scan output to `a11y-report.md` (report is saved by default) |
|
|
137
143
|
| `--no-ai` | on | Skip AI fix generation — scan runs faster and prints violations only |
|
|
138
|
-
| `--no-explain` |
|
|
144
|
+
| `--no-explain` | on | Print only the ready-to-paste prompt for each fix, without the AI explanation |
|
|
145
|
+
| `--terminal` | off | Print violations summary and AI fix prompts to terminal |
|
|
146
|
+
| `--fast-mode` | off | Output only AI fix prompts — no summaries, explanations, or progress messages |
|
|
139
147
|
| `--group <strategy>` | `rule` | `rule` (default) groups all violations of the same type into one fix prompt. `none` produces a separate prompt per element. Use `none` when violations of the same rule need different fixes |
|
|
140
148
|
| `--ci` | off | Exit with code `1` if any violations are found. Use this to fail a CI pipeline |
|
|
141
|
-
| `--provider <name>` | from config | Override the AI provider for this run
|
|
149
|
+
| `--provider <name>` | from config | Override the AI provider for this run. See [AI Providers](#ai-providers) for valid names. Does not modify the config file |
|
|
142
150
|
|
|
143
151
|
---
|
|
144
152
|
|
|
@@ -158,11 +166,15 @@ wcag-a11y fix -u http://localhost:3000 --apply
|
|
|
158
166
|
|
|
159
167
|
# Auto-discover pages + write fixes
|
|
160
168
|
wcag-a11y fix -u http://localhost:3000 --crawl --apply
|
|
169
|
+
|
|
170
|
+
# Skip rescanning — load violations from an existing report
|
|
171
|
+
wcag-a11y fix --from-report
|
|
172
|
+
wcag-a11y fix --from-report ./reports/a11y-report.md --apply
|
|
161
173
|
```
|
|
162
174
|
|
|
163
175
|
**How it works:**
|
|
164
176
|
|
|
165
|
-
1. Runs the same scan as `wcag-a11y scan`
|
|
177
|
+
1. Runs the same scan as `wcag-a11y scan` (or loads an existing report with `--from-report`)
|
|
166
178
|
2. For each violation, locates the source file — checks `violation.source` (React dev mode) first, then falls back to grepping `./src` for unique identifiers in the HTML snippet (`id=`, `name=`, `for=`, local `src=`, text content)
|
|
167
179
|
3. Groups violations by file (multiple violations in the same file → one AI call)
|
|
168
180
|
4. Sends the full file content + violation list to your configured AI provider and asks for the corrected file
|
|
@@ -186,30 +198,51 @@ src/components/Navbar.jsx — 2 violation(s)
|
|
|
186
198
|
|
|
187
199
|
| Flag | Default | Description |
|
|
188
200
|
|---|---|---|
|
|
189
|
-
| `-u, --url <url>` |
|
|
201
|
+
| `-u, --url <url>` | — | Base URL of your running dev server. Required unless `--from-report` is used |
|
|
190
202
|
| `-p, --pages <pages...>` | `/` | Specific pages to scan |
|
|
191
203
|
| `-c, --crawl` | off | Auto-discover pages by following same-origin links |
|
|
204
|
+
| `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of scanning. Useful when you already ran `scan --report` and just want to apply fixes |
|
|
192
205
|
| `--apply` | off | Write patched files to disk (dry-run without this flag) |
|
|
193
|
-
| `--provider <name>` | from config | Override AI provider for this run
|
|
206
|
+
| `--provider <name>` | from config | Override AI provider for this run. See [AI Providers](#ai-providers) for valid names |
|
|
194
207
|
|
|
195
208
|
> **Tip:** Always run without `--apply` first to review the diff. The dry-run is safe — nothing is written to disk.
|
|
196
209
|
|
|
210
|
+
**Common workflow:** run `scan --report` to generate a report for review, then run `fix --from-report --apply` to patch the files — no second browser crawl needed.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
wcag-a11y scan -u http://localhost:3000 # generates a11y-report.md automatically
|
|
214
|
+
wcag-a11y fix --from-report --apply # patch files from that report
|
|
215
|
+
```
|
|
216
|
+
|
|
197
217
|
---
|
|
198
218
|
|
|
199
219
|
## AI Providers
|
|
200
220
|
|
|
201
|
-
|
|
202
|
-
|---|---|---|---|
|
|
203
|
-
| `gemini` (default) | `gemini-2.5-flash` | Free tier | [aistudio.google.com](https://aistudio.google.com) |
|
|
204
|
-
| `openai` | `gpt-4o-mini` | Pay-per-use | [platform.openai.com](https://platform.openai.com/api-keys) |
|
|
205
|
-
| `ollama` | `llama3` | Free (local) | None — run `ollama serve` |
|
|
221
|
+
12 providers are supported. Set your provider in `a11y.config.json` or override per-run with `--provider <name>`.
|
|
206
222
|
|
|
207
|
-
|
|
223
|
+
| Provider | `--provider` name | Default model | API key source |
|
|
224
|
+
|---|---|---|---|
|
|
225
|
+
| Google Gemini | `gemini` *(default)* | `gemini-2.5-flash` | [aistudio.google.com](https://aistudio.google.com) — free tier |
|
|
226
|
+
| OpenAI | `openai` | `gpt-4o-mini` | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
|
|
227
|
+
| Anthropic | `anthropic` | `claude-sonnet-4-6` | [console.anthropic.com](https://console.anthropic.com) |
|
|
228
|
+
| Mistral | `mistral` | `mistral-large-latest` | [console.mistral.ai](https://console.mistral.ai) |
|
|
229
|
+
| Groq | `groq` | `llama-3.3-70b-versatile` | [console.groq.com](https://console.groq.com) |
|
|
230
|
+
| Cohere | `cohere` | `command-r-plus` | [dashboard.cohere.com](https://dashboard.cohere.com) |
|
|
231
|
+
| xAI | `xai` | `grok-2` | [console.x.ai](https://console.x.ai) |
|
|
232
|
+
| DeepSeek | `deepseek` | `deepseek-chat` | [platform.deepseek.com](https://platform.deepseek.com) |
|
|
233
|
+
| Together AI | `together` | `meta-llama/Llama-3-70b-chat-hf` | [api.together.xyz](https://api.together.xyz) |
|
|
234
|
+
| Perplexity | `perplexity` | `llama-3.1-sonar-large-128k-online` | [perplexity.ai/settings/api](https://www.perplexity.ai/settings/api) |
|
|
235
|
+
| Azure OpenAI | `azure-openai` | *(your deployment)* | [portal.azure.com](https://portal.azure.com) |
|
|
236
|
+
| Ollama | `ollama` | `llama3` | None — run `ollama serve` locally |
|
|
237
|
+
|
|
238
|
+
All models are configurable. Change the model field in `a11y.config.json` to use any model your API key has access to. If the AI response is unparseable, the tool generates a fix prompt directly from the violation data so you always get something actionable.
|
|
208
239
|
|
|
209
240
|
---
|
|
210
241
|
|
|
211
242
|
## Config (`a11y.config.json`)
|
|
212
243
|
|
|
244
|
+
Only the fields for your active `provider` are required. This file is gitignored by default.
|
|
245
|
+
|
|
213
246
|
```json
|
|
214
247
|
{
|
|
215
248
|
"provider": "gemini",
|
|
@@ -220,13 +253,40 @@ Set your provider in `a11y.config.json` or override it per-run with `--provider`
|
|
|
220
253
|
"openaiApiKey": "YOUR_OPENAI_API_KEY",
|
|
221
254
|
"openaiModel": "gpt-4o-mini",
|
|
222
255
|
|
|
256
|
+
"anthropicApiKey": "YOUR_ANTHROPIC_API_KEY",
|
|
257
|
+
"anthropicModel": "claude-sonnet-4-6",
|
|
258
|
+
|
|
259
|
+
"mistralApiKey": "YOUR_MISTRAL_API_KEY",
|
|
260
|
+
"mistralModel": "mistral-large-latest",
|
|
261
|
+
|
|
262
|
+
"groqApiKey": "YOUR_GROQ_API_KEY",
|
|
263
|
+
"groqModel": "llama-3.3-70b-versatile",
|
|
264
|
+
|
|
265
|
+
"cohereApiKey": "YOUR_COHERE_API_KEY",
|
|
266
|
+
"cohereModel": "command-r-plus",
|
|
267
|
+
|
|
268
|
+
"xaiApiKey": "YOUR_XAI_API_KEY",
|
|
269
|
+
"xaiModel": "grok-2",
|
|
270
|
+
|
|
271
|
+
"deepseekApiKey": "YOUR_DEEPSEEK_API_KEY",
|
|
272
|
+
"deepseekModel": "deepseek-chat",
|
|
273
|
+
|
|
274
|
+
"togetherApiKey": "YOUR_TOGETHER_API_KEY",
|
|
275
|
+
"togetherModel": "meta-llama/Llama-3-70b-chat-hf",
|
|
276
|
+
|
|
277
|
+
"perplexityApiKey": "YOUR_PERPLEXITY_API_KEY",
|
|
278
|
+
"perplexityModel": "llama-3.1-sonar-large-128k-online",
|
|
279
|
+
|
|
280
|
+
"azureOpenaiApiKey": "YOUR_AZURE_KEY",
|
|
281
|
+
"azureOpenaiEndpoint": "https://YOUR_RESOURCE.openai.azure.com",
|
|
282
|
+
"azureOpenaiDeployment": "YOUR_DEPLOYMENT_NAME",
|
|
283
|
+
"azureOpenaiApiVersion": "2024-10-01-preview",
|
|
284
|
+
|
|
223
285
|
"ollamaBaseUrl": "http://localhost:11434",
|
|
224
286
|
"ollamaModel": "llama3"
|
|
225
287
|
}
|
|
226
288
|
```
|
|
227
289
|
|
|
228
|
-
Only the fields for your active provider are required. This file is gitignored by default.
|
|
229
|
-
|
|
230
290
|
---
|
|
231
291
|
|
|
232
292
|
## What it checks
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { buildPrompt } from './prompt.js';
|
|
2
|
+
import { buildPatchPrompt } from './patch-prompt.js';
|
|
3
|
+
import { groupViolations } from './group.js';
|
|
4
|
+
import { BaseAIProvider } from './base.js';
|
|
5
|
+
export class AnthropicProvider extends BaseAIProvider {
|
|
6
|
+
apiKey;
|
|
7
|
+
model;
|
|
8
|
+
constructor(apiKey, model = 'claude-sonnet-4-6') {
|
|
9
|
+
super();
|
|
10
|
+
this.apiKey = apiKey;
|
|
11
|
+
this.model = model;
|
|
12
|
+
}
|
|
13
|
+
get headers() {
|
|
14
|
+
return {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'x-api-key': this.apiKey,
|
|
17
|
+
'anthropic-version': '2023-06-01',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async generateFixes(violations, strategy = 'rule', framework) {
|
|
21
|
+
if (violations.length === 0)
|
|
22
|
+
return [];
|
|
23
|
+
const groups = groupViolations(violations, strategy);
|
|
24
|
+
const prompt = buildPrompt(groups, framework);
|
|
25
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: this.headers,
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
model: this.model,
|
|
30
|
+
max_tokens: 8192,
|
|
31
|
+
temperature: 0.2,
|
|
32
|
+
messages: [{ role: 'user', content: prompt }],
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
|
|
37
|
+
}
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
const text = data.content?.[0]?.text ?? '[]';
|
|
40
|
+
return this.parse(text, groups, framework);
|
|
41
|
+
}
|
|
42
|
+
async generateFilePatch(fileContent, violations, filePath, framework) {
|
|
43
|
+
const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
|
|
44
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: this.headers,
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
model: this.model,
|
|
49
|
+
max_tokens: 16384,
|
|
50
|
+
temperature: 0.1,
|
|
51
|
+
messages: [{ role: 'user', content: prompt }],
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok)
|
|
55
|
+
throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
return data.content?.[0]?.text ?? '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { OpenAICompatProvider } from './openai-compat.js';
|
|
2
|
+
export class AzureOpenAIProvider extends OpenAICompatProvider {
|
|
3
|
+
apiVersion;
|
|
4
|
+
constructor(endpoint, apiKey, deployment, apiVersion = '2024-10-01-preview') {
|
|
5
|
+
super(`${endpoint}/openai/deployments/${deployment}`, apiKey, deployment, 'Azure OpenAI');
|
|
6
|
+
this.apiVersion = apiVersion;
|
|
7
|
+
}
|
|
8
|
+
buildUrl(path) {
|
|
9
|
+
return `${this.baseUrl}${path}?api-version=${this.apiVersion}`;
|
|
10
|
+
}
|
|
11
|
+
buildHeaders() {
|
|
12
|
+
return {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'api-key': this.apiKey,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/ai/base.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { fallbackExplanation } from './fallback-explanation.js';
|
|
2
|
+
export class BaseAIProvider {
|
|
3
|
+
parse(text, groups, framework) {
|
|
4
|
+
try {
|
|
5
|
+
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
6
|
+
const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
|
|
7
|
+
return groups.map((g) => {
|
|
8
|
+
const found = fixes.find((f) => f.ruleId === g.ruleId);
|
|
9
|
+
return found
|
|
10
|
+
? { ...found, selectors: g.selectors, instanceCount: g.count }
|
|
11
|
+
: this.fallbackFix(g, framework);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return groups.map((g) => this.fallbackFix(g, framework));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
fallback(groups, framework) {
|
|
19
|
+
return groups.map((g) => this.fallbackFix(g, framework));
|
|
20
|
+
}
|
|
21
|
+
fallbackFix(g, framework) {
|
|
22
|
+
const v = g.representative;
|
|
23
|
+
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
24
|
+
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
25
|
+
const fwNote = framework ? `This project uses ${framework}. ` : '';
|
|
26
|
+
const prompt = g.count > 1
|
|
27
|
+
? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
|
|
28
|
+
: `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
|
|
29
|
+
return {
|
|
30
|
+
ruleId: g.ruleId,
|
|
31
|
+
selectors: g.selectors,
|
|
32
|
+
instanceCount: g.count,
|
|
33
|
+
explanation,
|
|
34
|
+
fixedCode: v.html,
|
|
35
|
+
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
36
|
+
optimalPrompt: prompt,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { buildPrompt } from './prompt.js';
|
|
2
|
+
import { buildPatchPrompt } from './patch-prompt.js';
|
|
3
|
+
import { groupViolations } from './group.js';
|
|
4
|
+
import { BaseAIProvider } from './base.js';
|
|
5
|
+
export class CohereProvider extends BaseAIProvider {
|
|
6
|
+
apiKey;
|
|
7
|
+
model;
|
|
8
|
+
constructor(apiKey, model = 'command-r-plus') {
|
|
9
|
+
super();
|
|
10
|
+
this.apiKey = apiKey;
|
|
11
|
+
this.model = model;
|
|
12
|
+
}
|
|
13
|
+
get headers() {
|
|
14
|
+
return {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async generateFixes(violations, strategy = 'rule', framework) {
|
|
20
|
+
if (violations.length === 0)
|
|
21
|
+
return [];
|
|
22
|
+
const groups = groupViolations(violations, strategy);
|
|
23
|
+
const prompt = buildPrompt(groups, framework);
|
|
24
|
+
const response = await fetch('https://api.cohere.com/v2/chat', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: this.headers,
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
model: this.model,
|
|
29
|
+
messages: [{ role: 'user', content: prompt }],
|
|
30
|
+
max_tokens: 8192,
|
|
31
|
+
temperature: 0.2,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`Cohere API error: ${response.status} ${await response.text()}`);
|
|
36
|
+
}
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
const text = data.message?.content?.[0]?.text ?? '[]';
|
|
39
|
+
return this.parse(text, groups, framework);
|
|
40
|
+
}
|
|
41
|
+
async generateFilePatch(fileContent, violations, filePath, framework) {
|
|
42
|
+
const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
|
|
43
|
+
const response = await fetch('https://api.cohere.com/v2/chat', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: this.headers,
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
model: this.model,
|
|
48
|
+
messages: [{ role: 'user', content: prompt }],
|
|
49
|
+
max_tokens: 16384,
|
|
50
|
+
temperature: 0.1,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok)
|
|
54
|
+
throw new Error(`Cohere API error: ${response.status} ${await response.text()}`);
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
return data.message?.content?.[0]?.text ?? '';
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/ai/gemini.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { buildPrompt } from './prompt.js';
|
|
2
2
|
import { buildPatchPrompt } from './patch-prompt.js';
|
|
3
3
|
import { groupViolations } from './group.js';
|
|
4
|
-
import {
|
|
5
|
-
export class GeminiProvider {
|
|
4
|
+
import { BaseAIProvider } from './base.js';
|
|
5
|
+
export class GeminiProvider extends BaseAIProvider {
|
|
6
6
|
apiKey;
|
|
7
7
|
model;
|
|
8
8
|
constructor(apiKey, model = 'gemini-2.5-flash') {
|
|
9
|
+
super();
|
|
9
10
|
this.apiKey = apiKey;
|
|
10
11
|
this.model = model;
|
|
11
12
|
}
|
|
13
|
+
buildUrl(model) {
|
|
14
|
+
return `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${this.apiKey}`;
|
|
15
|
+
}
|
|
12
16
|
async generateFixes(violations, strategy = 'rule', framework) {
|
|
13
17
|
if (violations.length === 0)
|
|
14
18
|
return [];
|
|
15
19
|
const groups = groupViolations(violations, strategy);
|
|
16
20
|
const prompt = buildPrompt(groups, framework);
|
|
17
|
-
const
|
|
18
|
-
const response = await fetch(url, {
|
|
21
|
+
const response = await fetch(this.buildUrl(this.model), {
|
|
19
22
|
method: 'POST',
|
|
20
23
|
headers: { 'Content-Type': 'application/json' },
|
|
21
24
|
body: JSON.stringify({
|
|
@@ -32,8 +35,7 @@ export class GeminiProvider {
|
|
|
32
35
|
}
|
|
33
36
|
async generateFilePatch(fileContent, violations, filePath, framework) {
|
|
34
37
|
const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
|
|
35
|
-
const
|
|
36
|
-
const response = await fetch(url, {
|
|
38
|
+
const response = await fetch(this.buildUrl(this.model), {
|
|
37
39
|
method: 'POST',
|
|
38
40
|
headers: { 'Content-Type': 'application/json' },
|
|
39
41
|
body: JSON.stringify({
|
|
@@ -46,37 +48,4 @@ export class GeminiProvider {
|
|
|
46
48
|
const data = await response.json();
|
|
47
49
|
return data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
|
|
48
50
|
}
|
|
49
|
-
parse(text, groups, framework) {
|
|
50
|
-
try {
|
|
51
|
-
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
52
|
-
const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
|
|
53
|
-
return groups.map((g) => {
|
|
54
|
-
const found = fixes.find((f) => f.ruleId === g.ruleId);
|
|
55
|
-
return found
|
|
56
|
-
? { ...found, selectors: g.selectors, instanceCount: g.count }
|
|
57
|
-
: this.fallbackFix(g, framework);
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return groups.map((g) => this.fallbackFix(g, framework));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
fallbackFix(g, framework) {
|
|
65
|
-
const v = g.representative;
|
|
66
|
-
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
67
|
-
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
68
|
-
const fwNote = framework ? `This project uses ${framework}. ` : '';
|
|
69
|
-
const prompt = g.count > 1
|
|
70
|
-
? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
|
|
71
|
-
: `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
|
|
72
|
-
return {
|
|
73
|
-
ruleId: g.ruleId,
|
|
74
|
-
selectors: g.selectors,
|
|
75
|
-
instanceCount: g.count,
|
|
76
|
-
explanation,
|
|
77
|
-
fixedCode: v.html,
|
|
78
|
-
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
79
|
-
optimalPrompt: prompt,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
51
|
}
|
package/dist/ai/groq.js
ADDED
package/dist/ai/index.js
CHANGED
|
@@ -1,20 +1,84 @@
|
|
|
1
1
|
import { GeminiProvider } from './gemini.js';
|
|
2
2
|
import { OllamaProvider } from './ollama.js';
|
|
3
3
|
import { OpenAIProvider } from './openai.js';
|
|
4
|
+
import { AnthropicProvider } from './anthropic.js';
|
|
5
|
+
import { MistralProvider } from './mistral.js';
|
|
6
|
+
import { GroqProvider } from './groq.js';
|
|
7
|
+
import { CohereProvider } from './cohere.js';
|
|
8
|
+
import { XAIProvider } from './xai.js';
|
|
9
|
+
import { DeepSeekProvider } from './deepseek.js';
|
|
10
|
+
import { TogetherProvider } from './together.js';
|
|
11
|
+
import { PerplexityProvider } from './perplexity.js';
|
|
12
|
+
import { AzureOpenAIProvider } from './azure-openai.js';
|
|
4
13
|
export function createAIProvider(config) {
|
|
5
|
-
|
|
6
|
-
|
|
14
|
+
switch (config.provider) {
|
|
15
|
+
case 'ollama':
|
|
16
|
+
return new OllamaProvider(config.ollamaBaseUrl, config.ollamaModel);
|
|
17
|
+
case 'openai':
|
|
18
|
+
if (!config.openaiApiKey) {
|
|
19
|
+
console.error('No OpenAI API key found. Add "openaiApiKey" to a11y.config.json.');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
return new OpenAIProvider(config.openaiApiKey, config.openaiModel);
|
|
23
|
+
case 'anthropic':
|
|
24
|
+
if (!config.anthropicApiKey) {
|
|
25
|
+
console.error('No Anthropic API key found. Add "anthropicApiKey" to a11y.config.json.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
return new AnthropicProvider(config.anthropicApiKey, config.anthropicModel);
|
|
29
|
+
case 'mistral':
|
|
30
|
+
if (!config.mistralApiKey) {
|
|
31
|
+
console.error('No Mistral API key found. Add "mistralApiKey" to a11y.config.json.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
return new MistralProvider(config.mistralApiKey, config.mistralModel);
|
|
35
|
+
case 'groq':
|
|
36
|
+
if (!config.groqApiKey) {
|
|
37
|
+
console.error('No Groq API key found. Add "groqApiKey" to a11y.config.json.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
return new GroqProvider(config.groqApiKey, config.groqModel);
|
|
41
|
+
case 'cohere':
|
|
42
|
+
if (!config.cohereApiKey) {
|
|
43
|
+
console.error('No Cohere API key found. Add "cohereApiKey" to a11y.config.json.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
return new CohereProvider(config.cohereApiKey, config.cohereModel);
|
|
47
|
+
case 'xai':
|
|
48
|
+
if (!config.xaiApiKey) {
|
|
49
|
+
console.error('No xAI API key found. Add "xaiApiKey" to a11y.config.json.');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
return new XAIProvider(config.xaiApiKey, config.xaiModel);
|
|
53
|
+
case 'deepseek':
|
|
54
|
+
if (!config.deepseekApiKey) {
|
|
55
|
+
console.error('No DeepSeek API key found. Add "deepseekApiKey" to a11y.config.json.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
return new DeepSeekProvider(config.deepseekApiKey, config.deepseekModel);
|
|
59
|
+
case 'together':
|
|
60
|
+
if (!config.togetherApiKey) {
|
|
61
|
+
console.error('No Together AI API key found. Add "togetherApiKey" to a11y.config.json.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return new TogetherProvider(config.togetherApiKey, config.togetherModel);
|
|
65
|
+
case 'perplexity':
|
|
66
|
+
if (!config.perplexityApiKey) {
|
|
67
|
+
console.error('No Perplexity API key found. Add "perplexityApiKey" to a11y.config.json.');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
return new PerplexityProvider(config.perplexityApiKey, config.perplexityModel);
|
|
71
|
+
case 'azure-openai':
|
|
72
|
+
if (!config.azureOpenaiApiKey || !config.azureOpenaiEndpoint || !config.azureOpenaiDeployment) {
|
|
73
|
+
console.error('Azure OpenAI requires "azureOpenaiApiKey", "azureOpenaiEndpoint", and "azureOpenaiDeployment" in a11y.config.json.');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
return new AzureOpenAIProvider(config.azureOpenaiEndpoint, config.azureOpenaiApiKey, config.azureOpenaiDeployment, config.azureOpenaiApiVersion);
|
|
77
|
+
default:
|
|
78
|
+
if (!config.apiKey) {
|
|
79
|
+
console.error('No API key found. Run: wcag-a11y init');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
return new GeminiProvider(config.apiKey, config.model);
|
|
7
83
|
}
|
|
8
|
-
if (config.provider === 'openai') {
|
|
9
|
-
if (!config.openaiApiKey) {
|
|
10
|
-
console.error('No OpenAI API key found. Add "openaiApiKey" to a11y.config.json.');
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
return new OpenAIProvider(config.openaiApiKey, config.openaiModel);
|
|
14
|
-
}
|
|
15
|
-
if (!config.apiKey) {
|
|
16
|
-
console.error('No API key found. Run: wcag-a11y init');
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
return new GeminiProvider(config.apiKey, config.model);
|
|
20
84
|
}
|
package/dist/ai/ollama.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { buildPrompt } from './prompt.js';
|
|
2
2
|
import { buildPatchPrompt } from './patch-prompt.js';
|
|
3
3
|
import { groupViolations } from './group.js';
|
|
4
|
-
import {
|
|
5
|
-
export class OllamaProvider {
|
|
4
|
+
import { BaseAIProvider } from './base.js';
|
|
5
|
+
export class OllamaProvider extends BaseAIProvider {
|
|
6
6
|
baseUrl;
|
|
7
7
|
model;
|
|
8
8
|
constructor(baseUrl = 'http://localhost:11434', model = 'llama3') {
|
|
9
|
+
super();
|
|
9
10
|
this.baseUrl = baseUrl;
|
|
10
11
|
this.model = model;
|
|
11
12
|
}
|
|
@@ -52,25 +53,4 @@ export class OllamaProvider {
|
|
|
52
53
|
const data = await response.json();
|
|
53
54
|
return data.response ?? '';
|
|
54
55
|
}
|
|
55
|
-
fallback(groups, framework) {
|
|
56
|
-
return groups.map((g) => this.fallbackFix(g, framework));
|
|
57
|
-
}
|
|
58
|
-
fallbackFix(g, framework) {
|
|
59
|
-
const v = g.representative;
|
|
60
|
-
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
61
|
-
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
62
|
-
const fwNote = framework ? `This project uses ${framework}. ` : '';
|
|
63
|
-
const prompt = g.count > 1
|
|
64
|
-
? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
|
|
65
|
-
: `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
|
|
66
|
-
return {
|
|
67
|
-
ruleId: g.ruleId,
|
|
68
|
-
selectors: g.selectors,
|
|
69
|
-
instanceCount: g.count,
|
|
70
|
-
explanation,
|
|
71
|
-
fixedCode: v.html,
|
|
72
|
-
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
73
|
-
optimalPrompt: prompt,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
56
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { buildPrompt } from './prompt.js';
|
|
2
|
+
import { buildPatchPrompt } from './patch-prompt.js';
|
|
3
|
+
import { groupViolations } from './group.js';
|
|
4
|
+
import { BaseAIProvider } from './base.js';
|
|
5
|
+
export class OpenAICompatProvider extends BaseAIProvider {
|
|
6
|
+
baseUrl;
|
|
7
|
+
apiKey;
|
|
8
|
+
model;
|
|
9
|
+
providerName;
|
|
10
|
+
constructor(baseUrl, apiKey, model, providerName = 'API') {
|
|
11
|
+
super();
|
|
12
|
+
this.baseUrl = baseUrl;
|
|
13
|
+
this.apiKey = apiKey;
|
|
14
|
+
this.model = model;
|
|
15
|
+
this.providerName = providerName;
|
|
16
|
+
}
|
|
17
|
+
buildUrl(path) {
|
|
18
|
+
return `${this.baseUrl}${path}`;
|
|
19
|
+
}
|
|
20
|
+
buildHeaders() {
|
|
21
|
+
return {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async generateFixes(violations, strategy = 'rule', framework) {
|
|
27
|
+
if (violations.length === 0)
|
|
28
|
+
return [];
|
|
29
|
+
const groups = groupViolations(violations, strategy);
|
|
30
|
+
const prompt = buildPrompt(groups, framework);
|
|
31
|
+
const response = await fetch(this.buildUrl('/chat/completions'), {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: this.buildHeaders(),
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
model: this.model,
|
|
36
|
+
messages: [{ role: 'user', content: prompt }],
|
|
37
|
+
temperature: 0.2,
|
|
38
|
+
max_tokens: 8192,
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error(`${this.providerName} API error: ${response.status} ${await response.text()}`);
|
|
43
|
+
}
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
const text = data.choices?.[0]?.message?.content ?? '[]';
|
|
46
|
+
return this.parse(text, groups, framework);
|
|
47
|
+
}
|
|
48
|
+
async generateFilePatch(fileContent, violations, filePath, framework) {
|
|
49
|
+
const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
|
|
50
|
+
const response = await fetch(this.buildUrl('/chat/completions'), {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: this.buildHeaders(),
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
model: this.model,
|
|
55
|
+
messages: [{ role: 'user', content: prompt }],
|
|
56
|
+
temperature: 0.1,
|
|
57
|
+
max_tokens: 16384,
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok)
|
|
61
|
+
throw new Error(`${this.providerName} API error: ${response.status} ${await response.text()}`);
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
return data.choices?.[0]?.message?.content ?? '';
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/ai/openai.js
CHANGED
|
@@ -1,87 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { groupViolations } from './group.js';
|
|
4
|
-
import { fallbackExplanation } from './fallback-explanation.js';
|
|
5
|
-
export class OpenAIProvider {
|
|
6
|
-
apiKey;
|
|
7
|
-
model;
|
|
1
|
+
import { OpenAICompatProvider } from './openai-compat.js';
|
|
2
|
+
export class OpenAIProvider extends OpenAICompatProvider {
|
|
8
3
|
constructor(apiKey, model = 'gpt-4o-mini') {
|
|
9
|
-
|
|
10
|
-
this.model = model;
|
|
11
|
-
}
|
|
12
|
-
async generateFixes(violations, strategy = 'rule', framework) {
|
|
13
|
-
if (violations.length === 0)
|
|
14
|
-
return [];
|
|
15
|
-
const groups = groupViolations(violations, strategy);
|
|
16
|
-
const prompt = buildPrompt(groups, framework);
|
|
17
|
-
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
18
|
-
method: 'POST',
|
|
19
|
-
headers: {
|
|
20
|
-
'Content-Type': 'application/json',
|
|
21
|
-
'Authorization': `Bearer ${this.apiKey}`,
|
|
22
|
-
},
|
|
23
|
-
body: JSON.stringify({
|
|
24
|
-
model: this.model,
|
|
25
|
-
messages: [{ role: 'user', content: prompt }],
|
|
26
|
-
temperature: 0.2,
|
|
27
|
-
max_tokens: 8192,
|
|
28
|
-
}),
|
|
29
|
-
});
|
|
30
|
-
if (!response.ok) {
|
|
31
|
-
throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
|
|
32
|
-
}
|
|
33
|
-
const data = await response.json();
|
|
34
|
-
const text = data.choices?.[0]?.message?.content ?? '[]';
|
|
35
|
-
return this.parse(text, groups, framework);
|
|
36
|
-
}
|
|
37
|
-
async generateFilePatch(fileContent, violations, filePath, framework) {
|
|
38
|
-
const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
|
|
39
|
-
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
40
|
-
method: 'POST',
|
|
41
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}` },
|
|
42
|
-
body: JSON.stringify({
|
|
43
|
-
model: this.model,
|
|
44
|
-
messages: [{ role: 'user', content: prompt }],
|
|
45
|
-
temperature: 0.1,
|
|
46
|
-
max_tokens: 16384,
|
|
47
|
-
}),
|
|
48
|
-
});
|
|
49
|
-
if (!response.ok)
|
|
50
|
-
throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
|
|
51
|
-
const data = await response.json();
|
|
52
|
-
return data.choices?.[0]?.message?.content ?? '';
|
|
53
|
-
}
|
|
54
|
-
parse(text, groups, framework) {
|
|
55
|
-
try {
|
|
56
|
-
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
57
|
-
const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
|
|
58
|
-
return groups.map((g) => {
|
|
59
|
-
const found = fixes.find((f) => f.ruleId === g.ruleId);
|
|
60
|
-
return found
|
|
61
|
-
? { ...found, selectors: g.selectors, instanceCount: g.count }
|
|
62
|
-
: this.fallbackFix(g, framework);
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return groups.map((g) => this.fallbackFix(g, framework));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
fallbackFix(g, framework) {
|
|
70
|
-
const v = g.representative;
|
|
71
|
-
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
72
|
-
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
73
|
-
const fwNote = framework ? `This project uses ${framework}. ` : '';
|
|
74
|
-
const prompt = g.count > 1
|
|
75
|
-
? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
|
|
76
|
-
: `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
|
|
77
|
-
return {
|
|
78
|
-
ruleId: g.ruleId,
|
|
79
|
-
selectors: g.selectors,
|
|
80
|
-
instanceCount: g.count,
|
|
81
|
-
explanation,
|
|
82
|
-
fixedCode: v.html,
|
|
83
|
-
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
84
|
-
optimalPrompt: prompt,
|
|
85
|
-
};
|
|
4
|
+
super('https://api.openai.com/v1', apiKey, model, 'OpenAI');
|
|
86
5
|
}
|
|
87
6
|
}
|
package/dist/ai/xai.js
ADDED
package/dist/cli.js
CHANGED
|
@@ -13,11 +13,11 @@ const program = new Command();
|
|
|
13
13
|
program
|
|
14
14
|
.name('wcag-a11y')
|
|
15
15
|
.description('WCAG 2.1/2.2 accessibility auditor with AI-powered fixes')
|
|
16
|
-
.version('0.
|
|
16
|
+
.version('0.4.0');
|
|
17
17
|
program
|
|
18
18
|
.command('init')
|
|
19
19
|
.description('Create a11y.config.json in the current directory')
|
|
20
|
-
.option('--provider <name>', 'AI provider to configure (gemini|openai|ollama)', 'gemini')
|
|
20
|
+
.option('--provider <name>', 'AI provider to configure (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)', 'gemini')
|
|
21
21
|
.action((opts) => {
|
|
22
22
|
initConfig(opts.provider);
|
|
23
23
|
});
|
|
@@ -34,7 +34,7 @@ program
|
|
|
34
34
|
.option('--fast-mode', 'Output only AI fix prompts — no summaries or explanations', false)
|
|
35
35
|
.option('--group <strategy>', 'Group violations by rule or show individually (rule|none)', 'rule')
|
|
36
36
|
.option('--ci', 'Exit with code 1 if any violations are found (for CI/CD pipelines)', false)
|
|
37
|
-
.option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama)')
|
|
37
|
+
.option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
|
|
38
38
|
.action(async (opts) => {
|
|
39
39
|
try {
|
|
40
40
|
console.log(`\nScanning ${opts.url}...`);
|
|
@@ -77,21 +77,30 @@ program
|
|
|
77
77
|
program
|
|
78
78
|
.command('fix')
|
|
79
79
|
.description('Scan for violations and apply AI fixes directly to source files')
|
|
80
|
-
.
|
|
80
|
+
.option('-u, --url <url>', 'Base URL of your dev server (e.g. http://localhost:3000)')
|
|
81
81
|
.option('-p, --pages <pages...>', 'Specific pages to scan', ['/'])
|
|
82
82
|
.option('-c, --crawl', 'Auto-discover pages by following same-origin links', false)
|
|
83
|
+
.option('--from-report [path]', 'Use an existing report instead of scanning (default: a11y-report.md)')
|
|
83
84
|
.option('--apply', 'Write fixes to source files (default: dry-run, shows diff only)', false)
|
|
84
|
-
.option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama)')
|
|
85
|
+
.option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
|
|
85
86
|
.action(async (opts) => {
|
|
87
|
+
if (!opts.url && !opts.fromReport) {
|
|
88
|
+
console.error('\nError: provide --url <url> to scan, or --from-report [path] to load an existing report.');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
86
91
|
try {
|
|
87
92
|
const config = loadConfig();
|
|
88
93
|
if (opts.provider)
|
|
89
94
|
config.provider = opts.provider;
|
|
90
95
|
const provider = createAIProvider(config);
|
|
96
|
+
const reportPath = opts.fromReport
|
|
97
|
+
? (opts.fromReport === true ? 'a11y-report.md' : opts.fromReport)
|
|
98
|
+
: undefined;
|
|
91
99
|
await runFix({
|
|
92
100
|
url: opts.url,
|
|
93
101
|
pages: opts.pages,
|
|
94
102
|
crawl: opts.crawl,
|
|
103
|
+
reportPath,
|
|
95
104
|
apply: opts.apply,
|
|
96
105
|
provider,
|
|
97
106
|
srcDir: resolve(process.cwd(), 'src'),
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,15 @@ const DEFAULTS = {
|
|
|
7
7
|
ollamaBaseUrl: 'http://localhost:11434',
|
|
8
8
|
ollamaModel: 'llama3',
|
|
9
9
|
openaiModel: 'gpt-4o-mini',
|
|
10
|
+
anthropicModel: 'claude-sonnet-4-6',
|
|
11
|
+
mistralModel: 'mistral-large-latest',
|
|
12
|
+
groqModel: 'llama-3.3-70b-versatile',
|
|
13
|
+
cohereModel: 'command-r-plus',
|
|
14
|
+
xaiModel: 'grok-2',
|
|
15
|
+
deepseekModel: 'deepseek-chat',
|
|
16
|
+
togetherModel: 'meta-llama/Llama-3-70b-chat-hf',
|
|
17
|
+
perplexityModel: 'llama-3.1-sonar-large-128k-online',
|
|
18
|
+
azureOpenaiApiVersion: '2024-10-01-preview',
|
|
10
19
|
};
|
|
11
20
|
export function loadConfig() {
|
|
12
21
|
const configPath = join(process.cwd(), CONFIG_FILE);
|
|
@@ -30,6 +39,48 @@ const STARTER_CONFIGS = {
|
|
|
30
39
|
{ provider: 'ollama', ollamaBaseUrl: 'http://localhost:11434', ollamaModel: 'llama3' },
|
|
31
40
|
`Created ${CONFIG_FILE} — run \`ollama serve\` to start the local model server`,
|
|
32
41
|
],
|
|
42
|
+
anthropic: [
|
|
43
|
+
{ provider: 'anthropic', anthropicApiKey: 'YOUR_ANTHROPIC_API_KEY', anthropicModel: 'claude-sonnet-4-6' },
|
|
44
|
+
`Created ${CONFIG_FILE} — add your Anthropic API key from https://console.anthropic.com`,
|
|
45
|
+
],
|
|
46
|
+
mistral: [
|
|
47
|
+
{ provider: 'mistral', mistralApiKey: 'YOUR_MISTRAL_API_KEY', mistralModel: 'mistral-large-latest' },
|
|
48
|
+
`Created ${CONFIG_FILE} — add your Mistral API key from https://console.mistral.ai`,
|
|
49
|
+
],
|
|
50
|
+
groq: [
|
|
51
|
+
{ provider: 'groq', groqApiKey: 'YOUR_GROQ_API_KEY', groqModel: 'llama-3.3-70b-versatile' },
|
|
52
|
+
`Created ${CONFIG_FILE} — add your Groq API key from https://console.groq.com`,
|
|
53
|
+
],
|
|
54
|
+
cohere: [
|
|
55
|
+
{ provider: 'cohere', cohereApiKey: 'YOUR_COHERE_API_KEY', cohereModel: 'command-r-plus' },
|
|
56
|
+
`Created ${CONFIG_FILE} — add your Cohere API key from https://dashboard.cohere.com`,
|
|
57
|
+
],
|
|
58
|
+
xai: [
|
|
59
|
+
{ provider: 'xai', xaiApiKey: 'YOUR_XAI_API_KEY', xaiModel: 'grok-2' },
|
|
60
|
+
`Created ${CONFIG_FILE} — add your xAI API key from https://console.x.ai`,
|
|
61
|
+
],
|
|
62
|
+
deepseek: [
|
|
63
|
+
{ provider: 'deepseek', deepseekApiKey: 'YOUR_DEEPSEEK_API_KEY', deepseekModel: 'deepseek-chat' },
|
|
64
|
+
`Created ${CONFIG_FILE} — add your DeepSeek API key from https://platform.deepseek.com`,
|
|
65
|
+
],
|
|
66
|
+
together: [
|
|
67
|
+
{ provider: 'together', togetherApiKey: 'YOUR_TOGETHER_API_KEY', togetherModel: 'meta-llama/Llama-3-70b-chat-hf' },
|
|
68
|
+
`Created ${CONFIG_FILE} — add your Together AI API key from https://api.together.xyz`,
|
|
69
|
+
],
|
|
70
|
+
perplexity: [
|
|
71
|
+
{ provider: 'perplexity', perplexityApiKey: 'YOUR_PERPLEXITY_API_KEY', perplexityModel: 'llama-3.1-sonar-large-128k-online' },
|
|
72
|
+
`Created ${CONFIG_FILE} — add your Perplexity API key from https://www.perplexity.ai/settings/api`,
|
|
73
|
+
],
|
|
74
|
+
'azure-openai': [
|
|
75
|
+
{
|
|
76
|
+
provider: 'azure-openai',
|
|
77
|
+
azureOpenaiApiKey: 'YOUR_AZURE_OPENAI_API_KEY',
|
|
78
|
+
azureOpenaiEndpoint: 'https://YOUR_RESOURCE.openai.azure.com',
|
|
79
|
+
azureOpenaiDeployment: 'YOUR_DEPLOYMENT_NAME',
|
|
80
|
+
azureOpenaiApiVersion: '2024-10-01-preview',
|
|
81
|
+
},
|
|
82
|
+
`Created ${CONFIG_FILE} — fill in your Azure OpenAI endpoint, deployment, and API key from https://portal.azure.com`,
|
|
83
|
+
],
|
|
33
84
|
};
|
|
34
85
|
export function initConfig(provider = 'gemini') {
|
|
35
86
|
const configPath = join(process.cwd(), CONFIG_FILE);
|
package/dist/fixer.js
CHANGED
|
@@ -5,15 +5,32 @@ import { crawl } from './crawler.js';
|
|
|
5
5
|
const SOURCE_EXTS = new Set(['.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.html']);
|
|
6
6
|
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', '.next', '.nuxt', 'coverage', 'public']);
|
|
7
7
|
export async function runFix(opts) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
let allViolations;
|
|
9
|
+
let framework;
|
|
10
|
+
if (opts.reportPath) {
|
|
11
|
+
const absReport = resolve(process.cwd(), opts.reportPath);
|
|
12
|
+
if (!existsSync(absReport)) {
|
|
13
|
+
throw new Error(`Report file not found: ${opts.reportPath}`);
|
|
14
|
+
}
|
|
15
|
+
console.log(`\nLoading violations from ${opts.reportPath}...`);
|
|
16
|
+
allViolations = parseReportViolations(absReport);
|
|
17
|
+
if (allViolations.length === 0) {
|
|
18
|
+
console.log(chalk.green('\nNo violations found in report.'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(`Found ${allViolations.length} violation(s) in report. Locating source files...\n`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log(`\nScanning ${opts.url}...`);
|
|
25
|
+
const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
|
|
26
|
+
framework = result.framework;
|
|
27
|
+
if (result.totalViolations === 0) {
|
|
28
|
+
console.log(chalk.green('\nNo violations found.'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(`Found ${result.totalViolations} violation(s). Locating source files...\n`);
|
|
32
|
+
allViolations = result.pages.flatMap((p) => p.violations);
|
|
14
33
|
}
|
|
15
|
-
console.log(`Found ${result.totalViolations} violation(s). Locating source files...\n`);
|
|
16
|
-
const allViolations = result.pages.flatMap((p) => p.violations);
|
|
17
34
|
// Group violations by resolved source file path
|
|
18
35
|
const fileGroups = new Map();
|
|
19
36
|
let unlocated = 0;
|
|
@@ -97,6 +114,57 @@ export async function runFix(opts) {
|
|
|
97
114
|
console.log(chalk.yellow(`${unlocated} violation(s) could not be located in source files.`));
|
|
98
115
|
}
|
|
99
116
|
}
|
|
117
|
+
function parseReportViolations(reportPath) {
|
|
118
|
+
const content = readFileSync(reportPath, 'utf8');
|
|
119
|
+
const violations = [];
|
|
120
|
+
const pageParts = content.split(/^## Page:/m).slice(1);
|
|
121
|
+
for (const part of pageParts) {
|
|
122
|
+
const pageUrl = part.split('\n')[0].trim();
|
|
123
|
+
if (part.includes('✅ No violations found'))
|
|
124
|
+
continue;
|
|
125
|
+
for (const group of part.split(/^---$/m)) {
|
|
126
|
+
const headingMatch = group.match(/^###\s+\S+\s+\[(\w+)\]\s+(.+)$/m);
|
|
127
|
+
if (!headingMatch)
|
|
128
|
+
continue;
|
|
129
|
+
const impact = headingMatch[1].toLowerCase();
|
|
130
|
+
const description = headingMatch[2].trim();
|
|
131
|
+
const ruleMatch = group.match(/\*\*Rule:\*\*\s+`([^`]+)`/);
|
|
132
|
+
if (!ruleMatch)
|
|
133
|
+
continue;
|
|
134
|
+
const ruleId = ruleMatch[1];
|
|
135
|
+
let wcag = '';
|
|
136
|
+
let level = 'A';
|
|
137
|
+
const wcagStd = group.match(/\*\*WCAG:\*\*\s+SC\s+([\d.]+)\s+\(Level\s+([A-Z]+)\)/);
|
|
138
|
+
if (wcagStd) {
|
|
139
|
+
wcag = wcagStd[1];
|
|
140
|
+
level = wcagStd[2];
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const wcagAI = group.match(/\*\*WCAG:\*\*\s+WCAG\s+[\d.]+\s+SC\s+([\d.]+)/);
|
|
144
|
+
if (wcagAI)
|
|
145
|
+
wcag = wcagAI[1];
|
|
146
|
+
}
|
|
147
|
+
const repMatch = group.match(/\*\*Representative element:\*\*\s*\n`([^`]+)`(?:\s+—\s+`([^`]+)`)?/);
|
|
148
|
+
if (!repMatch)
|
|
149
|
+
continue;
|
|
150
|
+
const selector = repMatch[1];
|
|
151
|
+
const source = repMatch[2];
|
|
152
|
+
const htmlMatch = group.match(/\*\*Representative element:\*\*[\s\S]*?```html\n([\s\S]*?)\n```/);
|
|
153
|
+
const html = htmlMatch?.[1] ?? '';
|
|
154
|
+
violations.push({ ruleId, wcag, level, impact, description, selector, html, page: pageUrl, ...(source ? { source } : {}) });
|
|
155
|
+
const alsoSection = group.match(/\*\*Also affects[^*]*\*\*([\s\S]*?)(?:\n\n\*\*|\n---|\n```|$)/);
|
|
156
|
+
if (alsoSection) {
|
|
157
|
+
for (const line of alsoSection[1].split('\n')) {
|
|
158
|
+
const m = line.match(/^-\s+`([^`]+)`(?:\s+—\s+`([^`]+)`)?/);
|
|
159
|
+
if (!m)
|
|
160
|
+
continue;
|
|
161
|
+
violations.push({ ruleId, wcag, level, impact, description, selector: m[1], html: '', page: pageUrl, ...(m[2] ? { source: m[2] } : {}) });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return violations;
|
|
167
|
+
}
|
|
100
168
|
export function findSourceFile(violation, srcDir) {
|
|
101
169
|
// Priority 1: React dev-mode source annotation
|
|
102
170
|
if (violation.source) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wcag-a11y",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes. Crawls your dev server with Playwright, runs 40+ checks, and uses AI (12 providers) to generate fix prompts or patch source files directly.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"wcag",
|
|
7
|
+
"accessibility",
|
|
8
|
+
"a11y",
|
|
9
|
+
"wcag2",
|
|
10
|
+
"wcag21",
|
|
11
|
+
"wcag22",
|
|
12
|
+
"aria",
|
|
13
|
+
"playwright",
|
|
14
|
+
"ai",
|
|
15
|
+
"audit",
|
|
16
|
+
"fix",
|
|
17
|
+
"cli",
|
|
18
|
+
"gemini",
|
|
19
|
+
"openai",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"screen-reader"
|
|
22
|
+
],
|
|
5
23
|
"type": "module",
|
|
6
24
|
"bin": {
|
|
7
25
|
"wcag-a11y": "dist/cli.js"
|