Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .claude/issues/34-a11y-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Issue #34 — Add accessibility skill and wp-tooling a11y runner (pa11y-ci)

**Status:** in-progress <!-- in-progress | in-review | done -->
**Branch:** `v1.0.0/task/a11y-skill`
**PR:** #<pr-number> <!-- fill once opened -->
**Assignee:** @Adi-ty

---

## Summary

WordPress developers — and the coding agents working alongside them — need a fast, local loop to find and fix accessibility (WCAG2AA) problems in a plugin or theme. pa11y is already the org's established accessibility engine (installed and configured by the `setup/pa11y` scaffold), but there was no local, agent-driven command that turns pa11y's output into something an agent can act on and trace back to source.

This task adds a `wp-tooling a11y` command that runs the consumer-installed `pa11y-ci` and hands back a normalised report, plus a companion Claude Code `accessibility` skill that maps each violation's DOM selector to the template, block, or PHP that rendered it and fixes it with consent. Accessibility is a rendered-DOM concern, so this stays pure Node reusing pa11y — no WordPress ability, no MCP.

---

## Decisions made

- [2026-07-06] No `--url` flag — the runner is **config-driven only** (`.pa11yci.json` or `--config <path>`). pa11y-ci merges cwd-config URLs with positional CLI URLs, so a flag could not reliably override the config without temp-config patching; a single source of truth keeps the runner clean.
- [2026-07-06] `setup/pa11y` dependency pin corrected `pa11y-ci ^6.0.0` → `^4.1.1` (6.x does not exist on npm; 4.1.1 is latest). The scaffold's dep merge is non-destructive, so only fresh projects were affected.
- [2026-07-06] URL-level load failures (`net::ERR_*`) are classified as `scanError` + `summary.failedUrls`, never as violations; the CLI exits 1 for them (environment problem), keeping exit 3 meaningful.
- [2026-07-06] The `accessibility` skill treats `setup/pa11y` as one way to get a config, not a requirement — a project with its own pa11y setup works as-is.
- [2026-07-06] Skill housed in wp-tooling (`skills/accessibility/`) and distributed via `setup/claude-skills`, rather than wp-dev-tools as the Phase 2 plan sketches, because wp-dev-tools has no public repo yet for remote scaffold sources. Migrate later if the lens suite consolidates there.
- [2026-07-06] `setup/pa11y` template ships `"runners": ["axe", "htmlcs"]` (the engines catch disjoint issues — proven in the evals: axe found a nested-list violation htmlcs missed) per the Phase 2 plan §5.3.
- [2026-07-06] Template URLs changed to project-owned surfaces: front page + `sample_page` (default `/?p=1`) + `search_page` (default `/?s=hello`) + optional `extra_page`, each appended to `base_url`. `wp-admin`/`wp-login` dropped: unauthenticated `/wp-admin/` only audits the login redirect (double-counting it), and `wp-login.php` chrome is core-owned — permanently exit-3 on findings no project can fix. Page paths are scaffold inputs (engine renderer cannot iterate lists, so slot inputs + a falsy-omitted section; further URLs are added directly in the generated file).

---

## Files changed so far

