diff --git a/docs/design/0256-restore-a11y.md b/docs/design/0256-restore-a11y.md new file mode 100644 index 0000000..cde46d5 --- /dev/null +++ b/docs/design/0256-restore-a11y.md @@ -0,0 +1,68 @@ +# Design Doc: Restore a11y as `audit --check a11y` + +> **Status:** Accepted +> **Date:** 2026-07-13 +> **Author:** Claude (direct implementation, no worker — user directive) +> **Related issues:** #256 +> **Related PRs:** (this PR) + +--- + +## Problem + +`a11y_engine.py` (WCAG 2.1 accessibility analysis: missing alt text, missing +form labels, ARIA issues, keyboard-nav gaps, non-semantic HTML, color contrast, +heading order, link text, focus management) is fully functional — verified 2 +real findings on `tests/fixtures/sample.html` — but **orphaned**: its CLI +entry point (the old standalone `a11y` command) was deleted in the #195 +umbrella consolidation, and `audit_accessibility()` is now reachable from no +command, MCP tool, or `--check` sub-mode. + +This is the **exact same situation as `css-deep` (issue #251, PR #252)** and +`export-snapshot` (issue #218): a working engine with a dead entry point. + +## Goal + +`codelens audit --check a11y` runs `audit_accessibility()` and +returns findings in the standard audit umbrella shape, with `--severity` and +`--category` passthrough. + +## Changes + +### New Files +- `scripts/commands/a11y.py` — thin wrapper over + `a11y_engine.audit_accessibility()`, mirroring `css_deep.py`'s structure. + No engine logic duplicated. + +### Modified Files +- `scripts/commands/audit.py` — registered `a11y` in `_CHECKS`, added + namespace branch (severity + category passthrough), updated epilog. +- `tests/test_command_registry.py` — added `a11y` to the + implementation-module allowlist (it's imported by the audit umbrella, + not self-registering — same pattern as `css_deep`). + +### Not Changed +- `a11y_engine.py` — the engine already works and needed no changes. + +## Why a sub-check, not a restored top-level command + +Same reasoning as #251: the #195 consolidation reduced 78 commands to 12 +umbrellas. Re-adding `a11y` as a top-level command would violate that. +As `--check a11y` under `audit` — alongside dead-code / complexity / smell / +perf-hint / css — it fits the audit umbrella taxonomy and keeps the command +count at exactly 12. + +## Testing + +Verified end-to-end via CLI: `codelens audit tests --check a11y` → 2 real +findings (missing_label high + semantic_html low) from `tests/fixtures/sample.html`. +`--severity high` passthrough confirmed. `tests/test_command_registry.py` +2 passed. + +## Alternatives Considered + +- **Leave it dropped.** Rejected — the engine works, HTML/accessibility is in + scope (CodeLens covers HTML files), and the loss forces manual auditing. +- **Restore as a top-level `a11y` command.** Rejected — violates the + 12-umbrella consolidation. Sub-check placement recovers the capability + without growing the command surface. diff --git a/scripts/commands/a11y.py b/scripts/commands/a11y.py new file mode 100644 index 0000000..ccaa12e --- /dev/null +++ b/scripts/commands/a11y.py @@ -0,0 +1,45 @@ +# @WHO: scripts/commands/a11y.py +# @WHAT: Accessibility (WCAG 2.1) audit command — thin wrapper over a11y_engine (issue #256) +# @PART: commands +# @ENTRY: execute() +"""a11y command — accessibility audit (issue #256 restoration). + +Wraps ``a11y_engine.audit_accessibility()`` — detects missing alt text, +missing form labels, ARIA issues, keyboard-nav gaps, non-semantic HTML, +color-contrast, heading-order, link-text, and focus-management problems +(WCAG 2.1). + +The engine was never deleted, but its CLI entry point (the old standalone +``a11y`` command) was dropped in the #195 umbrella consolidation, leaving +the working engine orphaned — the exact same situation as ``css-deep`` +(issue #251) and ``export-snapshot`` (issue #218). This restores access as +``audit --check a11y`` — a sub-check under the audit umbrella, NOT a new +top-level command, so the 12-umbrella consolidation is preserved (command +count stays 12). +""" + +from a11y_engine import audit_accessibility +from commands import register_command + + +def add_args(parser): + parser.add_argument("workspace", nargs="?", default=None, + help="Path to workspace root (auto-detected if omitted)") + parser.add_argument("--severity", choices=["high", "medium", "low"], default=None, + help="Filter by severity level") + parser.add_argument("--category", default=None, + help="Filter to one category: missing_alt, missing_label, " + "aria_issues, keyboard_nav, semantic_html, color_contrast, " + "heading_order, link_text, focus_management") + + +def execute(args, workspace): + return audit_accessibility( + workspace, + category=getattr(args, "category", None), + severity=getattr(args, "severity", None), + ) + +# Issue #256: registered as the `a11y` sub-check of the `audit` umbrella +# (see commands/audit.py), NOT a standalone command — command count stays +# 12. Imported by audit.py, not self-registering. diff --git a/scripts/commands/audit.py b/scripts/commands/audit.py index 1ad623e..91197eb 100644 --- a/scripts/commands/audit.py +++ b/scripts/commands/audit.py @@ -63,6 +63,10 @@ "module": "commands.css_deep", "help": "Deep CSS analysis: unused vars, orphan keyframes, specificity wars, z-index abuse (issue #251)", }, + "a11y": { + "module": "commands.a11y", + "help": "Accessibility (WCAG 2.1): missing alt/labels, ARIA, keyboard-nav, semantic HTML (issue #256)", + }, } ALL_CHECKS = list(_CHECKS.keys()) @@ -81,12 +85,15 @@ def add_args(parser): " side-effect Pure vs impure function analysis\n" " css Deep CSS analysis: unused vars, orphan keyframes,\n" " specificity wars, z-index abuse (issue #251)\n" + " a11y Accessibility (WCAG 2.1): missing alt/labels, ARIA,\n" + " keyboard-nav, semantic HTML (issue #256)\n" "\n" "Examples:\n" " codelens audit . # all checks\n" " codelens audit . --check dead-code # only dead-code\n" " codelens audit . --check complexity,smell # pick subset\n" " codelens audit . --check css # deep CSS analysis\n" + " codelens audit . --check a11y # accessibility audit\n" ) parser.add_argument("workspace", nargs="?", default=None, help="Path to workspace root (auto-detected if omitted)") @@ -184,6 +191,10 @@ def _build_namespace(base_args, check_name: str) -> argparse.Namespace: # cssdeep_engine accepts severity (high|medium|low) + single category ns.severity = getattr(base_args, "severity", None) ns.category = getattr(base_args, "category", None) + elif check_name == "a11y": + # a11y_engine accepts severity (high|medium|low) + single category + ns.severity = getattr(base_args, "severity", None) + ns.category = getattr(base_args, "category", None) return ns diff --git a/tests/test_command_registry.py b/tests/test_command_registry.py index 7f4712c..4587ff4 100644 --- a/tests/test_command_registry.py +++ b/tests/test_command_registry.py @@ -39,7 +39,7 @@ def test_every_command_module_registers(): # register_command() calls were removed because the CLI alias is gone, # but the umbrella commands import them for --check sub-analyses. _DEPRECATED_ALIAS_MODULES = { - "affected", "arch_metrics", "architecture", "binary_scan", + "a11y", "affected", "arch_metrics", "architecture", "binary_scan", "circular", "complexity", "css_deep", "dashboard", "dataflow", "dead_code", "dependents", "diagnostics", "diff", "env_check", "export_snapshot", "git_status", "graph_schema", "import_snapshot",