Part of #1706: autumn a11y verify build-time pass for raw-html escape hatches#1845
Conversation
The typed autumn_web::a11y primitives prove accessible-name obligations at compile time for code that uses them. This adds a build-time pass that covers the escape hatch the type system cannot see: raw `html!` markup that bypasses the typed primitives. `autumn a11y verify` token-scans the `html!` blocks in a project's `.rs` files (mirroring `autumn i18n check`'s descent into macro bodies) and reports raw elements missing an accessible name, reusing the WCAG success criteria, rule ids, and Severity semantics of `autumn check --a11y`: - image-alt (WCAG 1.1.1) — <img> with no alt - label (WCAG 1.3.1 / 3.3.2 / 4.1.2) — form control with no label/aria - button-name (WCAG 4.1.2) — <button> with no name - link-name (WCAG 2.4.4 / 4.1.2) — <a href> with no name Text and `--format json` (a WCAG-keyed conformance manifest) output, a non-zero CI exit code (Serious by default, --strict lowers to Moderate), and a best-effort static scan that skips spliced/dynamic values so a typed-primitive splice like `(Img::new(src, alt))` is never falsely flagged. Adds the docs section and unit plus end-to-end tests. Part of #1706 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 22ea4bc799
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
An unreadable/misspelled/nonexistent PATH made the top-level read_dir error get swallowed into an empty file list, so `run_in` built a zero-findings report and exited 0 — printing PASS and silently disabling the whole audit on a CI path typo. Make the initial scan-root read fatal (mirroring `autumn i18n check`): `scan_project` now returns `io::Result<Scan>` and probes the root with `read_dir`, which surfaces path-absent, not-a-directory, and unreadable alike. `run_in` reports the io error on stderr and exits non-zero. A genuinely empty-but-readable directory still reads fine and passes; nested subdir read errors during recursion stay best-effort as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 746dc85152
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…se-positive An empty <label for=..> was recorded as a valid label association even though it provides no accessible name, causing check_field to suppress the label finding for an unlabeled control. Gate the for/id association on label_provides_name, matching the wrapped-label logic (dynamic bodies still count). An input with a spliced type=(kind) fell through the literal-only exempt branch and was flagged as an unlabeled control. Treat an unresolved type splice as non-failing, consistent with other splice handling. Adds focused unit tests for both fixes plus regression guards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72c8874e29
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The static a11y scanner treated a @match block's arm-list brace group as raw markup, so each arm's Rust pattern (e.g. `input => { .. }`) was parsed as an element and produced a false `label` WCAG finding on valid views. Special-case @match: skip the scrutinee, and for each arm skip the pattern (and any `if <guard>`) up to and including `=>`, then scan only the arm body as markup. Real elements in arm bodies are still flagged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 34ac8b4ec8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two false-report fixes in the raw-html! a11y scanner:
- Dynamic `for` labels (`label for=(id) { }`) set the block-wide
dynamic-for marker without the `label_provides_name` guard the
literal-`for` path already had, so an empty spliced-`for` label made
every static-id control pass as labeled (false negative). Apply the
same guard so only a name-providing label registers the association.
- `skip_control` parsed an `@let` initializer's brace group as markup,
so `@let props = Field { input: true };` misread the Rust field
`input` as an unlabeled `<input>` (false positive). Route `@let`
through a new `skip_let` that consumes the binding to its top-level
`;` without descending into any brace; `@if`/`@for`/`@match` are
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f2b284633
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…verify An icon-only button/link whose only content is an aria-hidden="true" glyph is effectively nameless, but named_content still counted that hidden text, wrongly suppressing the button-name/link-name finding. Skip literal aria-hidden="true" subtrees when deriving the accessible name; a dynamic aria-hidden=(expr) stays conservatively not-hidden. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 806c6c8bb3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…n a11y verify
BUG 1 (false positive): collect_labels dropped top-level (expr) splice
siblings, so a fragment-composed label spliced beside a raw control
(html! { (field_label) input id="email"; }) was treated as absent and
the control wrongly flagged. Node::Dynamic now records whether it is a
genuine splice vs an @-control head; a spliced fragment marks the block
as possibly-labeled, mirroring the dynamic-for convention. @-control
heads do not set the marker, so unrelated static controls stay flagged.
BUG 2 (false negative + false positive): the label validity check used a
naive text-any/dynamic walk that accepted aria-hidden text and ignored
child <img alt>. label_provides_name now reuses content_provides_name --
the same aria-hidden-excluding, img-alt-including, dynamic-counting walk
used for buttons/links -- so a hidden-only label is flagged and an
img-alt label is clean. Removes the now-dead subtree_has_text/dynamic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 43326c19ba
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…verify
Two follow-ons to the Node::Dynamic { splice } parser change:
- BUG (false negative): the accessible-name walk counted every Node::Dynamic
as a name, so a control that contains only an @-control HEAD (splice: false,
its branch bodies already parsed out) — e.g. `button { @if disabled { } }` or
an empty `label { @if show { } }` — wrongly passed. content_provides_name now
counts only a genuine (expr)/[..] splice (splice: true), matching the
distinction already used by collect_labels.
- BUG (false positive): after a parsed control block, skip_control treated ANY
following @ token as an @else continuation, so a sibling @let never reached
skip_let and its `Field { input: true }` struct literal was misparsed as
markup, reporting a bogus unlabeled <input>. The continuation is now gated on
@else only; any other @-control is returned to parse_nodes for its proper
handler. Genuine @if … @else … / @else if chains still parse.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4820c93f52
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… remaining edge cases
Address two reviewer findings plus proactively-found genuine defects in the
best-effort maud token-walker and ARIA accessible-name logic — every change
either removes a false positive on valid markup or restores a missed real
defect, never the reverse.
Parser (false positives on valid markup):
- skip_control: handle `@if`/`@while` block-expression conditions
(`@if { cond } { body }`, condition parsed with parse_without_eager_brace)
and `let`/`for` struct-pattern braces (`@for Foo { x } in xs`,
`@if let Foo { x } = e`); the body is the final brace of the clause. Also
covers `@while`, `@else if` chains, and block-expr conditions across `@else`.
- skip_match: skip a leading block-expression scrutinee (`@match { x } { arms }`).
- parse_shorthand: consume dynamic (`.(expr)`/`#(expr)`) and toggled
(`.class[cond]`) shorthand values so the element does not end early and
orphan its `{ … }` body; record `#(expr)` as a dynamic id.
Accessible-name / rules:
- collect_labels + walk: scope splice-fragment label suppression to a control's
DIRECT sibling list instead of the whole block, so an unrelated nested splice
(`p { (user.name) }`) no longer disables the unlabeled-control rule.
- aria-hidden on the control itself (and its subtree) exempts it from the rules.
- `title` counts as a last-resort accessible name for button/link/form controls.
- `<img>` is named by aria-label/aria-labelledby/title or a presentational
role, not only by `alt`.
- a descendant's own aria-label/aria-labelledby/title contributes to the
name-from-content walk.
Adds focused unit tests for each fix (false-positive gone AND a real defect in
the same construct still caught).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38b191c766
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Fix two false positives in `autumn a11y verify`'s maud shorthand parsing:
- A leading `.`/`#` shorthand with no explicit tag (`.input { }`,
`#button { }`) is maud's implicit `<div>`; the scanner dropped the
punctuation and read the class/id token as a phantom `<input>`/`<button>`.
It is now parsed as a `div` element (no rule), while its body is still
scanned for real controls.
- A markup-valued class shorthand (`button.@if c { "x" } { "Save" }`) ended
the element before its real `{ … }` body, so the control was analyzed as
empty and falsely flagged. The `@`-control (and braced markup group) class
value is now fully consumed before the element's real body is parsed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2548277fb4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… subtrees
Fix three false-result bugs in the raw-html! accessibility scanner:
- BUG 1 (false positive): only analyze maud html! bodies. A different html!
macro (e.g. a Yew/JSX island using <tag>...</tag>) was fed to the maud
parser, which stripped the angle brackets and misread <button></button> as
an empty maud button -> false button-name. Detect JSX angle-bracket tag
syntax (`<` followed by an ident or `/`) and skip such bodies entirely.
- BUG 2 (false positive): maud names accept `:` as well as `-`. read_name now
consumes colon-qualified names (x-on:click, svg:path, data:foo) as one name,
so the element's real { ... } body is still parsed instead of being orphaned
at the leftover `:`.
- BUG 3 (false negative): collect_labels now stops descending into
aria-hidden="true" subtrees, consistent with walk. A <label for=..> that
only exists inside a hidden container no longer counts, so the visible input
is correctly flagged.
Adds 8 focused unit tests (113 -> 121 a11y tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0ba5abcaf3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Clarify that `autumn a11y verify` is a build-time static scanner over raw maud html! bodies that is best-effort and advisory: it errs toward not failing on ambiguous/dynamic/non-maud constructs, so it can miss a defect in exotic markup and its raw-html! findings are advisory-grade, not proof. The complete compile-time accessibility guarantee lives in the typed primitives in autumn_web::a11y. Docs only; no scanner behavior changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7de84206e7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…me edge cases Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c6f6be1513
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…rify Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4db18643b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Fix two false positives that made the a11y verify scanner fail valid maud markup: - Element tag names are a full maud HtmlName (Punctuated<HtmlNameFragment, '-' | ':'>), so read the whole name via read_name instead of only the first ident. Custom/namespaced tags like x-input, x-button, and svg:rect are now one tag (matching no rule) rather than a phantom input/button suffix. - A Rust macro call in an @if/@while/@for condition head (ready! { input }) owns its !-delimited group as macro input, part of the condition. Detect ident-followed-by-! and skip the group so it is not misparsed as markup, leaving the final brace as the body. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4110eb02ee
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
maud HtmlName fragments may be integers, so a name reader that stopped at a numeric fragment (e.g. .col-12, data-1) orphaned the element body and falsely flagged named buttons/links as empty. Read integer fragments as part of the name. Elements with the native boolean hidden attribute are removed from the accessibility tree, so extend the hidden predicate (used by walk, collect_labels, and content_provides_name) to treat a present static hidden as hidden alongside aria-hidden="true". A dynamic hidden=(expr) stays non-hidden (conservative). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a5a15e7b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Part of #1706 (Acceptance Criterion 3 + 4). Non-scaffold follow-on to the typed accessible primitives — does not touch
scaffold.rs.Before / After
Before — an alt-less
<img>or an unlabeled<input>written in rawhtml!markup compiles clean and ships silently; the type system only guards code that goes through the typed primitives.After —
autumn a11y verifyfails CI (exit 1) with a WCAG-keyed report:How
The typed
autumn_web::a11yprimitives (Img/Button/Link/MenuItem/TextField) already prove accessible-name obligations at compile time for code that uses them. This pass covers the escape hatch the type system can't see: raw markup inmaud::html! { … }blocks. There is no walkable widget tree at runtime, so this is an honest best-effort static scan.autumn-cli/src/a11y.rs, structured afterautumn-cli/src/i18n.rs: it scans project.rsfiles and token-walks descending intohtml!macro bodies (the same descentautumn i18n checkuses to findt!calls nested inhtml!), building aReportwith anexit_code(strict)and text/JSON output.Severitysemantics fromautumn-cli/src/check.rs(the runtimeautumn check --a11ylint), applied to static source instead of rendered HTML.--format jsonemits the "conformance manifest" AC3 asks for: an array of findings keyed to WCAG SCs plus a summary.--strictlowers the threshold to Moderate), mirroringi18n check'sexit_code(strict). This is what fails CI.main.rs: ana11ysubcommand group with averifyvariant ([PATH]defaulting to.,--format,--strict), dispatched likeI18nSubcommands.Scope note (the honest guarantee)
Typed primitives = the compile-time proof; this pass = the escape-hatch net. A typed primitive spliced into markup as
(Img::new(src, alt))is a(expr)splice, not animgelement, so it is never re-scanned or falsely flagged. Likeautumn i18n check, the scanner reads tokens rather than a resolved AST and always errs toward not flagging what it cannot resolve, so it never breaks CI on a false positive: a spliced attribute/content value (alt=(caption),button { (label) }) is treated as present, and a<label for>/idassociation a splice makes unresolvable suppresses thelabelfinding. This is a documented best-effort static scan with a dynamic-value limitation.WCAG SC mapping
image-alt<img>altattributelabel<input>/<select>/<textarea><label for=…>,aria-label, oraria-labelledbybutton-name<button>aria-label/aria-labelledbylink-name<a href>aria-label/aria-labelledbyTests
html!snippets: RED cases (alt-less img, unlabeled input/select/textarea, empty button, textless anchor) flagged with the correct WCAG SC; GREEN cases (img with alt / empty alt / dynamic alt, input with matching labelfor/id, aria-label, wrapped label, button/anchor with text or aria-label, icon button with alt image) clean; a typed-primitive splice and a fully-dynamic splice are not flagged; an element inside a@ifblock is still scanned.Docs (AC4)
docs/guide/accessibility.mdgains anautumn a11y verifysection: what it checks, the WCAG SC mapping table, a--format jsonexample, CI usage (exit code), and the explicit statement that typed primitives are the compile-time proof whileverifycatches raw-html!escape hatches.Gate
cargo fmt --all -- --check,cargo clippy -p autumn-cli --all-targets -- -D warnings, and the newautumn-clia11y tests all pass.Follow-ons
altheuristics,role/tabindexmisuse, duplicateid) as further slices.html!-block<label for>/idresolution (associations currently scoped per block).verifythrough the/actuator/a11yactuator or a dedicated CI workflow step.Scope and limitations
autumn a11y verify, a build-time static scanner over raw maudhtml!bodies (WCAG rules: image-alt 1.1.1, label 1.3.1 / 3.3.2, button-name / link-name 4.1.2), emitting a WCAG-keyed report and failing CI on findings.html!macros) to avoid false CI failures. It may therefore miss a defect in exotic raw markup — acceptable by design — but should not fail valid markup.autumn_web::a11y(Img/Button/TextField/Link/MenuItem— accessible name is a type-level obligation proven by trybuild), landed in Part of #1706: typed accessible UI primitives (compile-time accessible-name obligation) #1840 and Part of #1706: typed accessible Link and MenuItem primitives #1842. Findings froma11y verifyover rawhtml!are advisory signal, not the proof — consistent with Prove UI accessibility conformance at build time by construction #1706 scoping raw hand-written HTML as the "unverifiable, not proven" escape hatch.html!). Any remaining niche parser corners are a documented limitation of the best-effort net rather than blockers.🤖 Generated with Claude Code
https://claude.ai/code/session_01Pf5JUv54F6Bxo8KzErU9sz
Generated by Claude Code