v0.2.0
Two big additions, both targeting the write/build stage of the
workflow (T1–T6) where rendered-DOM scan can't reach: source-level
lint and file:// build-output audit.
Added — lint command (source-level static analysis)
- New
a11y-moda lint <paths...>subcommand. Walks JSX/TSX/TS/JS/HTML
source via tree-sitter, runs deterministic AST checks. No
LLM/VLM — fast, offline, repeatable. Complements rendered-DOM
scan(different signal, same rule_id namespace). - 50 rules ported from the scan rule set (out of ~77 lintable),
spanning: image-alt family, page metadata, forms structure,
ARIA roles/labels, navigation/landmark, link text, headings,
tables, lang attributes, deprecated tags, keyboard handlers,
inline style, RTL/bidi, viewport, media controls, dialog/carousel
patterns, focus visibility. - Three-tier status —
fail(deterministic violation),caveat
(likely violation but needs human/runtime check; e.g. wrapper
components with{...spread}),info(style/preference). Lets
AI agents decide which to act on;--fail-onlyfilters to fail
tier;--strictexits non-zero on any issue. --exclude PATTERN(repeatable, gitignore-style globs); built-in
excludes fornode_modules/.next/dist/build/.git
/.cacheetc..gitignorerespected by default; opt out with
--no-gitignore.- Output formats:
--format json|md,-o FILEhonored same as
scan/site. - Wrapper-component heuristic — capital-first JSX tags
(<Button>,<Dialog>) downgrade keyboard/structure violations
tocaveat, since shadcn/Radix/HeadlessUI commonly delegate
accessibility to the underlying primitive. - Decorative-image heuristic —
<img alt="">paired with explicit
role="presentation"/role="none"/aria-hidden="true"is
recognised as intentional and skipped silently. - New runtime dependencies:
tree-sitter(>=0.23,<0.26),
tree-sitter-typescript,tree-sitter-html,pathspec.
Added — file:// build-output audit
--allow-fileflag on bothscanandsite. Permitsfile://
URLs and accepts plain filesystem paths (./index.html,
D:\dist\,/var/www/site/). Off by default so a redirect from
a public site can't trick the scanner into reading local files.A11Y_ALLOW_FILE=1environment variable equivalent.- Filesystem-walk discovery for
sitemode when target is a
file://URL or directory. Recursively finds all*.html/
*.htmfiles, sorts deterministically, respects--max-pages/
--exclude-folder. Sitemap and BFS link-crawl don't apply
(build output has no sitemap; following<a href>from local
files is not what users expect). crawler.discover_filesystem(start_url, ...)public function.- Relative paths (
./out/,dist/index.html) and Windows
backslash paths (D:\dist\index.html) auto-resolve to absolute
file://URIs when--allow-fileis on. fetcher._read_local_file()for the static path;fetch_with_page
(Playwright) handlesfile://natively, no extra code needed.
Notes
--renderworks withfile://— Playwright loads the file URL
natively; all probes (contrast, focus, tab walk, form simulation,
screenshots) operate on the rendered local DOM._security.is_safe_http_url()gains anallow_fileparameter
(defaults to env var). Existing callers pass through unchanged.- Workflow positioning —
lintcovers T1–T3 (write/edit/save),
file://scan covers T4–T6 (build/preview/pre-deploy), HTTP scan
covers T7+ (staging/prod). All share the MODA rule_id namespace.
Complements rather than replaceseslint-plugin-jsx-a11y(no
MODA rule_id mapping) and DopplerKuoa11y-tw-audit-skill(LLM
at write-time). - Lint rule files live at
src/a11y_moda/lint/codes/<topic>/<RULE_ID>.py
with auto-discovery (one file per rule_id, mirrorsrules/codes/
layout).