[v1.0.0] feat(a11y): add wp-tooling a11y runner and accessibility skill - #35
Conversation
e218727 to
c280617
Compare
There was a problem hiding this comment.
Avoid magic numbers, use readable variable names, split functions into smaller units that have smaller responsibilities. Since this is a company wide tooling project, code readability needs to be kept in mind.
A reader should be able to understand a function's flow at a first glance.
There was a problem hiding this comment.
Pull request overview
Adds a config-driven pa11y accessibility runner and companion source-mapping skill for WordPress projects.
Changes:
- Adds the
a11yCLI/library, report normalisation, binary resolution, and tests. - Adds and distributes the accessibility skill with behavioural evals.
- Updates the pa11y scaffold, documentation, and lint formatting.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
tests/ui/selects.test.js |
Applies lint formatting. |
tests/scaffolds/bundled-manifests.test.js |
Tests rendered pa11y configurations. |
tests/a11y/urls.test.js |
Tests config URL resolution. |
tests/a11y/resolve-bin.test.js |
Tests consumer binary discovery. |
tests/a11y/normalize.test.js |
Tests report normalisation. |
tests/a11y/fixtures/pa11y-ci.json |
Provides violation fixtures. |
tests/a11y/fixtures/empty-urls.pa11yci.json |
Provides an empty-config fixture. |
tests/a11y/fixtures/.pa11yci.json |
Provides a standard config fixture. |
tests/a11y/cli.test.js |
Tests CLI output and exit codes. |
src/init/index.js |
Resolves lint findings. |
src/cli/commands/a11y.js |
Registers the command. |
src/a11y/urls.js |
Loads URLs from configuration. |
src/a11y/run.js |
Implements runner and CLI behaviour. |
src/a11y/resolve-bin.js |
Resolves consumer-installed pa11y-ci. |
src/a11y/normalize.js |
Produces stable accessibility reports. |
src/a11y/index.js |
Exposes the library API. |
src/a11y/errors.js |
Defines structured runner errors. |
skills/README.md |
Documents the accessibility skill. |
skills/accessibility/SKILL.md |
Defines the accessibility workflow. |
skills/accessibility/evals/evals.json |
Adds skill evaluations. |
scaffolds/setup/pa11y/templates/.pa11yci.json.mustache |
Adds runners and project-owned URLs. |
scaffolds/setup/pa11y/scaffold.json |
Updates inputs and dependency version. |
scaffolds/setup/claude-skills/templates/accessibility-SKILL.md |
Distributes the skill definition. |
scaffolds/setup/claude-skills/templates/accessibility-evals.json |
Distributes skill evaluations. |
scaffolds/setup/claude-skills/scaffold.json |
Registers accessibility skill files. |
package.json |
Exports the a11y library. |
CHANGELOG.md |
Records the new runner and skill. |
.claude/issues/34-a11y-skill.md |
Records implementation decisions and verification. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- ECONFIGJS exit 2 for JS config paths (was EBADJSON exit 1) - parseReport handles dual JSON from reporters:['json'] - propagate config defaults.standard to report label - dry-run exits 2 when pa11y-ci missing (was 0) - add evals for missing-alt and colour-contrast scenarios
edf3eff to
3dd6cb9
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Binary invocation, exit-code classification, and setup guidance contain functional cross-platform and workflow defects.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 33/33 changed files
- Comments generated: 8
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Config selection can diverge from the validated JSON file, DOM hints mishandle quoted greater-than characters, and documentation and scaffold coverage need correction.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
node-packages/wp-tooling/src/a11y/urls.js:74
pa11y-ci4.1.1 does not necessarily load this exact JSON path. Its CLI strips the extension, then prefers an extensionless file,.cjs, and.jsbefore.json; therefore a project containing both.pa11yci.jsand.pa11yci.jsonpasses this JSON validation but the scan executes the JavaScript config instead. Reject shadowing sibling configs (or otherwise guarantee that pa11y-ci consumes the validated file) so the resolved URLs/standard cannot diverge from the actual scan.
- Files reviewed: 35/35 changed files
- Comments generated: 3
- Review effort level: Balanced
| { | ||
| "src": "templates/accessibility-SKILL.md", | ||
| "dest": "{{skills_dir}}/accessibility/SKILL.md", | ||
| "raw": true |
There was a problem hiding this comment.
🔵 Needs a closer look
Mixed-standard reports, incomplete dynamic-block guidance, and broken direct-copy documentation remain unresolved.
Review details
Suppressed comments (4)
Previously missed (4) — in code that hasn't changed since the last review.
node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/accessibility-SKILL.md:85
- Keep the scaffolded copy aligned with the canonical skill: this workflow also lacks the generated Gutenberg class handling and nearest-
wp-block-*ancestor fallback required by the bundled color-contrast eval.
node-packages/wp-tooling/skills/accessibility/SKILL.md:85 - The workflow does not contain the dynamic-block fallback required by the new evals: it always starts by grepping
classList, whilehas-*-colorclasses are generated and eval 5 expects their slugs to be traced to block attributes/theme.json via the nearestwp-block-*selector ancestor. Add that special case and ancestor fallback so the shipped skill can satisfy its own evaluation contract.
node-packages/wp-tooling/src/a11y/urls.js:50 - Valid pa11y-ci URL objects can override
standardper URL, but this collapses the config todefaults.standard. A mixed config therefore reports (and prints) the wrong WCAG standard for some results. Either reject per-URL standard overrides or preserve each URL’s effective standard in the normalized contract.
node-packages/wp-tooling/skills/README.md:28 - These copy commands fail after the preceding default-branch clone: this monorepo stores the skills under
node-packages/wp-tooling/skills, not at repository-rootskills. Point the commands at the monorepo paths (or clone thenpm/wp-toolingsplit branch) so Option B is executable.
- Files reviewed: 35/35 changed files
- Comments generated: 0 new
- Review effort level: Balanced
…ll (#35) * feat(a11y): add wp-tooling a11y runner and accessibility skill * feat(scaffolds): best-practice pa11y config with configurable page paths * refactor(a11y): address code review for readability and DRY * fix(a11y): address code review and add eval coverage - ECONFIGJS exit 2 for JS config paths (was EBADJSON exit 1) - parseReport handles dual JSON from reporters:['json'] - propagate config defaults.standard to report label - dry-run exits 2 when pa11y-ci missing (was 0) - add evals for missing-alt and colour-contrast scenarios * refactor(a11y): break up long function and consolidate counters * delete: remove claude issue tracking documentation * fix(a11y): address CodeQL polynomial regex findings * fix(a11y): distinguish missing vs broken binaries, fix error codes, and accuracy * test(a11y): tighten test fixtures and consolidate duplicate specs * fix(a11y): address Copilot review — quoted-tag parsing, doc accuracy, scaffold test
What this PR does
Adds a
wp-tooling a11ycommand that runs the consumer-installedpa11y-ciagainst the project's pa11y config and emits a normalized report an agent can act on. Ships a companionaccessibilityClaude Code skill that triages the violations, maps each one to the source that rendered it via the report'sdomHints, fixes it with consent, and re-verifies.Closes
Closes #34
Changes
src/a11y/library (runA11y()core +runCli()adapter) exposed as@rtcamp/wp-tooling/a11y, with asrc/cli/commands/a11y.jsdispatcher shim..pa11yci.jsonor--config <path>. Exit codes are 0 clean, 1 run failure or unreachable URL, 2 usage or binary missing, 3 violations found.scanErrorand counted insummary.failedUrls, never as violations.skills/accessibility/skill (SKILL.md + evals) on the find → fix → re-check lens flow;setup/claude-skillsnow distributes it alongsidescaffoldandsetup.setup/pa11yscaffold fixed and improved:pa11y-cipin corrected to the published^4.1.1, template runs both engines (axe+htmlcs), and scans project-owned URLs (front page plus configurablesample_page/search_page/extra_pagepaths) instead ofwp-admin/wp-login.CHANGELOG.mdand.claude/issues/34-a11y-skill.mdupdated.src/init/index.jsandtests/ui/selects.test.jscarry small lint fixes: HEAD fails thenpm run lintgate without them.How I verified
edge cases ✓ skips entry with missing slug and warns (1 ms) ✓ skips preset section when array is empty ✓ skips preset section when key is absent ✓ returns a valid @theme {} block for empty themeJson GenerateTailwindThemePlugin.generate() ✓ writes _tailwind-theme.css containing the @theme block (13 ms) ✓ scaffolds tailwind.css with layer and theme imports when absent (2 ms) ✓ does not overwrite tailwind.css when it already exists (2 ms) ✓ updates _tailwind-theme.css when theme.json changes (3 ms) ✓ does not rewrite _tailwind-theme.css when content is unchanged (2 ms) ✓ logs an error and writes nothing when theme.json is missing ✓ logs an error when theme.json contains invalid JSON (1 ms) Test Suites: 1 passed, 1 total Tests: 20 passed, 20 total Snapshots: 0 total Time: 0.107 s, estimated 1 s Ran all test suites. > @rtcamp/wp-tooling@0.1.0 check > npm run lint && npm test > @rtcamp/wp-tooling@0.1.0 lint > eslint src tests > @rtcamp/wp-tooling@0.1.0 test > jest PASS tests/scaffolds/features.test.js feature block validation ✓ rejects a feature block missing config_key (13 ms) status() ✓ lists features as disabled when no .wp-tooling.json exists (3 ms) ✓ reflects an enabled flag from .wp-tooling.json (3 ms) enable() ✓ creates files, sets the flag, and adds gitignore lines (5 ms) ✓ is idempotent on re-enable (existing files skipped, flag stays true) (5 ms) ✓ renders {{placeholder}} gitignore lines against resolved inputs (3 ms) ✓ throws ENOTFEATURE for a non-feature scaffold (1 ms) disable() ✓ removes owned files, clears the flag, and removes gitignore lines (5 ms) ✓ keeps a confirm_remove file when confirmRemove is absent (7 ms) ✓ removes a confirm_remove file when confirmRemove returns true (5 ms) ✓ reports missing files and is a safe no-op when already disabled (2 ms) enable()/disable() mirror via persisted feature files ✓ persists rendered file lists on enable (5 ms) ✓ disable removes the enable-time paths without re-supplying inputs (5 ms) recordDeps() ✓ records missing deps sorted, preserving tab indentation ✓ never overwrites an existing version range (1 ms) ✓ is a no-op without a package.json ✓ applyChange with install:false, record:true records deps instead of installing (4 ms) ✓ applyChange with plain install:false reports deps without touching package.json (6 ms) parseArgs() ✓ wires the shared value guard for --enable (5 ms) ✓ accepts --flag=value and repeated flags ✓ parses --record-deps (off by default) applyChange disable summary ✓ includes the documented `missing` list (4 ms) PASS tests/scaffolds/add.test.js add command argument parsing ✓ exits 0 on --help without doing work (2 ms) ✓ exits 1 when scaffold id is missing ✓ still accepts --flag=value for values starting with -- ✓ exits 1 (not interactive hang) on a parse error (1 ms) formatErrorPayload ✓ keeps EBADSCAFFOLD file + errors fields (documented payload) add command non-interactive flow ✓ writes files in non-interactive mode (43 ms) ✓ emits a single JSON line on --json (15 ms) ✓ --dry-run does not write files (19 ms) ✓ emits structured error JSON on --json for missing input (30 ms) ✓ emits ENOSCAFFOLD JSON for unknown id (33 ms) engine core is not coupled to TTY UI ✓ non-interactive code path does not require the TTY UI kit (2 ms) PASS tests/scaffolds/validate.test.js validate base scaffold ✓ accepts a minimal valid scaffold (5 ms) ✓ rejects non-objects ✓ reports missing required fields ✓ rejects unknown top-level fields (1 ms) ✓ rejects malformed slug ✓ accepts optional category in kebab-case ✓ accepts nested category with slashes (e.g. lint/phpcs) ✓ rejects unknown source value ✓ rejects unknown wizard_step value (1 ms) validate files block ✓ rejects non-array files ✓ rejects entry missing src (1 ms) ✓ accepts empty files array (source: package case) validate inputs block ✓ accepts a full inputs entry (1 ms) ✓ rejects uppercase input key ✓ rejects duplicate input keys ✓ rejects invalid transform value validate wiring block ✓ accepts a wiring entry ✓ accepts a wiring entry with no anchor (anchor is advisory per the AI contract) ✓ rejects a wiring entry whose anchor is an empty string ✓ rejects a wiring entry missing snippet_template validate tests block ✓ accepts a tests entry ✓ rejects unknown framework ✓ accepts actionlint for YAML validate secrets block ✓ accepts a secrets entry ✓ rejects lowercase secret key ✓ rejects unknown scope ✓ rejects extra fields (no value smuggling) (1 ms) validate scripts block ✓ accepts npm-only scripts (1 ms) ✓ accepts composer-only scripts ✓ accepts both targets ✓ rejects unknown script target (1 ms) ✓ rejects non-string command ✓ rejects non-object map per target validate dependency maps ✓ accepts a composer dependency map (1 ms) ✓ rejects non-string version validate rejects the removed repository field ✓ treats 'repository' as an unknown top-level field ✓ rejects source: 'repository' (no longer an allowed source) validateOne on-disk checks ✓ source: template still reports missing files (1 ms) validate runCli ✓ exits 0 on --help and prints usage ✓ validates the whole bundled catalogue with exit 0 (19 ms) ✓ filters by single scaffold id (6 ms) ✓ exits 1 on unknown scaffold id with available list (5 ms) ✓ --json emits machine-readable results (5 ms) ✓ --json emits structured error for unknown id on stderr (6 ms) ✓ includes project-local scaffolds when --cwd is set (7 ms) ✓ rejects unknown flags ✓ validates a valid project sources file (offline, no fetch) (9 ms) ✓ fails when a source has a bad repository slug (7 ms) validate --remote (sources + index) ✓ default validate (no --remote, no cache) makes no network call (13 ms) ✓ recognises a remote id offline from a cached index (28 ms) ✓ --remote fetches the index + schema-validates a sound manifest (ok) (12 ms) ✓ --remote fails a manifest whose checksum mismatches the index (13 ms) ✓ --remote surfaces EFETCHFAIL when the manifest 404s (9 ms) ✓ --remote reports schema errors from a fetched-but-invalid manifest (11 ms) ✓ --remote flags a remote id colliding with a local scaffold (10 ms) PASS tests/scaffolds/list.test.js list command help / parsing ✓ --help prints usage and exits 0 (3 ms) ✓ rejects --origin with invalid value list command, human output (default catalogue only) ✓ lists all bundled scaffolds with categories (38 ms) ✓ groups scaffolds by category in the human output (19 ms) list command, --json output ✓ emits a single JSON line on stdout (16 ms) ✓ JSON entries carry the expected shape and counts (29 ms) ✓ reports secrets for the WPORG workflow scaffold (20 ms) list command, two-directory merge ✓ shows project-local scaffolds alongside defaults (30 ms) ✓ --origin=project shows ONLY project scaffolds (30 ms) ✓ --category filter narrows the listing (32 ms) ✓ empty filter returns an empty scaffolds array (not an error) (27 ms) PASS tests/cli/index.test.js cli main() ✓ no args prints top-level usage and exits 0 (1 ms) ✓ --help prints top-level usage ✓ -h prints top-level usage ✓ --version prints package version and exits 0 ✓ -v prints package version ✓ unknown top-level flag exits 2 with stderr message (1 ms) ✓ unknown subcommand exits 2 with stderr message ✓ routes detect-changes --help to its runCli (2 ms) ✓ routes detect-changes through to its runCli with args (2 ms) ✓ detect-changes propagates a usage-error exit code cli main() central error handling ✓ CancelledError from a subcommand exits 130 with a stderr message (1 ms) ✓ non-CancelledError rejections propagate to the bin shim (5 ms) cli COMMANDS registry ✓ detect-changes is registered with a summary and run handler ✓ install-hooks is registered with a summary and run handler ✓ scaffold subcommand "add" is registered with a summary and run handler ✓ scaffold subcommand "list" is registered with a summary and run handler ✓ scaffold subcommand "validate" is registered with a summary and run handler (1 ms) cli main() routes scaffold subcommands ✓ routes add --help to src/scaffolds/add runCli (2 ms) ✓ routes list --help to src/scaffolds/list runCli (1 ms) ✓ routes validate --help to src/scaffolds/validate runCli (1 ms) cli main() routes install-hooks ✓ routes install-hooks --help to its runCli (1 ms) cli loadCommands() ✓ discovers a valid command module and indexes it by name (52 ms) ✓ ignores non-.js files in the commands directory (8 ms) ✓ throws a clear error when a module is missing required fields (3 ms) ✓ throws when two modules register the same name (5 ms) ✓ returns entries in deterministic (sorted) order (8 ms) PASS tests/init/git.test.js installGitHooks ✓ installs both native hooks and uses no Husky (47 ms) ✓ preserves an existing non-Husky prepare by chaining (49 ms) ✓ replaces a Husky prepare and is idempotent (73 ms) ✓ skips cleanly when the directory is not a git repo PASS tests/scaffolds/registry.test.js ScaffoldRegistry construction ✓ rejects empty args (17 ms) ✓ accepts a single string (back-compat for project-only) (4 ms) ✓ accepts object form (1 ms) scan() ✓ discovers scaffolds and tags source (3 ms) ✓ returns empty when no scaffolds found (1 ms) ✓ throws EBADSCAFFOLD on malformed JSON (1 ms) ✓ throws EBADSCAFFOLD on schema-invalid scaffold (1 ms) two-directory merge ✓ project entries override default entries on category/slug collision (4 ms) ✓ non-colliding default and project entries both appear (12 ms) get() and filter() ✓ finds scaffold by category/slug id (1 ms) ✓ finds scaffold by slug alone (no category) ✓ returns null for unknown id ✓ filter by predicate collectDependencies() ✓ merges composer/npm deps across selected scaffolds (3 ms) execute() result shape ✓ returns the four-block shape (2 ms) ✓ dry-run writes no files (2 ms) ✓ full run writes files (2 ms) ✓ skips existing files (no overwrite) (1 ms) ✓ treats a test entry sharing a file dest as declarative, on both clean run and idempotent re-run (D2) (3 ms) ✓ warns on supplied inputs the scaffold does not declare (1 ms) ✓ does not warn when every supplied input is declared execute() error paths ✓ throws ENOSCAFFOLD with available list ✓ throws EMISSINGINPUT with missingDetails when required input is absent (1 ms) execute() passes scripts through to developer block ✓ passes npm and composer scripts verbatim (2 ms) ✓ emits empty script maps when scaffold declares none (1 ms) ✓ keeps npm runtime vs dev vs composer-suggest in distinct fields (no merging) (2 ms) execute() never embeds secret values ✓ passes through declared secrets without any value field (2 ms) remote scaffolds (sources + index) ✓ scan fetches the index and surfaces a thin remote record (10 ms) ✓ remote id colliding with a local scaffold throws EBADSCAFFOLD (4 ms) ✓ same id offered by two sources throws EBADSCAFFOLD (2 ms) ✓ unreachable index is skipped with a warning, not a hard failure (3 ms) ✓ malformed index is skipped with a warning; local scaffolds survive (4 ms) ✓ schema-invalid index is skipped with a warning, not a hard failure (3 ms) ✓ execute hydrates the manifest then fetches + writes the template (6 ms) ✓ renders the fetched manifest inputs into the template (9 ms) ✓ ETag cache serves the body on 304 across fresh registries (8 ms) ✓ malformed remote manifest throws EBADSCAFFOLD (6 ms) ✓ schema-invalid remote manifest throws EBADSCAFFOLD (6 ms) ✓ manifest 404 throws EFETCHFAIL (3 ms) ✓ index checksum is verified against the fetched manifest (10 ms) ✓ checksum mismatch throws EBADSCAFFOLD before any write (10 ms) ✓ checksum matches case-insensitively (uppercase hex + SHA256: prefix) (11 ms) ✓ remote manifest dest escaping --cwd throws EWRITEFAIL (6 ms) ✓ dry-run fetches the manifest but not the template, writes nothing (8 ms) ✓ parallel template prefetch: 3 templates fetched concurrently (106 ms) ✓ re-run on an existing dest performs no further fetch (offline-safe) (21 ms) PASS tests/version-monitor/npm.test.js npm detector ✓ reports an update when a newer version is published (3 ms) ✓ reports nothing when current equals latest ✓ skips floating pins like "*" ✓ skips pre-release publishes PASS tests/scaffolds/config.test.js detectIndent() ✓ detects tab indentation (2 ms) ✓ detects two-space indentation ✓ returns null for single-line / empty input (1 ms) writeConfig() indentation ✓ preserves tabs when the file was written with tabs (1 ms) ✓ defaults to two spaces for a new file (1 ms) feature-file persistence ✓ setFeatureState sets the flag and round-trips file lists in one write ✓ setFeatureState preserves an existing file indentation (1 ms) ✓ returns null when nothing was persisted ✓ clearFeatureFiles removes the entry and an empty block PASS tests/scaffolds/cli-support.test.js requireFlagValue ✓ returns a normal value ✓ throws when the value is missing (5 ms) ✓ throws when the next token is itself a flag ✓ points the user at the =value form for dash-leading values fetchOptsFrom ✓ maps only the set fetch options paths + buildRegistry ✓ resolves the bundled defaults dir and a project bin/scaffolds dir ✓ scans the bundled catalogue (71 ms) PASS tests/ui/terminal.test.js ANSI ✓ should expose the expected escape sequences (2 ms) isTTY ✓ should return true when stdout is a TTY (1 ms) ✓ should return false when stdout is not a TTY write / writeLine ✓ should write text without appending a newline ✓ should append a newline when writeLine is called (1 ms) ✓ should default writeLine to an empty line clearLine ✓ should write clear-line + carriage return when stdout is a TTY ✓ should write nothing when stdout is not a TTY (1 ms) moveCursorUp ✓ should emit the move-up escape when n > 0 in a TTY ✓ should not write anything when n is 0 ✓ should not write anything in non-TTY (1 ms) hideCursor / showCursor ✓ should write the hide/show sequences in a TTY ✓ should write nothing in non-TTY readLine -- TTY mode ✓ should resolve with the typed answer ✓ should reject with CancelledError on SIGINT (1 ms) ✓ should resolve empty when closed without an answer or cancellation readLine -- non-TTY mode ✓ should buffer lines from stdin and resolve in order (2 ms) ✓ should reject queued waiters as cancelled when stdin closes (1 ms) ✓ should reject immediately as cancelled after stdin has closed (2 ms) ✓ should buffer lines that arrive before a reader is waiting onKeypress ✓ should return a no-op cleanup in non-TTY (1 ms) ✓ should wire up keypress events on stdin in a TTY and clean them up (2 ms) ✓ should skip setRawMode when stdin does not support it PASS tests/release/cli.test.js release CLIs - dispatcher registration ✓ release:bump is registered ✓ release:changelog is registered ✓ release:zip is registered release CLIs - parseArgs ✓ release:bump parses --type, --to, --dry-run, --help ✓ release:bump rejects unknown args (4 ms) ✓ release:changelog parses --to, --dry-run, --help ✓ release:zip parses --force, --dry-run, --help (1 ms) release CLIs - runCli help ✓ release:bump --help prints usage and returns 0 ✓ release:changelog --help prints usage and returns 0 ✓ release:zip --help prints usage and returns 0 ✓ release:bump unknown arg returns 2 release CLIs - runCli end-to-end against fixture ✓ release:bump --type patch updates files and returns 0 (7 ms) ✓ release:bump --dry-run does not modify files (4 ms) ✓ release:bump exits 1 when plugin entry missing (5 ms) ✓ release:changelog rewrites CHANGELOG and returns 0 (7 ms) ✓ release:changelog exits 1 on empty Unreleased (5 ms) ✓ release:zip writes dist/<slug>-<version>.zip and returns 0 (29 ms) ✓ release:zip refuses to overwrite without --force (78 ms) PASS tests/release/bump.test.js release/bump - pure helpers ✓ nextVersion patch ✓ nextVersion minor zeros patch ✓ nextVersion major zeros minor + patch ✓ nextVersion explicit override wins ✓ nextVersion rejects malformed current (6 ms) ✓ nextVersion rejects malformed explicit ✓ nextVersion rejects unknown type ✓ slugToConstantPrefix converts kebab to UPPER_SNAKE ✓ rewritePluginEntry rewrites Version header and constant value ✓ rewritePluginEntry also rewrites const-style constants ✓ rewriteJsonVersion preserves a 2-space indent ✓ rewriteJsonVersion returns null when version key absent release/bump - integration against fixture ✓ patch bump updates package.json, composer.json, plugin entry, constant (6 ms) ✓ --to overrides --type (6 ms) ✓ dry-run does not write any file (8 ms) ✓ composer.json without version is left untouched (7 ms) ✓ throws when plugin entry has no Version header (12 ms) ✓ exits non-zero (via thrown error) when plugin entry is missing (5 ms) ✓ rejects a malformed config.constantPrefix (9 ms) ✓ config.constantPrefix overrides the slug-derived constant name (7 ms) PASS tests/scaffolds/prompt-inputs.test.js promptMissingInputs ✓ passes `defaultValue` (not `default`) to text() (1 ms) ✓ omits defaultValue when scaffold input has no default confirmRun ✓ passes `defaultValue: true` (not `default: true`) to confirm() PASS tests/release/changelog.test.js release/changelog - pure helpers ✓ isoDate formats UTC date as YYYY-MM-DD (2 ms) ✓ unreleasedHasContent detects bullets ✓ unreleasedHasContent detects plain paragraph content ✓ unreleasedHasContent rejects empty Unreleased ✓ unreleasedHasContent rejects subheadings without bullets ✓ rewriteChangelog renames heading + prepends fresh Unreleased (1 ms) ✓ rewriteChangelog throws when Unreleased missing (3 ms) ✓ rewriteChangelog throws when Unreleased is empty release/changelog - integration against fixture ✓ reads version from package.json by default (6 ms) ✓ --to overrides package.json version (6 ms) ✓ dryRun leaves CHANGELOG unchanged (4 ms) ✓ refuses to run when Unreleased is empty (11 ms) ✓ throws when CHANGELOG.md is missing (9 ms) ✓ throws on malformed semver passed via --to (3 ms) PASS tests/release/zip.test.js release/zip - helpers ✓ crc32 matches known vectors ✓ dosTimeDate encodes a known epoch (1 ms) ✓ dosTimeDate clamps year >= 1980 ✓ compileIgnorePattern returns null for comments and blanks ✓ compileIgnorePattern matches a simple filename ✓ compileIgnorePattern with trailing / matches directories only (1 ms) ✓ compileIgnorePattern with single * does not cross / ✓ compileIgnorePattern with ** crosses / ✓ loadIgnorePatterns falls back to defaults when no .distignore (1 ms) ✓ resolveEpoch honours SOURCE_DATE_EPOCH env var ✓ resolveEpoch honours explicit option over env ✓ FALLBACK_EPOCH is exposed and stable release/zip - walkProject ✓ honours .distignore and skips .git / dist always (14 ms) release/zip - integration ✓ builds dist/<slug>-<version>.zip and excludes per .distignore (9 ms) ✓ two runs against the same tree produce byte-identical zips (6 ms) ✓ refuses to overwrite existing zip without --force (11 ms) ✓ overwrites existing zip when --force is set (7 ms) ✓ dryRun does not write dist/ (6 ms) ✓ default ignore list excludes node_modules when .distignore absent (4 ms) ✓ zipPack round-trips a single entry through DEFLATE (1 ms) PASS tests/version-monitor/reporter.test.js report ✓ groups updates by source with a heading and a summary (2 ms) ✓ adds a Notes column only to groups containing a major bump ✓ uses singular wording for a single update ✓ reports no updates cleanly PASS tests/scaffolds/features-install.test.js installDeps ✓ runs npm without a shell, passing each spec as a literal argv element (6 ms) ✓ raises EINSTALLFAIL with npm stderr attached on failure (8 ms) PASS tests/release/context.test.js release/context ✓ findPluginEntry returns the *.php with Plugin Name header (3 ms) ✓ findPluginEntry throws when no entry file is present (16 ms) ✓ findPluginEntry throws when multiple entry files exist at root (6 ms) ✓ findPluginEntry ignores nested *.php with Plugin Name header (5 ms) ✓ loadContext returns the full project shape (3 ms) ✓ loadContext sets composerJson to null when composer.json is absent (3 ms) ✓ loadContext throws when package.json is missing (3 ms) ✓ loadContext throws when package.json has no version field (8 ms) ✓ loadContext propagates a malformed package.json error (3 ms) PASS tests/a11y/normalize.test.js normalizeA11y ✓ tags the tool and default standard (1 ms) ✓ summary counts violations by impact across URLs ✓ results are sorted by URL ✓ a URL with no issues is reported as clean ✓ violations sort by impact, then id, then selector ✓ maps HTMLCS codes to a WCAG criterion and axe codes to null (5 ms) ✓ extracts DOM hints from the context for the grep-to-source step (1 ms) ✓ honours a custom standard label (1 ms) ✓ tolerates missing or malformed results ✓ a load failure becomes scanError, not a violation parseWcagCriterion ✓ pulls the dotted criterion from an HTMLCS code ✓ returns null when there is no criterion extractDomHints ✓ falls back to the selector tag when context has no opening tag ✓ captures aria attributes PASS tests/ui/selects.test.js checkbox (non-TTY) ✓ should return selected items by number (1 ms) ✓ should return unique selections in display order ✓ should handle empty input gracefully radio (non-TTY) ✓ should return a single selected item ✓ should default to first choice on invalid input flat select validation ✓ should throw when checkbox choices is missing (8 ms) ✓ should throw when radio choices is empty flat select (TTY) -- Ctrl+C ✓ should reject with CancelledError on Ctrl+C (1 ms) checkboxTree (non-TTY) ✓ should return selected items from groups by number (1 ms) ✓ should return unique selections in display order ✓ should keep pre-checked items when input is empty (keep model) ✓ should toggle items off and on from the pre-checked state (1 ms) ✓ should handle empty selection ✓ should throw when groups is missing (2 ms) ✓ should throw when groups is null ✓ should throw when a group items is not an array ✓ should return an empty array for empty groups without prompting checkboxTree (TTY) ✓ should resolve selections in display order, not toggle order (1 ms) ✓ should reject with CancelledError on Ctrl+C PASS tests/scaffolds/fetch.test.js defaultCacheDir ✓ honours XDG_CACHE_HOME when set (1 ms) ✓ falls back to ~/.cache when XDG_CACHE_HOME is empty composeUrl ✓ builds a raw-content URL ✓ normalises leading and trailing slashes on each segment ✓ rejects a '..' segment in the relative path (5 ms) ✓ rejects a '..' segment in the source path with EFETCHFAIL fetchRemoteFile ✓ cache miss fetches, writes body + ETag sidecar (3 ms) ✓ cache hit revalidates with If-None-Match; 304 serves cache (11 ms) ✓ 200 with a new body updates the cache (9 ms) ✓ --refresh re-fetches without a conditional header (7 ms) ✓ --refresh ignores a stray 304 and re-fetches a real body (7 ms) ✓ offline (transport error) with a cached copy serves cache + warns (14 ms) ✓ 404 with a cached copy still throws (does not serve stale) (9 ms) ✓ sends User-Agent and Authorization when token supplied (2 ms) ✓ falls back to WP_TOOLING_GITHUB_TOKEN env var (1 ms) ✓ non-2xx with no cache throws EFETCHFAIL with statusCode ✓ 403 + rate-limit body sets rateLimited (1 ms) ✓ transport error with no cache throws EFETCHFAIL with cause ✓ response-stream error (reset mid-body) rejects EFETCHFAIL (1 ms) ✓ timeout with no cache throws EFETCHFAIL (1 ms) ✓ cache write failure records a warning but returns body (2 ms) readCached ✓ returns null when nothing is cached, never hits the network ✓ returns the cached body when present (1 ms) PASS tests/version-monitor/config.test.js parseConfigYaml ✓ parses inline-flow mappings and lists ✓ ignores comments and blank lines loadConfig ✓ loads and normalises the sample config (2 ms) ✓ throws naming the path when the config is missing (1 ms) validate ✓ defaults paths for an enabled source that omits them ✓ rejects an unknown source ✓ rejects a non-string paths entry ✓ rejects a missing sources mapping PASS tests/scaffolds/discover-from.test.js discover_from: composer.json:autoload.psr-4 ✓ replaces the first segment of each default, preserving its sub-namespace (10 ms) ✓ does NOT override a path input (base_path keeps its default) (3 ms) ✓ falls back to default when composer.json is absent (backward compatible) (5 ms) ✓ falls back to default when composer.json is malformed (10 ms) ✓ lets a supplied value win over discovery (5 ms) discover_from: config:<key> ✓ resolves a value from .wp-tooling.json (3 ms) ✓ falls back to default when .wp-tooling.json is absent (6 ms) discover_from: package.json:<dot.path> ✓ resolves a string value via a dotted path (3 ms) PASS tests/version-monitor/updater.test.js applyUpdates ✓ rewrites each of the six source types in place (5 ms) ✓ skips major bumps unless allowMajor is set (6 ms) ✓ rewrites every occurrence of a duplicated scalar pin (5 ms) ✓ writes nothing on a dry run (4 ms) ✓ records an update whose current value is not found as unmatched (5 ms) PASS tests/version-monitor/http.test.js getJson ✓ resolves parsed JSON on a 2xx response (2 ms) ✓ sends a User-Agent and a bearer token when given one (4 ms) ✓ rejects on a non-2xx status (2 ms) ✓ flags rate-limit responses ✓ rejects on invalid JSON ✓ rejects on a transport error ✓ rejects and tears down the request on timeout (1 ms) isClientError ✓ is true for a non-rate-limit 4xx ✓ is false for rate limits, 5xx, and transport errors PASS tests/a11y/cli.test.js a11y runCli ✓ --help prints usage and exits 0 (1 ms) ✓ unknown flag exits 2 ✓ invalid --output exits 2 ✓ --config with no value exits 2 ✓ an unreadable config exits 2 (ENOURLS) (1 ms) ✓ missing pa11y-ci exits 2 with the install hint (1 ms) ✓ the resolved config path is handed to pa11y-ci via --config ✓ violations found: exits 3 with a parseable JSON report (1 ms) ✓ clean run exits 0 ✓ text mode prints a human summary (1 ms) ✓ unparseable output exits 1 (EBADJSON) ✓ a genuine run failure exits 1 (EBINFAIL) (1 ms) ✓ an unreachable URL is a run failure (exit 1), not a violation (2 ms) ✓ a scan failure is reported in text mode ✓ --dry-run prints the plan and runs pa11y-ci not at all PASS tests/init/examples.test.js stripRegions ✓ drops only the marker lines when keeping the body (1 ms) ✓ drops the markers and the enclosed body when removing ✓ only touches regions tagged with the given marker expandGlob ✓ expands a single-segment * glob to matching absolute paths (3 ms) ✓ returns nothing when no file matches (1 ms) PASS tests/scaffolds/traversal.test.js execute() refuses paths outside --cwd ✓ rejects a `..` in a path input (7 ms) ✓ rejects a `..` in a manifest file dest (2 ms) ✓ rejects a `..` in a manifest test dest (4 ms) ✓ still allows nested paths inside the target directory (7 ms) disable() refuses removals outside --cwd ✓ rejects a tampered featureFiles entry pointing outside the project (6 ms) PASS tests/ci/detect-changes.test.js detectChanges ✓ counts files into the right buckets (1 ms) ✓ default ignore excludes docs and .wordpress-org ✓ default ignore preserves .github/workflows and .github/actions (1 ms) ✓ lockfile changes count under both css and js buckets ✓ phpstan.neon and phpstan.neon.dist count as php (1 ms) ✓ composer.json and composer.lock count as php ✓ string --ignore overrides the default ✓ RegExp --ignore is accepted directly (1 ms) ✓ null ignore disables filtering ✓ empty-string ignore disables filtering ✓ invalid ignore type throws TypeError (23 ms) ✓ accepts a newline-delimited string for files ✓ tolerates Windows line endings in file list ✓ invalid files type throws TypeError ✓ returns zero counts for an empty list ✓ gha bucket excludes nested-directory yml files outside workflows/actions ✓ includeFiles adds <bucket>-files arrays alongside counts (1 ms) ✓ includeFiles omitted leaves the result counts-only (8 ms) ✓ includeFiles preserves the same file in multiple buckets (5 ms) exports ✓ DEFAULT_PATTERNS has the four expected buckets (2 ms) ✓ DEFAULT_IGNORE matches docs/, .wordpress-org/, and .github/ non-workflow paths runCli ✓ --help prints usage and exits 0 ✓ unknown flag exits 2 with stderr message (1 ms) ✓ invalid --output exits 2 ✓ --files <path> with --output json prints valid JSON (1 ms) ✓ --output github appends key=value lines to $GITHUB_OUTPUT (1 ms) ✓ --output github warns to stderr when GITHUB_OUTPUT is unset ✓ --dry-run parses cleanly and exits 0 (1 ms) ✓ --dry-run + --output github does not touch $GITHUB_OUTPUT and previews to stdout (1 ms) ✓ --dry-run + --output github previews even when $GITHUB_OUTPUT is unset (1 ms) ✓ text mode prints key: value lines ✓ --ignore overrides default ✓ missing --files path exits 1 with stderr message ✓ --files followed by another flag exits 2 (does not swallow the flag) (1 ms) ✓ --files at end of argv exits 2 ✓ --ignore followed by another flag exits 2 ✓ --files - is accepted as the stdin sentinel (parser does not reject lone dash) ✓ --include-files in json mode emits <bucket>-files arrays (1 ms) ✓ --include-files in text mode prints space-joined paths ✓ --include-files in github mode writes heredoc multi-line outputs (1 ms) ✓ --include-files dry-run previews heredoc blocks without writing (1 ms) ✓ invalid --ignore regex exits 2 with a clean usage error PASS tests/version-monitor/php.test.js php detector ✓ targets the latest stable at the current granularity (2 ms) ✓ reports nothing when already on the latest branch (1 ms) ✓ skips compound constraints ✓ ignores pre-release releases when picking the latest (1 ms) PASS tests/ui/spinner.test.js spinner ✓ should return an object with start, succeed, fail, update methods (1 ms) ✓ should default to current text when succeed is called without args non-TTY mode ✓ should print plain text on start in non-TTY (1 ms) ✓ should print succeed message in non-TTY (4 ms) ✓ should print fail message in non-TTY TTY mode ✓ should animate frames on an interval (1 ms) ✓ should not create multiple intervals when start is called twice ✓ should stop animation on succeed ✓ should stop animation on fail ✓ should update text while running (1 ms) version-monitor: actions lookup for "actions/checkout" failed: rate limited PASS tests/version-monitor/actions.test.js actions.parseUses ✓ extracts version-tagged actions and skips non-tag refs actions detector ✓ reports an update when a newer release exists (1 ms) ✓ reports nothing when the pinned ref is already latest ✓ stops early on a rate-limit error (1 ms) PASS tests/scaffolds/render.test.js render ✓ substitutes a single placeholder ✓ substitutes multiple placeholders ✓ substitutes the same placeholder repeatedly ✓ accepts whitespace inside braces (1 ms) ✓ throws ERENDERFAIL on undefined placeholder (5 ms) ✓ does NOT HTML-escape (critical for code generation) ✓ treats null vars argument as empty (1 ms) ✓ passes through templates with no placeholders ✓ throws on non-string template collectPlaceholders ✓ lists unique placeholders in first-appearance order ✓ returns empty array for templates with no placeholders ✓ returns empty for non-string input ✓ includes section flags and inverted sections render — sections ✓ keeps inner content when section flag is truthy ("true") ✓ drops inner content when section flag is falsy ("false") ✓ treats empty string, "no", "0" as falsy ✓ treats arbitrary non-empty value as truthy (1 ms) ✓ inverted section renders when flag is falsy ✓ substitutes variables inside a truthy section ✓ throws ERENDERFAIL when section flag is undefined (3 ms) ✓ handles multi-line section content (singleton use-case) applyTransform ✓ pascal-case from kebab ✓ pascal-case from snake ✓ kebab-case from PascalCase ✓ snake-case from kebab (1 ms) ✓ upper-snake-case from kebab ✓ json-escape doubles backslashes ✓ returns value unchanged when no transform ✓ returns value unchanged for unknown transform PASS tests/skills/evals-json.test.js skills/accessibility/evals/evals.json ✓ file exists ✓ parses as JSON (1 ms) ✓ skill_name matches the directory name ✓ evals[] is a non-empty array ✓ each eval has id, prompt, expected_output, files, expectations (5 ms) ✓ eval ids are unique within the skill (1 ms) skills/scaffold/evals/evals.json ✓ file exists ✓ parses as JSON ✓ skill_name matches the directory name ✓ evals[] is a non-empty array ✓ each eval has id, prompt, expected_output, files, expectations (2 ms) ✓ eval ids are unique within the skill skills/setup/evals/evals.json ✓ file exists ✓ parses as JSON (1 ms) ✓ skill_name matches the directory name ✓ evals[] is a non-empty array ✓ each eval has id, prompt, expected_output, files, expectations (4 ms) ✓ eval ids are unique within the skill PASS tests/scaffolds/bundled-manifests.test.js setup/psr4 wiring snippet ✓ renders a JSON-valid PSR-4 key for a multi-segment namespace (6 ms) wp/cli PSR-4 discovery (grafted sub-namespaces) ✓ fills namespace and tests_namespace from composer.json, keeping kind sub-namespaces (3 ms) wiring targetFile normalisation ✓ emits a `..`-free targetFile for wp/cli (4 ms) setup/pa11y rendered config ✓ renders valid JSON with default page paths (extra_page omitted) (12 ms) ✓ renders custom page paths and appends extra_page when given (6 ms) PASS tests/init/transform.test.js applyReplacements ✓ replaces every token in one pass (1 ms) ✓ does not re-scan substituted text (no cascade) ✓ prefers the longest source token at each position (1 ms) ✓ returns the input unchanged with no replacements resolveWithin ✓ resolves a relative path inside the root ✓ allows the root itself ✓ rejects a parent-directory escape (2 ms) ✓ rejects an absolute path outside the root (1 ms) applyVersion ✓ stamps the version into json and php-header files (1 ms) PASS tests/init/identity.test.js validateName ✓ accepts a normal multi-word name ✓ rejects an empty name ✓ rejects a name starting with a digit ✓ rejects a PHP reserved keyword generateIdentity ✓ derives every case variant and WP convention from a name (1 ms) ✓ splits acronym boundaries buildIdentityReplacements ✓ covers the key case variants and prefixes ✓ covers the namespace in single- and double-backslash forms ✓ is de-duplicated by source token ✓ is sorted longest-source-first so specific tokens win (1 ms) PASS tests/scaffolds/sources.test.js parseId ✓ splits category/slug (1 ms) ✓ handles nested category ✓ handles slug with no category readSources ✓ returns null when the file is absent ✓ reads + parses the fixture sources (1 ms) ✓ throws EBADSCAFFOLD on invalid JSON (1 ms) validateSources ✓ accepts a valid sources file ✓ accepts the fixture ✓ rejects a non-object ✓ rejects a missing sources array ✓ rejects an unknown top-level field ✓ rejects a source missing required fields ✓ rejects a bad repository slug (1 ms) ✓ rejects an unknown source field ✓ flags a duplicate repository + path source ✓ same repo + path but different ref is not a duplicate validateIndex ✓ accepts a valid index ✓ accepts an optional checksum ✓ rejects a non-object ✓ rejects a missing scaffolds array ✓ rejects an entry missing required fields ✓ rejects a bad slug in id ✓ rejects an unknown entry field ✓ rejects duplicate ids indexEntryToRecord ✓ builds a thin remote record with the resolved scaffold path ✓ normalises slashes when joining source.path + entry.path version-monitor: npm lookup for "lodash" failed: network down version-monitor: npm lookup for "jest" failed: network down version-monitor: npm lookup for "lodash" failed: rate limited version-monitor: npm lookup for "lodash" failed: not found version-monitor: npm lookup for "jest" failed: not found PASS tests/version-monitor/detect.test.js detect orchestrator ✓ annotates each update with is_major (1 ms) ✓ runs no detector when every source is disabled (1 ms) ✓ records hard failures so the run is not silently empty ✓ treats a rate limit as soft (no recorded error) ✓ does not record an expected 404 as a hard failure PASS tests/ui/wizard.test.js Wizard ✓ should run all steps in order (1 ms) ✓ should honour the skip() predicate ✓ should pass context to skip() ✓ should default context to empty object ✓ should default steps to empty array ✓ should handle an empty steps array ✓ should throw a clear error when steps is not an array (2 ms) ✓ should omit ANSI formatting in non-TTY mode ✓ should propagate step errors PASS tests/version-monitor/cli.test.js runCli arg handling ✓ prints usage and exits 0 on --help (1 ms) ✓ exits 2 when no mode is given ✓ exits 2 on mutually exclusive modes ✓ exits 2 on an unknown argument runCli --detect ✓ loads config, runs detect, prints JSON ✓ exits 2 when the config is missing ✓ exits 1 but still prints results when a detector errored runCli --report / --apply (stdin) ✓ reads JSON from stdin and prints markdown for --report ✓ reads JSON from stdin and applies for --apply ✓ exits 2 when stdin is not valid JSON (2 ms) PASS tests/scaffolds/cache.test.js clear ✓ removes a populated cache directory (9 ms) ✓ is a no-op on a missing directory (2 ms) parseArgs ✓ extracts the subaction (1 ms) ✓ extracts --cache-dir <path> ✓ extracts --cache-dir=<path> ✓ extracts --dry-run ✓ extracts --help ✓ throws on unexpected positional after the action (1 ms) runCli ✓ clear --cache-dir succeeds on a populated dir (3 ms) ✓ clear --dry-run reports without removing the dir ✓ --help returns 0 and prints usage (1 ms) ✓ no subaction returns 1 and prints usage ✓ unknown subaction returns 1 and writes to stderr PASS tests/ui/prompts.test.js text ✓ should return the user input (1 ms) ✓ should return defaultValue when input is empty ✓ should trim whitespace from input ✓ should retry when validation fails (1 ms) ✓ should accept a string message shortcut ✓ should propagate CancelledError from readLine (2 ms) confirm ✓ should return true for "y" (1 ms) ✓ should return true for "yes" ✓ should return false for "n" ✓ should return defaultValue on empty input (1 ms) ✓ should default to false when no defaultValue ✓ should accept a string message shortcut ✓ should propagate CancelledError from readLine (1 ms) password ✓ should fall back to readLine in non-TTY (1 ms) ✓ should accept a string message shortcut ✓ should reject with CancelledError on Ctrl+C (6 ms) PASS tests/version-monitor/wp-cli.test.js wp-cli detector ✓ reports an update when a newer release exists (1 ms) ✓ reports nothing when already on the latest release ✓ makes no request when no wp-cli version is pinned PASS tests/a11y/resolve-bin.test.js findInNodeModules ✓ finds a directly installed binary as local (1 ms) ✓ finds a hoisted binary in an ancestor as hoisted (3 ms) ✓ returns null when no installed copy exists resolveBin ✓ falls back to npx --no-install when nothing is installed (1 ms) detectBin ✓ reports available with a trimmed version when the probe succeeds (1 ms) ✓ reports unavailable when the probe throws PASS tests/version-monitor/container.test.js container helpers ✓ splits image references and skips digest pins (1 ms) ✓ maps official images to library/* and rejects other registries ✓ picks the newest tag of the same numeric shape (4 ms) ✓ collects images past --platform flags and across multi-stage FROMs (1 ms) container detector ✓ reports updates for Dockerfile and devcontainer images (6 ms) ✓ reports nothing when the current tag is newest PASS tests/version-monitor/semver.test.js semver.splitVersion ✓ separates a range prefix from the numeric core ✓ separates a v tag prefix (1 ms) ✓ returns an empty core for non-numeric specs semver.parse ✓ fills missing minor/patch with zero ✓ captures the pre-release tag semver.compareStable / gt ✓ ranks by major, then minor, then patch ✓ ignores prefixes and pre-release tags semver.isMajorBump ✓ is true only when the major increases semver.isPreRelease ✓ detects pre-release suffixes semver.formatLatest ✓ re-attaches the current spec prefix to the latest core (1 ms) PASS tests/ui/report.test.js report helpers ✓ prints a success line with a + marker ✓ prints warn and error lines with their markers ✓ prints a heading with a leading blank line ✓ prints an indented info line (1 ms) ✓ renders a key/value table with a title and box borders ✓ skips empty tables PASS tests/version-monitor/node.test.js node detector ✓ bumps .nvmrc to the newest LTS in the same major ✓ preserves the range prefix in package.json engines ✓ reports nothing when already on the newest LTS (1 ms) PASS tests/a11y/urls.test.js resolveUrls ✓ reads string and { url } entries from the default config ✓ a custom --config path is resolved relative to cwd (empty urls -> ENOURLS) (1 ms) ✓ a missing config throws ENOURLS with the install hint extractUrls ✓ handles strings, objects, and skips junk ✓ returns [] when urls is absent or not an array PASS tests/ui/style.test.js style ✓ wraps text in ANSI codes when stdout is a TTY ✓ returns plain text when stdout is not a TTY (no ANSI leaks into logs) (1 ms) ✓ exposes the full set of semantic helpers PASS tests/hooks/install.test.js installHooks ✓ installs both hooks with the executable bit set (58 ms) ✓ injects the version header right after the shebang (33 ms) ✓ skips when a hook already exists, prints the right reason (46 ms) ✓ detects Husky-managed hooks and tags the skip reason (44 ms) ✓ --force overwrites existing hooks (61 ms) ✓ --dry-run plans without touching the filesystem (67 ms) ✓ throws a clear error outside a git repository (20 ms) ✓ resolves the hooks dir when .git is a file (separate git dir) (75 ms) ✓ onConflict callback receives the conflict info per template (96 ms) ✓ onConflict returning true overwrites the existing hook (102 ms) ✓ --force bypasses the onConflict callback entirely (66 ms) ✓ --dry-run reports skips for conflicts without consulting onConflict (68 ms) ✓ onBeforeWrite / onAfterWrite fire around each successful install (69 ms) injectVersionHeader ✓ inserts the header between the shebang and the body ✓ returns the body unchanged when there is no newline looksLikeHusky ✓ matches the `.husky/` directory marker ✓ matches the husky.sh sourcing marker ✓ returns false for arbitrary hook scripts install-hooks runCli ✓ --help exits 0 and prints usage (3 ms) ✓ -h exits 0 and prints usage ✓ unknown flag exits 2 with a stderr message (1 ms) ✓ runs end-to-end inside a git repo, prints `installed` lines (32 ms) ✓ --dry-run prints planned actions, writes nothing (29 ms) ✓ outside a git repo exits 1 with a clear stderr message (9 ms) ✓ non-TTY conflict skips without prompting (CI-safe) (28 ms) PASS tests/hooks/templates.test.js commit-msg template ✓ accepts "feat: add Logger" (3 ms) ✓ accepts "fix: handle empty CHANGELOG" (1 ms) ✓ accepts "docs: update README" ✓ accepts "feat(utilities): add Logger" ✓ accepts "fix(release): handle empty CHANGELOG" ✓ accepts "feat(ui)!: breaking change to wizard" ✓ accepts "chore(deps): bump eslint" ✓ accepts "ci(detect-changes): cover edge case" ✓ accepts "refactor(scaffolds): inline registry scan" ✓ accepts "revert: feat(ui): add wizard" ✓ accepts "revert(release): restore prior tag" ✓ rejects "wip" (1 ms) ✓ rejects "WIP: x" ✓ rejects "feat:" ✓ rejects "feat: " ✓ rejects "feat(): no scope chars allowed empty" ✓ rejects "random commit message" ✓ rejects "Feat: capitalised type" ✓ rejects "feature: not in type list" ✓ shell bypasses cover merge / revert / fixup / squash commit-msg template (end-to-end against /bin/sh) ✓ exits 0 for a valid Conventional Commit subject (12 ms) ✓ exits 1 for an invalid subject and prints guidance (14 ms) ✓ exits 0 for merge commits (10 ms) ✓ exits 0 for revert commits (12 ms) ✓ exits 0 for fixup! commits (9 ms) pre-commit template ✓ no-ops when package.json is absent (5 ms) ✓ no-ops when package.json has no lint:staged script (122 ms) ✓ does not false-positive when "lint:staged" appears only as a string (154 ms) ✓ runs the lint:staged script when defined (248 ms) ✓ propagates a non-zero exit from the lint:staged script (209 ms) shellcheck (optional) ✓ commit-msg passes shellcheck -s sh (72 ms) ✓ pre-commit passes shellcheck -s sh (23 ms) Test Suites: 54 passed, 54 total Tests: 728 passed, 728 total Snapshots: 0 total Time: 1.335 s, estimated 2 s Ran all test suites.Acceptance criteria
Runtime behavior
wp-tooling a11yruns the pa11y accessibility check locally (using thepa11y-cicommand-line tool) and emits the normalized report in bothtextandjson.wp-tooling add setup/pa11yhint is shown when it is missing;pa11y-ciis never added to@rtcamp/wp-toolingdependencies.--dry-runruns nothing.Code quality
Housekeeping
accessibilityskill (SKILL.md + evals) is added and auto-validated bytests/skills/evals-json.test.js.setup/claude-skillsscaffold copies the skill (SKILL.md + evals), and the bundled scaffold-manifest tests pass.skills/README.mdlists the skill, and aCHANGELOG.mdentry is added under## Unreleased..claude/issues/34-a11y-skill.mdupdated with final state.Reviewer notes
--urlflag by design: pa11y-ci merges cwd-config URLs with positional URLs, so a flag cannot reliably override the config. The config is the single source of truth.setup/pa11yas one way to obtain a config, not a requirement; a project with its own pa11y setup works as-is.Full decision log and verification history for this work lives in
.claude/issues/34-a11y-skill.mdon this branch.