wcag-a11y 0.2.0 → 0.3.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 CHANGED
@@ -87,14 +87,14 @@ Each command creates an `a11y.config.json` pre-wired for that provider. Fill in
87
87
  Scan a built-in page with 10 intentional violations. No dev server or config required — useful for trying the tool before pointing it at your own project.
88
88
 
89
89
  ```bash
90
- wcag-a11y demo # violations only
91
- wcag-a11y demo --ai # violations + AI fixes (requires config)
92
- wcag-a11y demo --ai --report # + save a11y-report.md
90
+ wcag-a11y demo # violations + AI fixes (default, requires config)
91
+ wcag-a11y demo --no-ai # violations only, no AI faster
92
+ wcag-a11y demo --report # + save a11y-report.md
93
93
  ```
94
94
 
95
95
  | Flag | Description |
96
96
  |---|---|
97
- | `--ai` | Generate AI fix explanations and prompts for each violation |
97
+ | `--no-ai` | Skip AI fix generation prints violations only, no prompts |
98
98
  | `-r, --report` | Save the full report to `a11y-report.md` in the current directory |
99
99
 
100
100
  ---
@@ -122,7 +122,7 @@ Scan a running dev server for accessibility violations.
122
122
  ```bash
123
123
  wcag-a11y scan -u http://localhost:3000
124
124
  wcag-a11y scan -u http://localhost:3000 --pages / /about /contact
125
- wcag-a11y scan -u http://localhost:3000 --crawl --ai --report
125
+ wcag-a11y scan -u http://localhost:3000 --crawl --report
126
126
  wcag-a11y scan -u http://localhost:3000 --no-ai --ci
127
127
  ```
128
128
 
@@ -132,7 +132,7 @@ wcag-a11y scan -u http://localhost:3000 --no-ai --ci
132
132
  | `-p, --pages <pages...>` | `/` | One or more paths to scan. Separate with spaces: `--pages / /about /contact` |
133
133
  | `-c, --crawl` | off | Follow same-origin links and scan all reachable pages automatically |
134
134
  | `-r, --report` | off | Save the full scan output to `a11y-report.md` |
135
- | `--ai` / `--no-ai` | on | Generate AI fix explanations and prompts. Use `--no-ai` for a fast violation-only scan |
135
+ | `--no-ai` | on | Skip AI fix generation scan runs faster and prints violations only |
136
136
  | `--no-explain` | off | Print only the ready-to-paste prompt for each fix, without the AI explanation |
137
137
  | `--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 |
138
138
  | `--ci` | off | Exit with code `1` if any violations are found. Use this to fail a CI pipeline |
package/dist/ai/gemini.js CHANGED
@@ -45,15 +45,19 @@ export class GeminiProvider {
45
45
  }
46
46
  fallbackFix(g) {
47
47
  const v = g.representative;
48
- const instanceNote = g.count > 1 ? ` There are ${g.count} similar instances at: ${g.selectors.join(', ')}.` : '';
48
+ const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
49
+ const explanation = `Users relying on assistive technologies are affected: ${g.description.toLowerCase()}. This fails WCAG 2.1 SC ${g.wcag} (Level ${g.level}).`;
50
+ const prompt = g.count > 1
51
+ ? `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}.`
52
+ : `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}.`;
49
53
  return {
50
54
  ruleId: g.ruleId,
51
55
  selectors: g.selectors,
52
56
  instanceCount: g.count,
53
- explanation: g.description,
57
+ explanation,
54
58
  fixedCode: v.html,
55
59
  wcagReference: `WCAG 2.1 SC ${g.wcag}`,
56
- optimalPrompt: `Fix this accessibility violation: The element \`${v.html.slice(0, 120)}\` at selector \`${g.selectors[0]}\` violates ${g.wcag} — ${g.description}.${instanceNote} Please fix it in the codebase.`,
60
+ optimalPrompt: prompt,
57
61
  };
58
62
  }
59
63
  }
package/dist/ai/ollama.js CHANGED
@@ -43,15 +43,19 @@ export class OllamaProvider {
43
43
  }