- `src/a11y/errors.js` — new (`RunnerError`: `EBINMISSING` / `EBINFAIL` / `EBADJSON` / `ENOURLS`)
- `src/a11y/resolve-bin.js` — new (local/hoisted `.bin` walk-up, `npx --no-install` fallback, version probe)
- `src/a11y/urls.js` — new (resolve URLs from the pa11y config)
- `src/a11y/normalize.js` — new (pure normaliser: summary counts, `wcagCriterion` parse, `domHints` extraction, `scanError` classification)
- `src/a11y/run.js` — new (`runA11y()` core + `runCli()` adapter; exit codes 0/1/2/3)
- `src/a11y/index.js` — new (barrel exposed as `@rtcamp/wp-tooling/a11y`)
- `src/cli/commands/a11y.js` — new (dispatcher shim)
- `package.json` — edited (`"./a11y"` exports entry)
- `tests/a11y/*` — new (cli, normalize, resolve-bin, urls specs + fixtures)
- `skills/accessibility/SKILL.md`, `skills/accessibility/evals/evals.json` — new (seven-step lens skill + 3 behavioural evals)
- `scaffolds/setup/claude-skills/**` — edited (manifest description + two `files[]` entries; two new template copies)
- `scaffolds/setup/pa11y/scaffold.json` — edited (dep pin fix)
- `skills/README.md` — edited (What's here + install snippets)
- `CHANGELOG.md` — edited (two Unreleased entries)
- `src/init/index.js`, `tests/ui/selects.test.js` — edited (pre-existing lint-gate errors at HEAD: `no-shadow` on `cap`, prettier wrapping; `npm run check` fails without these fixes)

---

## Verification run

```bash
$ npm run check # eslint src tests && jest
# ESLint: clean
# Tests: 726 passed, 54 suites
```

Tested live end-to-end on a WordPress plugin running under wp-env, with `@rtcamp/wp-tooling` installed as a dev dependency and the skills installed via `setup/claude-skills`: `--dry-run` resolves the local `pa11y-ci` 4.1.1, the config and its URLs; a plugin-rendered alt-less `<img>` seeded on the front page produces H37 alongside WordPress core's F92/ARIA4 on wp-login → exit 3 with `failedUrls: 0`.

Skill evals ran as subagents from `skills/accessibility/evals/evals.json`, with all assertions passing: eval-1 (audit and fix) 8/8, eval-2 (missing setup) 5/5, eval-3 (upstream/unreachable) 6/6. Outputs and per-eval `grading.json` are kept locally (`skills/*-workspace/` is gitignored).

---

## Open questions

- _(none blocking)_

---

## Notes for the reviewer

- The exit-code contract is the API: 0 clean · 1 run failure or unreachable URL · 2 usage/binary missing · 3 violations. `failedUrls > 0` downgrades an otherwise-clean run to exit 1 so CI never greenlights a scan that silently loaded nothing.
- `pa11y-ci` is never a dependency of `@rtcamp/wp-tooling` (zero-runtime-deps rule) — the runner resolves the consumer's install and `npx --no-install` never fetches from the network.
- `skills/accessibility-workspace/` (eval outputs) is gitignored by the existing `skills/*-workspace/` rule.

---

## Handoff log

_(no rotations yet — delete this line when the first entry is added)_
2 changes: 2 additions & 0 deletions node-packages/wp-tooling/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- `wp-tooling a11y` subcommand + `@rtcamp/wp-tooling/a11y` library (`runA11y()` core, `runCli()` CLI adapter). Runs the consumer-installed `pa11y-ci` — resolved from the project's `node_modules/.bin` (walking up for hoisted installs), else `npx --no-install`; never a network install — against the URLs in the project's pa11y config (`.pa11yci.json` by default, `--config <path>` to point elsewhere) and normalises the JSON into a stable report: `summary` counts plus per-URL `violations` carrying `id`, parsed `wcagCriterion`, `impact`, `runner`, `message`, `selector`, `context`, and grep-ready `domHints` (`tagName`, `classList`, `idAttr`, `attrs`). A URL that fails to load is a `scanError` counted in `summary.failedUrls`, never a violation. Exit codes: 0 clean · 1 run failure or unreachable URL · 2 usage/binary missing · 3 violations found. Supports `--output text|json` and `--dry-run`. The `setup/pa11y` scaffold's dependency pin is corrected to the published `pa11y-ci ^4.1.1`, and its config template now runs both engines (`"runners": ["axe", "htmlcs"]`) and scans project-owned URLs — the front page plus configurable page paths (`sample_page`, `search_page`, optional `extra_page`, each appended to `base_url`) — instead of `wp-admin`/`wp-login` (unauthenticated admin scans only ever audit the core-owned login chrome).
- `accessibility` Claude Code skill (`skills/accessibility/`) — a find → fix → re-check lens over `wp-tooling a11y`: triages violations by WCAG criterion and impact, maps each one to the theme/plugin source that rendered it via the report's `domHints`, proposes minimal fixes with consent, and re-verifies until clean; core/third-party markup is classified as upstream and reported, never patched. Installed into consumers by `setup/claude-skills` alongside the `scaffold` and `setup` skills.
- Remote scaffolds — a scaffold's `scaffold.json` + templates can live in another repo. `scaffolds/sources.json` lists the source repos (pinned `{ repository, ref, path }`); each repo publishes a `scaffolds/index.json` enumerating the scaffolds it offers, which the registry fetches to discover them (one PR in the owning repo adds/changes a scaffold; wp-tooling only changes to onboard a new repo). Manifests + templates are fetched on `add`, cached under `${XDG_CACHE_HOME:-$HOME/.cache}/wp-tooling/remote/` and validated with ETag conditional requests (`304 Not Modified` serves the cache; movable tags refresh when they move). New error code `EFETCHFAIL` (network/HTTP) distinct from `EBADSCAFFOLD` (bad index/manifest). `list` is online-preferred with a cache fallback and reports unreachable sources as warnings; `validate --remote` fetches + schema-validates each index + manifest; `wp-tooling cache clear` empties the cache. Dormant by default — no `sources.json` ships.
- Engine-side input discovery (`discover_from`) — an input declaration can say where to source its value from the project, so the engine fills it instead of the caller guessing. Resolves from `composer.json` / `package.json` (dotted paths; `autoload.psr-4` yields the root namespace) and `.wp-tooling.json`, with precedence `supplied → discovered → default`. Fail-safe: a missing or malformed project file falls through to the input's `default`, so a project without those files behaves exactly as before the resolver existed. Adds an optional `transform` step for derived inputs (`json-escape` doubles backslashes for the PSR-4 composer key). The inputs the engine actually rendered with are surfaced on `execute()` as `engine.inputs`. Bundled `setup/psr4` + `wp/*` scaffolds annotated with `discover_from`.
- Feature toggle layer — a scaffold may declare an optional `feature` block (`config_key`, `owned_files`, `confirm_remove`, `gitignore`) marking it as a toggleable project feature. New TTY-free `enable` / `disable` / `status` verbs create or remove the owned files idempotently, manage `.gitignore` lines (Mustache-rendered against resolved inputs), prompt before deleting consumer-editable files (`confirm_remove`, overridable with `--force`), and persist on/off state in `.wp-tooling.json`. New `wp-tooling features` command — lists feature status by default, with `--enable` / `--disable` to toggle (plus `--json`, `--force`, `--no-install`, `--dry-run`); `setup/tailwind` ships as the first such feature. Additive — the `feature` block never affects the `add` / `execute` path.
Expand Down
1 change: 1 addition & 0 deletions node-packages/wp-tooling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"./release": "./src/release/index.js",
"./hooks": "./src/hooks/index.js",
"./ci": "./src/ci/index.js",
"./a11y": "./src/a11y/index.js",
"./version-monitor": "./src/version-monitor/index.js"
},
"files": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"slug": "claude-skills",
"category": "setup",
"name": "Claude Code skills",
"description": "Installs the rtCamp scaffold and setup Claude Code skills into .claude/skills/. Drop-in copy of the SKILL.md and evals.json that ship with @rtcamp/wp-tooling. Run this once per project to let Claude drive the scaffolding flow.",
"description": "Installs the rtCamp scaffold, setup and accessibility Claude Code skills into .claude/skills/. Drop-in copy of the SKILL.md and evals.json that ship with @rtcamp/wp-tooling. Run this once per project to let Claude drive the scaffolding flow.",
"source": "template",
"inputs": [
{
Expand Down Expand Up @@ -31,6 +31,16 @@
"src": "templates/setup-evals.json",
"dest": "{{skills_dir}}/setup/evals/evals.json",
"raw": true
},
{
"src": "templates/accessibility-SKILL.md",
"dest": "{{skills_dir}}/accessibility/SKILL.md",
"raw": true
},
{
"src": "templates/accessibility-evals.json",
"dest": "{{skills_dir}}/accessibility/evals/evals.json",
"raw": true
}
]
}
Loading