From a622bf203f01d1ddb37b423b5cabd91045e16a48 Mon Sep 17 00:00:00 2001 From: Mark Eaton Date: Wed, 17 Sep 2025 14:23:26 -0400 Subject: [PATCH] feat: add script to audit guides for unique WCAG 2.1 AA issues using pa11y Co-authored-by: aider (o3) --- check_wcag_unique.py | 100 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 check_wcag_unique.py diff --git a/check_wcag_unique.py b/check_wcag_unique.py new file mode 100644 index 0000000..507d310 --- /dev/null +++ b/check_wcag_unique.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Audit each guide URL in guides.csv for WCAG 2.1 AA accessibility issues +(using pa11y). Writes a report that omits issues appearing in *every* +guide. + +Output file: output2.txt +Usage: + python check_wcag_unique.py +Prerequisite: + npm install -g pa11y # or have pa11y available via npx +""" +from __future__ import annotations + +import csv +import json +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Dict, List, Set + +GUIDES_CSV = Path(__file__).with_name("guides.csv") +OUTPUT_FILE = Path("output2.txt") + + +def run_pa11y(url: str) -> List[dict]: + """Run pa11y on the URL and return its JSON‐parsed list of issues.""" + cmd_base = ["pa11y"] if shutil.which("pa11y") else ["npx", "pa11y"] + result = subprocess.run( + [*cmd_base, url, "--standard", "WCAG2AA", "--reporter", "json"], + capture_output=True, + text=True, + check=False, + ) + + # pa11y exits 0 (no issues) or 1 (issues found). Treat both as success. + if result.returncode not in (0, 1): + print(f"[ERROR] pa11y failed with code {result.returncode} for {url}", file=sys.stderr) + return [] + + try: + return json.loads(result.stdout) + except json.JSONDecodeError: + print(f"[WARN] Could not parse pa11y JSON output for {url}", file=sys.stderr) + return [] + + +def read_guides(csv_path: Path) -> List[Dict[str, str]]: + with csv_path.open(newline="", encoding="utf-8-sig") as fh: + return list(csv.DictReader(fh)) + + +def compute_common_codes(issues_by_guide: Dict[str, List[dict]]) -> Set[str]: + """Return set of issue codes present in *every* guide.""" + common: Set[str] | None = None + for issues in issues_by_guide.values(): + codes = {i["code"] for i in issues} + common = codes if common is None else common & codes + return common or set() + + +def format_issue(issue: dict) -> str: + selector = (issue.get("selector") or "").strip() or "N/A" + return f"[{issue['type']}] {issue['code']}: {issue['message']} (selector: {selector})" + + +def main() -> None: + guides = read_guides(GUIDES_CSV) + issues_by_guide: Dict[str, List[dict]] = {} + + print("Auditing guides with pa11y…\n") + for guide in guides: + name, url = guide["Name"], guide["URL"] + print(f"• {name} — {url}") + issues_by_guide[name] = run_pa11y(url) + + common_codes = compute_common_codes(issues_by_guide) + + lines: List[str] = [] + lines.append("Accessibility report (WCAG 2.1 AA — unique issues only)") + lines.append("=" * 50) + + for name, issues in issues_by_guide.items(): + unique = [i for i in issues if i["code"] not in common_codes] + lines.append(f"\nGuide: {name}") + if not unique: + lines.append(" ✔ No unique issues found.") + continue + for issue in unique: + lines.append(f" • {format_issue(issue)}") + + report = "\n".join(lines) + print("\n" + report) + OUTPUT_FILE.write_text(report, encoding="utf-8") + print(f"\nReport written to {OUTPUT_FILE.resolve()}") + + +if __name__ == "__main__": + main()