44
44
  fallbackFix(g) {
45
45
  const v = g.representative;
46
- const instanceNote = g.count > 1 ? ` There are ${g.count} similar instances at: ${g.selectors.join(', ')}.` : '';
46
+ const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
47
+ const explanation = `Users relying on assistive technologies are affected: ${g.description.toLowerCase()}. This fails WCAG 2.1 SC ${g.wcag} (Level ${g.level}).`;
48
+ const prompt = g.count > 1
49
+ ? `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}.`
50
+ : `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}.`;
47
51
  return {
48
52
  ruleId: g.ruleId,
49
53
  selectors: g.selectors,
50
54
  instanceCount: g.count,
51
- explanation: g.description,
55
+ explanation,
52
56
  fixedCode: v.html,
53
57
  wcagReference: `WCAG 2.1 SC ${g.wcag}`,
54
- optimalPrompt: `Fix this accessibility violation: The element \`${v.html.slice(0, 120)}\` at selector \`${g.selectors[0]}\` violates ${g.wcag} — ${g.description}.${instanceNote} Please fix it in the codebase.`,
58
+ optimalPrompt: prompt,
55
59
  };
56
60
  }
57
61
  }
package/dist/ai/openai.js CHANGED
@@ -49,15 +49,19 @@ export class OpenAIProvider {
49
49
  }
50
50
  fallbackFix(g) {
51
51
  const v = g.representative;
52
- const instanceNote = g.count > 1 ? ` There are ${g.count} similar instances at: ${g.selectors.join(', ')}.` : '';
52
+ const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
53
+ const explanation = `Users relying on assistive technologies are affected: ${g.description.toLowerCase()}. This fails WCAG 2.1 SC ${g.wcag} (Level ${g.level}).`;
54
+ const prompt = g.count > 1
55
+ ? `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}.`
56
+ : `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}.`;
53
57
  return {
54
58
  ruleId: g.ruleId,
55
59
  selectors: g.selectors,
56
60
  instanceCount: g.count,
57
- explanation: g.description,
61
+ explanation,
58
62
  fixedCode: v.html,
59
63
  wcagReference: `WCAG 2.1 SC ${g.wcag}`,
60
- optimalPrompt: `Fix this accessibility violation: The element \`${v.html.slice(0, 120)}\` at selector \`${g.selectors[0]}\` violates ${g.wcag} — ${g.description}.${instanceNote} Please fix it in the codebase.`,
64
+ optimalPrompt: prompt,
61
65
  };
62
66
  }
63
67
  }
