a11y-fixer 0.1.0__tar.gz
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.
- a11y_fixer-0.1.0/CHANGELOG.md +85 -0
- a11y_fixer-0.1.0/LICENSE +21 -0
- a11y_fixer-0.1.0/MANIFEST.in +6 -0
- a11y_fixer-0.1.0/PKG-INFO +364 -0
- a11y_fixer-0.1.0/README.md +328 -0
- a11y_fixer-0.1.0/a11y.toml.example +30 -0
- a11y_fixer-0.1.0/a11y_fixer.egg-info/PKG-INFO +364 -0
- a11y_fixer-0.1.0/a11y_fixer.egg-info/SOURCES.txt +56 -0
- a11y_fixer-0.1.0/a11y_fixer.egg-info/dependency_links.txt +1 -0
- a11y_fixer-0.1.0/a11y_fixer.egg-info/entry_points.txt +3 -0
- a11y_fixer-0.1.0/a11y_fixer.egg-info/requires.txt +13 -0
- a11y_fixer-0.1.0/a11y_fixer.egg-info/top_level.txt +2 -0
- a11y_fixer-0.1.0/a11y_fixer.py +1674 -0
- a11y_fixer-0.1.0/a11y_pr.py +220 -0
- a11y_fixer-0.1.0/evals/README.md +86 -0
- a11y_fixer-0.1.0/evals/fixtures/already-accessible.expected.json +8 -0
- a11y_fixer-0.1.0/evals/fixtures/already-accessible.input.jsx +15 -0
- a11y_fixer-0.1.0/evals/fixtures/anchor-no-content.expected.json +9 -0
- a11y_fixer-0.1.0/evals/fixtures/anchor-no-content.input.jsx +4 -0
- a11y_fixer-0.1.0/evals/fixtures/anchor-no-href.expected.json +7 -0
- a11y_fixer-0.1.0/evals/fixtures/anchor-no-href.input.jsx +7 -0
- a11y_fixer-0.1.0/evals/fixtures/decorative-img.expected.json +10 -0
- a11y_fixer-0.1.0/evals/fixtures/decorative-img.input.jsx +3 -0
- a11y_fixer-0.1.0/evals/fixtures/div-as-button.expected.json +10 -0
- a11y_fixer-0.1.0/evals/fixtures/div-as-button.input.jsx +10 -0
- a11y_fixer-0.1.0/evals/fixtures/handler-drop-risk.expected.json +6 -0
- a11y_fixer-0.1.0/evals/fixtures/handler-drop-risk.input.jsx +10 -0
- a11y_fixer-0.1.0/evals/fixtures/iframe-no-title.expected.json +10 -0
- a11y_fixer-0.1.0/evals/fixtures/iframe-no-title.input.jsx +3 -0
- a11y_fixer-0.1.0/evals/fixtures/img-no-alt.expected.json +10 -0
- a11y_fixer-0.1.0/evals/fixtures/img-no-alt.input.jsx +3 -0
- a11y_fixer-0.1.0/evals/fixtures/injection-in-comment.expected.json +10 -0
- a11y_fixer-0.1.0/evals/fixtures/injection-in-comment.input.jsx +8 -0
- a11y_fixer-0.1.0/evals/fixtures/injection-in-text.expected.json +7 -0
- a11y_fixer-0.1.0/evals/fixtures/injection-in-text.input.jsx +14 -0
- a11y_fixer-0.1.0/evals/fixtures/invalid-aria-prop.expected.json +11 -0
- a11y_fixer-0.1.0/evals/fixtures/invalid-aria-prop.input.jsx +16 -0
- a11y_fixer-0.1.0/evals/fixtures/label-no-control.expected.json +9 -0
- a11y_fixer-0.1.0/evals/fixtures/label-no-control.input.jsx +13 -0
- a11y_fixer-0.1.0/evals/fixtures/mouse-only-handlers.expected.json +7 -0
- a11y_fixer-0.1.0/evals/fixtures/mouse-only-handlers.input.jsx +11 -0
- a11y_fixer-0.1.0/evals/fixtures/positive-tabindex.expected.json +8 -0
- a11y_fixer-0.1.0/evals/fixtures/positive-tabindex.input.jsx +11 -0
- a11y_fixer-0.1.0/evals/fixtures/redundant-role.expected.json +11 -0
- a11y_fixer-0.1.0/evals/fixtures/redundant-role.input.jsx +12 -0
- a11y_fixer-0.1.0/evals/run.py +380 -0
- a11y_fixer-0.1.0/lint/.gitignore +2 -0
- a11y_fixer-0.1.0/lint/axe.mjs +138 -0
- a11y_fixer-0.1.0/lint/eslint.config.mjs +19 -0
- a11y_fixer-0.1.0/lint/lint.mjs +49 -0
- a11y_fixer-0.1.0/lint/package-lock.json +3908 -0
- a11y_fixer-0.1.0/lint/package.json +19 -0
- a11y_fixer-0.1.0/pyproject.toml +57 -0
- a11y_fixer-0.1.0/requirements.txt +4 -0
- a11y_fixer-0.1.0/setup.cfg +4 -0
- a11y_fixer-0.1.0/tests/test_a11y_fixer.py +1419 -0
- a11y_fixer-0.1.0/tests/test_a11y_pr.py +184 -0
- a11y_fixer-0.1.0/tests/test_evals.py +235 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project aims to
|
|
5
|
+
adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `--fail-on STATUSES` — narrow the exit-1 gate to specific outcomes (e.g.
|
|
12
|
+
`invalid`, or `would-fix,invalid,residual-violations`). Overrides the `--check`
|
|
13
|
+
gate; a file that errored still exits 1.
|
|
14
|
+
- Eval harness `--axe` — renders each opted-in fixture's fixed component to
|
|
15
|
+
static markup and runs axe-core against it in jsdom (`lint/axe.mjs`), so a
|
|
16
|
+
fixture can assert the fix is actually clean to assistive tech, not just to
|
|
17
|
+
the linter. Fixtures opt in with `expect_axe_clean` / `axe_props`; adds an
|
|
18
|
+
`axe_clean_rate` metric.
|
|
19
|
+
- Eval corpus grown from 7 to 15 fixtures: `invalid-aria-prop`, `iframe-no-title`,
|
|
20
|
+
`anchor-no-content`, `redundant-role`, `mouse-only-handlers`, a second
|
|
21
|
+
false-positive guard `decorative-img` (a correct empty `alt=""` the model must
|
|
22
|
+
not "helpfully" fill in), and two `prompt-injection` fixtures that verify
|
|
23
|
+
override instructions embedded in a JS comment or in rendered JSX text don't
|
|
24
|
+
steer the model off its task (SYSTEM_PROMPT constraint #7).
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `--workers > 1` now shares a single rate-limit cooldown across all worker
|
|
29
|
+
threads: a provider 429 pauses the whole pool (with jitter) instead of each
|
|
30
|
+
thread running its own blind backoff into the same quota. Single-worker runs
|
|
31
|
+
are unchanged.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- The sdist no longer vendors `lint/node_modules/` (a stray `*.json` glob in
|
|
36
|
+
`MANIFEST.in` was pulling in every nested `package.json`). It ships only the
|
|
37
|
+
runner scripts, `package.json`, and `package-lock.json`; run `npm install` in
|
|
38
|
+
`lint/` after install as before.
|
|
39
|
+
|
|
40
|
+
## [0.1.0] - 2026-09-08
|
|
41
|
+
|
|
42
|
+
First tagged release. The agent went from a "trust the model and write to disk"
|
|
43
|
+
script to a checked-remediation tool with CI, caching, and an eval harness.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **Structural safety gate** — every fix is parsed as JSX/TSX (tree-sitter) and
|
|
48
|
+
compared against a fingerprint of the original (event handlers, hook calls,
|
|
49
|
+
identifiers). A fix that fails to parse or drops behavior is rejected as
|
|
50
|
+
`invalid`, never written.
|
|
51
|
+
- **Repair pass** — a rejected fix is re-prompted once with the specific reason
|
|
52
|
+
(`--repair-attempts`, default 1) before being reported.
|
|
53
|
+
- **`--lint` feedback loop** — bundled `eslint-plugin-jsx-a11y` runs in-process,
|
|
54
|
+
its violations are fed into the prompt, and the model's output is re-linted
|
|
55
|
+
(`--lint-rounds`, default 2). Lint-clean files skip the model entirely.
|
|
56
|
+
- **CI modes** — `--check` (exit 1 on any residual issue, never writes),
|
|
57
|
+
`--patch FILE` (unified diff), `--json-summary FILE` (machine-readable run
|
|
58
|
+
report).
|
|
59
|
+
- **`--since REF`** — restrict the run to files changed vs a git ref (changed
|
|
60
|
+
tracked files plus new untracked ones).
|
|
61
|
+
- **Content-hash cache** (`.a11y-cache.json`) keyed by a prompt fingerprint
|
|
62
|
+
(model, provider, base URL, system prompt, lint ruleset); concurrency-safe
|
|
63
|
+
merge-on-save behind a lock file.
|
|
64
|
+
- **Provider abstraction** — Groq (default), OpenAI, or any OpenAI-compatible
|
|
65
|
+
server via `--provider` / `--base-url`. Deterministic decoding, truncation
|
|
66
|
+
detection, retry with `Retry-After` awareness.
|
|
67
|
+
- **`a11y.toml`** project config (auto-discovered) — design-system primitives to
|
|
68
|
+
leave alone, extra prompt rules, ignore globs.
|
|
69
|
+
- **Observability** — `--log FILE` writes a line-flushed JSONL event stream;
|
|
70
|
+
`--verbose` / `--quiet`; `--timeout`; graceful SIGINT (drains in-flight files,
|
|
71
|
+
saves, exits 130).
|
|
72
|
+
- **PR-bot** (`a11y-fixer-pr`) — runs the fixer, opens or updates a pull request
|
|
73
|
+
with a per-file summary via `gh`.
|
|
74
|
+
- **Eval harness** (`evals/run.py`) — golden fixtures scored on status,
|
|
75
|
+
lint-cleanliness, and identifier preservation; aggregate `fix_rate`,
|
|
76
|
+
`false_positive_rate`, `regression_rate`.
|
|
77
|
+
- Faithful write-back of original encoding and line endings (BOM / CRLF);
|
|
78
|
+
machine-readable outputs are always LF.
|
|
79
|
+
- `--version`.
|
|
80
|
+
- Packaging (`pyproject.toml`, console scripts `a11y-fixer` and
|
|
81
|
+
`a11y-fixer-pr`), 138-test pytest suite, and GitHub Actions workflows for the
|
|
82
|
+
PR check, the PR-bot, and the eval.
|
|
83
|
+
|
|
84
|
+
[Unreleased]: https://github.com/arysarin/a11y-fixer/compare/v0.1.0...HEAD
|
|
85
|
+
[0.1.0]: https://github.com/arysarin/a11y-fixer/releases/tag/v0.1.0
|
a11y_fixer-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 a11y-fixer authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
include LICENSE README.md CHANGELOG.md requirements.txt a11y.toml.example MANIFEST.in
|
|
2
|
+
recursive-include lint *.mjs
|
|
3
|
+
include lint/package.json lint/package-lock.json lint/eslint.config.mjs lint/.gitignore
|
|
4
|
+
prune lint/node_modules
|
|
5
|
+
recursive-include tests *.py
|
|
6
|
+
recursive-include evals *.py *.json *.jsx *.tsx *.toml *.md
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: a11y-fixer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LLM accessibility remediation agent for JSX/TSX with structural safety checks
|
|
5
|
+
Author: a11y-fixer authors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/arysarin/a11y-fixer
|
|
8
|
+
Project-URL: Repository, https://github.com/arysarin/a11y-fixer
|
|
9
|
+
Project-URL: Changelog, https://github.com/arysarin/a11y-fixer/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/arysarin/a11y-fixer/issues
|
|
11
|
+
Keywords: accessibility,a11y,wcag,aria,jsx,react,llm,codemod
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: groq>=0.11.0
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
28
|
+
Requires-Dist: tree-sitter>=0.21
|
|
29
|
+
Requires-Dist: tree-sitter-typescript>=0.23
|
|
30
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
33
|
+
Provides-Extra: openai
|
|
34
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# a11y-fixer
|
|
38
|
+
|
|
39
|
+
An accessibility remediation agent for React JSX/TSX. It scans a file or
|
|
40
|
+
directory, sends each component through an LLM (Groq, OpenAI, or any
|
|
41
|
+
OpenAI-compatible endpoint) with a WCAG 2.0/2.2 + ARIA APG system prompt,
|
|
42
|
+
**validates the result structurally**, and then either rewrites the source,
|
|
43
|
+
emits a patch, or reports for CI.
|
|
44
|
+
|
|
45
|
+
The validation step is what makes the output safe to apply unattended: model
|
|
46
|
+
output is parsed as JSX/TSX and compared against the original, and a fix is
|
|
47
|
+
rejected if it fails to parse or drops an event handler or hook call.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install a11y-fixer
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That gives you the core agent — structural gate, `--write` / `--patch` /
|
|
56
|
+
`--check` / `--fail-on`, all providers, the cache, `--since`. The optional
|
|
57
|
+
eslint-jsx-a11y feedback loop (`--lint`) and the eval harness's axe pass
|
|
58
|
+
(`--axe`) run a bundled Node package that isn't in the wheel; for those, work
|
|
59
|
+
from a clone:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/arysarin/a11y-fixer && cd a11y-fixer
|
|
63
|
+
pip install -e ".[dev]" # editable install + pytest
|
|
64
|
+
npm --prefix lint install # only needed for --lint / --axe
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Set an API key via environment or a local `.env` (the variable depends on the
|
|
68
|
+
provider — `GROQ_API_KEY` by default, `OPENAI_API_KEY` for `--provider openai`):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
echo "GROQ_API_KEY=gsk-..." > .env
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# dry run: print diffs, change nothing
|
|
78
|
+
a11y-fixer src/
|
|
79
|
+
|
|
80
|
+
# apply validated fixes in place (writes .bak alongside each changed file)
|
|
81
|
+
a11y-fixer src/ --write
|
|
82
|
+
|
|
83
|
+
# CI: check only what this PR changed
|
|
84
|
+
a11y-fixer src/ --since origin/main --check
|
|
85
|
+
|
|
86
|
+
# write validated fixes to a patch instead of touching the tree
|
|
87
|
+
a11y-fixer src/ --patch a11y-fixes.patch
|
|
88
|
+
|
|
89
|
+
# CI gate: exit non-zero if any file still has an accessibility issue
|
|
90
|
+
a11y-fixer src/ --check --json-summary a11y-summary.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Output modes
|
|
94
|
+
|
|
95
|
+
| Mode | Flag | Writes source? | Exit on findings |
|
|
96
|
+
| --- | --- | --- | --- |
|
|
97
|
+
| Dry run | *(default)* | no | 0 |
|
|
98
|
+
| Write | `--write` | yes (validated only) | 0 |
|
|
99
|
+
| Patch | `--patch FILE` | no (diff to `FILE`) | 0 |
|
|
100
|
+
| Check | `--check` | no | 1 |
|
|
101
|
+
|
|
102
|
+
`--write` is ignored when combined with `--check` or `--patch`.
|
|
103
|
+
|
|
104
|
+
### Exit codes
|
|
105
|
+
|
|
106
|
+
- `0` — clean, or fixes applied/emitted successfully
|
|
107
|
+
- `1` — `--check` (or `--fail-on`) found a gated outcome, **or** a file errored
|
|
108
|
+
(API failure, truncated response, unreadable file)
|
|
109
|
+
- `2` — bad invocation (missing API key, path not found, `--workers < 1`)
|
|
110
|
+
- `130` — interrupted with Ctrl-C (partial results saved)
|
|
111
|
+
|
|
112
|
+
### Options
|
|
113
|
+
|
|
114
|
+
| Flag | Default | Purpose |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `--ext` | `jsx,tsx` | comma-separated extensions to scan |
|
|
117
|
+
| `--exclude` | `node_modules,.git,dist,build,.next` | directory names to skip |
|
|
118
|
+
| `--since REF` | — | only process files changed vs a git ref (e.g. `origin/main`) |
|
|
119
|
+
| `--model` | `openai/gpt-oss-120b` | Groq model id |
|
|
120
|
+
| `--write` | off | apply fixes in place |
|
|
121
|
+
| `--no-backup` | off | skip `.bak` files when writing |
|
|
122
|
+
| `--strict` | off | also block a fix that drops *any* identifier, not just handlers/hooks |
|
|
123
|
+
| `--repair-attempts N` | `1` | re-prompt a fix that fails the structural gate, up to N times (`0` disables) |
|
|
124
|
+
| `--lint` | off | run eslint-jsx-a11y, target its violations, verify the fix (needs Node) |
|
|
125
|
+
| `--lint-rounds N` | `2` | max model passes to clear lint violations |
|
|
126
|
+
| `--lint-config FILE` | bundled a11y config | ESLint config to lint with |
|
|
127
|
+
| `--config FILE` | auto-discover `a11y.toml` | project config (see above) |
|
|
128
|
+
| `--no-config` | off | skip config auto-discovery |
|
|
129
|
+
| `--provider` | `groq` | `groq`, `openai`, or `openai-compatible` |
|
|
130
|
+
| `--model` | provider default | model id (`openai/gpt-oss-120b` for groq, `gpt-4o-mini` for openai) |
|
|
131
|
+
| `--base-url` | — | API base URL; required for `openai-compatible` |
|
|
132
|
+
| `--check` | off | CI mode: never write, exit 1 on findings |
|
|
133
|
+
| `--fail-on STATUSES` | — | narrow the exit-1 gate to specific outcomes, e.g. `invalid` or `would-fix,invalid,residual-violations` (overrides `--check`'s gate; a file that errored still exits 1) |
|
|
134
|
+
| `--patch FILE` | — | write validated fixes to `FILE` as a unified diff |
|
|
135
|
+
| `--json-summary FILE` | — | write a machine-readable run summary |
|
|
136
|
+
| `--cache FILE` | `.a11y-cache.json` | cache file for skipping unchanged files |
|
|
137
|
+
| `--no-cache` | off | ignore and don't update the cache |
|
|
138
|
+
| `--api-key` | `$GROQ_API_KEY` | Groq key override |
|
|
139
|
+
| `--workers` | `1` | files processed concurrently (with `> 1`, all workers share one rate-limit cooldown, so a provider 429 pauses the pool instead of each thread backing off blind) |
|
|
140
|
+
| `--timeout SECONDS` | `120` | per model request / lint subprocess timeout (`0` = none) |
|
|
141
|
+
| `--log FILE` | — | append a JSONL event log |
|
|
142
|
+
| `--quiet` / `--verbose` | — | suppress per-file output / show lint+repair progress |
|
|
143
|
+
|
|
144
|
+
## Observability
|
|
145
|
+
|
|
146
|
+
`--log FILE` appends one JSON object per line as the run proceeds — it survives
|
|
147
|
+
Ctrl-C and crashes, since each line is flushed immediately:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
{"event":"run_start","mode":"CHECK","model":"...","files":42, ...}
|
|
151
|
+
{"event":"file_start","path":"src/NavBar.jsx"}
|
|
152
|
+
{"event":"lint","path":"src/NavBar.jsx","phase":"initial","violations":2}
|
|
153
|
+
{"event":"model_call","path":"src/NavBar.jsx","kind":"lint_round","round":1,"prompt_tokens":970,"completion_tokens":734}
|
|
154
|
+
{"event":"file_done","path":"src/NavBar.jsx","status":"would-fix","seconds":7.6,"violations_before":2,"violations_after":0,"repair_attempts":1, ...}
|
|
155
|
+
{"event":"run_end","duration_seconds":8.6,"interrupted":false,"scanned":42, ...}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Ctrl-C lets in-flight files finish, saves the cache and prints the summary, then
|
|
159
|
+
exits `130`. A second Ctrl-C bails immediately. Parallel runs sharing a
|
|
160
|
+
`.a11y-cache.json` merge on save (each writes only the entries it touched) behind
|
|
161
|
+
a best-effort lock file, so they don't clobber each other.
|
|
162
|
+
|
|
163
|
+
## Linter feedback loop (`--lint`)
|
|
164
|
+
|
|
165
|
+
By default the agent trusts the model to find and fix accessibility issues.
|
|
166
|
+
`--lint` closes that loop with a real linter:
|
|
167
|
+
|
|
168
|
+
1. Run `eslint-plugin-jsx-a11y` over the file.
|
|
169
|
+
2. If it's clean, **skip the model entirely** (no tokens spent).
|
|
170
|
+
3. Otherwise pass the exact violations into the prompt, get a fix, and re-lint it.
|
|
171
|
+
4. Repeat up to `--lint-rounds` (default 2) until clean or give up.
|
|
172
|
+
5. The structural gate (parse, handlers/hooks) still runs on the final result.
|
|
173
|
+
|
|
174
|
+
The run summary reports `a11y lint N -> M violations`, the JSON summary carries
|
|
175
|
+
per-file `violations_before` / `violations_after` / `lint_rounds`, and under
|
|
176
|
+
`--check` any file left with violations fails CI.
|
|
177
|
+
|
|
178
|
+
**Setup** — needs Node.js and a one-time install of the bundled ESLint:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
cd lint && npm install # installs eslint + jsx-a11y + the TS parser
|
|
182
|
+
a11y-fixer src/ --lint --check
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`--lint-config FILE` points at your project's own ESLint config instead of the
|
|
186
|
+
bundled a11y-only one. The bundled rule set is folded into the cache key, so
|
|
187
|
+
changing it re-checks every file.
|
|
188
|
+
|
|
189
|
+
## Project config
|
|
190
|
+
|
|
191
|
+
Drop an `a11y.toml` (or `.a11y.toml`) in the directory you run from — it's
|
|
192
|
+
auto-discovered (`--config FILE` to point elsewhere, `--no-config` to skip). See
|
|
193
|
+
[a11y.toml.example](a11y.toml.example).
|
|
194
|
+
|
|
195
|
+
```toml
|
|
196
|
+
primitives = ["Button", "Link", "Modal"] # already-accessible components; leave them alone
|
|
197
|
+
rules = ["Our <Icon> is decorative unless a title prop is set."]
|
|
198
|
+
ignore = ["**/*.stories.tsx", "src/legacy/**"] # skip these files
|
|
199
|
+
ext = "jsx,tsx" # optional; CLI --ext still wins
|
|
200
|
+
exclude = "node_modules,dist,coverage" # optional; CLI --exclude still wins
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`primitives` and `rules` are injected into the system prompt so the model stops
|
|
204
|
+
adding redundant ARIA to your wrapped components. `ignore` patterns are matched
|
|
205
|
+
(with `fnmatch`) against both the working-dir-relative path and the basename.
|
|
206
|
+
Changing the config changes the prompt, which invalidates the cache.
|
|
207
|
+
|
|
208
|
+
## Providers
|
|
209
|
+
|
|
210
|
+
The model call goes through a small `LLMClient` adapter, so any OpenAI-shaped
|
|
211
|
+
API works:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Groq (default)
|
|
215
|
+
a11y-fixer src/ --check
|
|
216
|
+
|
|
217
|
+
# OpenAI
|
|
218
|
+
a11y-fixer src/ --provider openai --model gpt-4o-mini --check # needs OPENAI_API_KEY
|
|
219
|
+
pip install "a11y-fixer[openai]" # installs the openai SDK
|
|
220
|
+
|
|
221
|
+
# Any OpenAI-compatible server (Ollama, vLLM, LiteLLM, LM Studio, ...)
|
|
222
|
+
a11y-fixer src/ --provider openai-compatible \
|
|
223
|
+
--base-url http://localhost:11434/v1 --model qwen2.5-coder:7b --check
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`--base-url` also lets the `openai-compatible` provider run without an API key
|
|
227
|
+
(for local servers that don't need one). Changing provider, model, or base URL
|
|
228
|
+
invalidates the cache.
|
|
229
|
+
|
|
230
|
+
## Caching
|
|
231
|
+
|
|
232
|
+
Every run records the content hash of each file it verified as **clean**
|
|
233
|
+
(`unchanged`) or **already fixed** (`fixed`) into `--cache` (default
|
|
234
|
+
`.a11y-cache.json`, keyed relative to the working directory). A later run whose
|
|
235
|
+
file content still matches skips the model call entirely and reports it as
|
|
236
|
+
`unchanged (from cache)` with zero tokens.
|
|
237
|
+
|
|
238
|
+
Only known-good outcomes are cached, so the cache can never suppress a pending
|
|
239
|
+
fix, a validation failure, or a diff you need to see — `would-fix`, `invalid`,
|
|
240
|
+
and `error` files are re-processed every run. The whole cache is discarded when
|
|
241
|
+
the model (`--model`) or the system prompt changes, since a stale "clean"
|
|
242
|
+
verdict would no longer be trustworthy.
|
|
243
|
+
|
|
244
|
+
Add `.a11y-cache.json` to `.gitignore` (already done here). Use `--no-cache` in
|
|
245
|
+
environments where you want every file re-checked unconditionally.
|
|
246
|
+
|
|
247
|
+
## How validation works
|
|
248
|
+
|
|
249
|
+
For each file the agent builds a structural fingerprint (via `tree-sitter`) of
|
|
250
|
+
the original and the model output:
|
|
251
|
+
|
|
252
|
+
1. **Parse gate** — if the original parsed cleanly and the fix does not, the fix
|
|
253
|
+
is rejected (`invalid`) and never written.
|
|
254
|
+
2. **Handler / hook preservation** — any `on*` JSX attribute or `use*` hook call
|
|
255
|
+
present in the original but gone from the fix rejects it.
|
|
256
|
+
3. **`--strict`** — additionally rejects if any other identifier drops to zero
|
|
257
|
+
references (noisier: legitimate refactors like `window.location.href = …` →
|
|
258
|
+
`<a href>` trip this).
|
|
259
|
+
|
|
260
|
+
If the original file does not parse, all checks are skipped (no trustworthy
|
|
261
|
+
baseline) and the fix is written as-is under `--write`.
|
|
262
|
+
|
|
263
|
+
**Repair pass** — when the structural gate rejects a fix, it is sent back once
|
|
264
|
+
(configurable with `--repair-attempts N`, default 1, `0` disables) with the exact
|
|
265
|
+
reason (`event handlers removed: onClick`, `does not parse`, …) and re-validated.
|
|
266
|
+
A recovered fix proceeds normally; the summary reports `repair K/N recovered`.
|
|
267
|
+
This costs one extra model call per rejected file.
|
|
268
|
+
|
|
269
|
+
A truncated or empty model response is always an error, never a partial write.
|
|
270
|
+
|
|
271
|
+
File content is treated strictly as code to remediate; the system prompt tells
|
|
272
|
+
the model to ignore any instructions embedded in it (prompt-injection guard),
|
|
273
|
+
and the structural gate above is the real backstop regardless. The eval corpus
|
|
274
|
+
carries `prompt-injection` fixtures (override text in a JS comment and in
|
|
275
|
+
rendered JSX) that check this holds against the live model.
|
|
276
|
+
|
|
277
|
+
Files are read as UTF-8; a byte-order mark and CRLF line endings are detected
|
|
278
|
+
and preserved on write-back. A non-UTF-8 file is reported as an `error` rather
|
|
279
|
+
than guessed at.
|
|
280
|
+
|
|
281
|
+
`--since REF` narrows the run to files that differ from a git ref — tracked
|
|
282
|
+
modifications plus new untracked files, deletions excluded. A run with nothing
|
|
283
|
+
changed exits `0` ("No changed files to process"); an unknown ref or a
|
|
284
|
+
non-git directory exits `2`.
|
|
285
|
+
|
|
286
|
+
## CI
|
|
287
|
+
|
|
288
|
+
See [.github/workflows/a11y.yml](.github/workflows/a11y.yml): runs `--check` on
|
|
289
|
+
pull requests touching `.jsx`/`.tsx`, and a manual `workflow_dispatch` produces
|
|
290
|
+
a fix patch as a build artifact. Requires a `GROQ_API_KEY` repository secret
|
|
291
|
+
(or `OPENAI_API_KEY` if you switch `--provider`).
|
|
292
|
+
|
|
293
|
+
To stop the build only on a fix that failed the safety gate — while letting
|
|
294
|
+
merely-unremediated files through — use `--fail-on`:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
a11y-fixer src/ --since origin/main --lint --fail-on invalid
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Opening a PR with the fixes
|
|
301
|
+
|
|
302
|
+
`a11y_pr.py` (console script `a11y-fixer-pr`) runs the agent and turns the result
|
|
303
|
+
into a pull request. Run it from the repo root with a clean tracked tree:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
a11y-fixer-pr src/ --lint --label accessibility
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
It runs `a11y-fixer src/ --lint --patch … --json-summary …` (any extra args pass
|
|
310
|
+
through), and if there are validated fixes: branches from HEAD (`--branch`,
|
|
311
|
+
default `a11y-fixes`), applies the patch, commits (message carries the lint
|
|
312
|
+
delta), force-pushes, and creates or refreshes a PR through the `gh` CLI. The PR
|
|
313
|
+
body is a per-file table (status, `violations_before → after`, repair passes)
|
|
314
|
+
with token/cost totals and a note that rejected fixes were left out. `--dry-run`
|
|
315
|
+
stops after the local commit; without `gh` it pushes and prints the compare
|
|
316
|
+
instructions. [.github/workflows/a11y-pr.yml](.github/workflows/a11y-pr.yml) wires
|
|
317
|
+
it to `workflow_dispatch`.
|
|
318
|
+
|
|
319
|
+
## Limitations
|
|
320
|
+
|
|
321
|
+
- Without `--lint`, semantic correctness of a fix is not verified — only that it
|
|
322
|
+
parses and keeps handlers/hooks. `--lint` verifies against
|
|
323
|
+
`eslint-plugin-jsx-a11y`; `axe-core` on rendered output would be stronger still.
|
|
324
|
+
- The bundled `lint/` directory ships in source checkouts and editable installs
|
|
325
|
+
(`pip install -e .`); a plain wheel install does not carry it, so run `--lint`
|
|
326
|
+
from a checkout.
|
|
327
|
+
- Each file is processed in isolation; the agent has no knowledge of shared
|
|
328
|
+
design-system components (an `a11y.toml` mitigates this).
|
|
329
|
+
- Cost figures in the summary are estimates from `MODEL_PRICING` in
|
|
330
|
+
`a11y_fixer.py`; update the rates for your model.
|
|
331
|
+
|
|
332
|
+
## Tests
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
pytest
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
`.github/workflows/ci.yml` runs the suite on Python 3.10 / 3.11 / 3.12 on every
|
|
339
|
+
push and PR, plus a smoke check that the bundled `eslint-plugin-jsx-a11y` config
|
|
340
|
+
still flags a known violation and that the package builds with its runtime data
|
|
341
|
+
files (`lint/`, `a11y.toml.example`).
|
|
342
|
+
|
|
343
|
+
## Releasing
|
|
344
|
+
|
|
345
|
+
`a11y_fixer.__version__` is the single source of truth; `pyproject.toml` reads it
|
|
346
|
+
dynamically, so a release is: bump that string, move the `CHANGELOG.md`
|
|
347
|
+
`[Unreleased]` items under a dated version heading, tag `vX.Y.Z`. `a11y-fixer
|
|
348
|
+
--version` reports the installed version.
|
|
349
|
+
|
|
350
|
+
## Evals
|
|
351
|
+
|
|
352
|
+
[evals/](evals/) holds golden fixtures and a scorer for measuring a prompt /
|
|
353
|
+
model / validation change rather than eyeballing it:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
GROQ_API_KEY=... python evals/run.py --lint --json after.json
|
|
357
|
+
GROQ_API_KEY=... python evals/run.py --lint --axe # also render + run axe-core
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Reports `fix_rate`, `false_positive_rate`, `regression_rate`, and (with `--axe`)
|
|
361
|
+
`axe_clean_rate`; exits non-zero below `--threshold` (default 1.0). `--axe`
|
|
362
|
+
renders each opted-in fixture's fixed component and runs axe-core against it in
|
|
363
|
+
jsdom, checking the fix actually helps assistive tech rather than just passing
|
|
364
|
+
the linter. See [evals/README.md](evals/README.md).
|