package/dist/ai/prompt.js CHANGED
@@ -11,10 +11,10 @@ export function buildPrompt(groups) {
11
11
  Each item must have:
12
12
  - "ruleId": the rule id from the input
13
13
  - "selectors": array of CSS selectors affected (copy from input)
14
- - "explanation": 1-2 sentences explaining why this matters for users with disabilities (plain English, no jargon)
14
+ - "explanation": 1-2 sentences on concrete user impact — name exactly who is affected (e.g. "screen reader users", "keyboard-only users", "users with low vision") and what they cannot do because of this specific violation. Do not write generic statements like "users with disabilities may be affected."
15
15
  - "fixedCode": the corrected HTML snippet only (no explanation, just code)
16
- - "wcagReference": e.g. "WCAG 2.1 SC 1.1.1 Non-text Content"
17
- - "optimalPrompt": a ready-to-paste prompt the developer can give to an AI coding assistant (Cursor, GitHub Copilot, Claude) to fix this issue in their codebase. Include the violating elements, what rule they break, how many instances there are, and exactly what change is needed. Be specific and actionable.
16
+ - "wcagReference": the full criterion name, e.g. "WCAG 2.1 SC 1.1.1 Non-text Content (Level A)"
17
+ - "optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude). Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated (e.g. "WCAG 2.1 SC 1.1.1 Non-text Content, Level A"), (2) list the affected selectors and HTML snippets, (3) state the exact code change needed to comply. Focus solely on the fix — do not explain why WCAG exists or what accessibility is. Be precise and actionable.
18
18
 
19
19
  Return ONLY a valid JSON array. No markdown, no code fences, no explanation outside the JSON.
20
20
 
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import { Command } from 'commander';
3
3
  import { loadConfig, initConfig } from './config.js';
4
4
  import { crawl } from './crawler.js';
5
5
  import { createAIProvider } from './ai/index.js';
6
+ import { groupViolations } from './ai/group.js';
6
7
  import { printTerminalReport, printAIPrompts } from './reporter/terminal.js';
7
8
  import { generateMarkdownReport } from './reporter/markdown.js';
8
9
  import { runDemo } from './demo.js';
@@ -10,7 +11,7 @@ const program = new Command();
10
11
  program
11
12
  .name('wcag-a11y')
12
13
  .description('WCAG 2.1/2.2 accessibility auditor with AI-powered fixes')
13
- .version('0.2.0');
14
+ .version('0.3.0');
14
15
  program
15
16
  .command('init')
16
17
  .description('Create a11y.config.json in the current directory')
@@ -27,6 +28,8 @@ program
27
28
  .option('-r, --report', 'Save a full markdown report to a11y-report.md', false)
28
29
  .option('--no-ai', 'Skip AI fix generation (faster, violations only)')
29
30
  .option('--no-explain', 'Hide AI fix explanations in terminal output')
31
+ .option('--no-terminal', 'Suppress terminal output (violations summary)')
32
+ .option('--fast-mode', 'Output only AI fix prompts — no summaries or explanations', false)
30
33
  .option('--group <strategy>', 'Group violations by rule or show individually (rule|none)', 'rule')
31
34
  .option('--ci', 'Exit with code 1 if any violations are found (for CI/CD pipelines)', false)
32
35
  .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama)')
@@ -34,7 +37,9 @@ program
34
37
  try {
35
38
  console.log(`\nScanning ${opts.url}...`);
36
39
  const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
37
- printTerminalReport(result);
40
+ if (opts.terminal && !opts.fastMode) {
41
+ printTerminalReport(result);
42
+ }
38
43
  const strategy = opts.group === 'none' ? 'none' : 'rule';
39
44
  if (opts.ai && result.totalViolations > 0) {
40
45
  const config = loadConfig();
@@ -43,15 +48,20 @@ program
43
48
  }
44
49
  const provider = createAIProvider(config);
45
50
  const allViolations = result.pages.flatMap((p) => p.violations);
46
- console.log(`\nGenerating AI fixes for ${allViolations.length} violations...`);
51
+ const ruleGroups = groupViolations(allViolations, strategy);
52
+ if (!opts.fastMode) {
53
+ console.log(`\nGenerating AI fixes for ${ruleGroups.length} rule groups (${allViolations.length} violations)...`);
54
+ }
47
55
  const fixes = await provider.generateFixes(allViolations, strategy);
48
- printAIPrompts(fixes, { explain: opts.explain });
56
+ if (opts.terminal) {
57
+ printAIPrompts(fixes, { explain: opts.explain, fastMode: opts.fastMode });
58
+ }
49
59
  if (opts.report) {
50
- generateMarkdownReport(result, fixes);
60
+ generateMarkdownReport(result, fixes, { fastMode: opts.fastMode });
51
61
  }
52
62
  }
53
63
  else if (opts.report) {
54
- generateMarkdownReport(result, []);
64
+ generateMarkdownReport(result, [], { fastMode: opts.fastMode });
55
65
  }
56
66
  if (opts.ci && result.totalViolations > 0) {
57
67
  process.exit(1);
@@ -5,7 +5,31 @@ const IMPACT_EMOJI = {
5
5
  moderate: '🟡',
6
6
  minor: '🟢',
7
7
  };
8
- export function generateMarkdownReport(result, fixes, outputPath = 'a11y-report.md') {
8
+ export function generateMarkdownReport(result, fixes, opts = {}, outputPath = 'a11y-report.md') {
9
+ const lines = opts.fastMode
10
+ ? buildFastReport(fixes)
11
+ : buildFullReport(result, fixes);
12
+ writeFileSync(outputPath, lines.join('\n'), 'utf-8');
13
+ console.log(`\nReport saved → ${outputPath}`);
14
+ }
15
+ function buildFastReport(fixes) {
16
+ const lines = [
17
+ '# WCAG A11y — Fix Prompts',
18
+ `> Generated: ${new Date().toLocaleString()}`,
19
+ '',
20
+ ];
21
+ for (const fix of fixes) {
22
+ const countLabel = fix.instanceCount > 1 ? ` ×${fix.instanceCount}` : '';
23
+ lines.push(`## \`${fix.ruleId}\`${countLabel}`, '');
24
+ lines.push('**Selectors:**');
25
+ for (const sel of fix.selectors) {
26
+ lines.push(`- \`${sel}\``);
27
+ }
28
+ lines.push('', '```', fix.optimalPrompt, '```', '', '---', '');
29
+ }
30
+ return lines;
31
+ }
32
+ function buildFullReport(result, fixes) {
9
33
  const lines = [
10
34
  '# WCAG A11y Report',
11
35
  `> Generated: ${new Date().toLocaleString()}`,
@@ -37,6 +61,5 @@ export function generateMarkdownReport(result, fixes, outputPath = 'a11y-report.
37
61
  lines.push('---', '');
38
62
  }
39
63
  }
40
- writeFileSync(outputPath, lines.join('\n'), 'utf-8');
41
- console.log(`\nReport saved → ${outputPath}`);
64
+ return lines;
42
65
  }
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { groupViolations } from '../ai/group.js';
2
3
  const IMPACT_COLOR = {
3
4
  critical: chalk.red,
4
5
  serious: chalk.yellow,
@@ -22,12 +23,17 @@ export function printTerminalReport(result) {
22
23
  m > 0 ? chalk.blue(`${m} moderate`) : '',
23
24
  ].filter(Boolean).join(' ');
24
25
  console.log(`\n ${chalk.red('✖')} ${url} ${counts}`);
25
- for (const v of page.violations) {
26
- const color = IMPACT_COLOR[v.impact] ?? chalk.white;
27
- const tag = color(`[${v.impact.toUpperCase()}]`);
28
- const wcag = chalk.gray(`WCAG ${v.wcag}`);
29
- console.log(` ${tag} ${v.description} ${wcag}`);
30
- console.log(` ${chalk.gray('→')} ${chalk.dim(v.selector)}`);
26
+ const groups = groupViolations(page.violations, 'rule');
27
+ for (const g of groups) {
28
+ const color = IMPACT_COLOR[g.impact] ?? chalk.white;
29
+ const countSuffix = g.count > 1 ? chalk.gray(` ×${g.count}`) : '';
30
+ const tag = color(`[${g.impact.toUpperCase()}]`) + countSuffix;
31
+ const wcag = chalk.gray(`WCAG ${g.wcag}`);
32
+ console.log(` ${tag} ${g.description} ${wcag}`);
33
+ console.log(` ${chalk.gray('→')} ${chalk.dim(g.selectors[0])}`);
34
+ if (g.count > 1) {
35
+ console.log(` ${chalk.gray(` …and ${g.count - 1} more`)}`);
36
+ }
31
37
  }
32
38
  }
33
39
  console.log('\n' + chalk.gray('─'.repeat(60)));
@@ -37,6 +43,15 @@ export function printTerminalReport(result) {
37
43
  export function printAIPrompts(fixes, opts) {
38
44
  if (fixes.length === 0)
39
45
  return;
46
+ if (opts.fastMode) {
47
+ for (let i = 0; i < fixes.length; i++) {
48
+ console.log(fixes[i].optimalPrompt);
49
+ if (i < fixes.length - 1)
50
+ console.log('\n' + chalk.gray('─'.repeat(60)));
51
+ }
52
+ console.log('');
53
+ return;
54
+ }
40
55
  console.log('\n' + chalk.bold.magenta('AI Fix Prompts') + chalk.gray(' — paste any of these into Cursor, Copilot, or Claude'));
41
56
  console.log(chalk.gray('─'.repeat(60)));
42
57
  for (const fix of fixes) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wcag-a11y",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes",
5
5
  "type": "module",
6
6
  "bin": {