diff --git a/docs/superpowers/specs/2026-08-16-a11y-rule-validity-review.md b/docs/superpowers/specs/2026-08-16-a11y-rule-validity-review.md new file mode 100644 index 000000000..b3c0eb41a --- /dev/null +++ b/docs/superpowers/specs/2026-08-16-a11y-rule-validity-review.md @@ -0,0 +1,114 @@ +# a11y rule-validity review — all 15 rules + +Date: 2026-08-16 +Status: Review record (findings only — fixes tracked separately) +Phase B-4 of `2026-08-16-v1-roadmap.md`. The a11y category shipped **after** +`2026-08-09-v1-rule-validity-review.md`, so it never had that pass; this is it, at the same +standard. + +Method: four parallel reviewers (ARIA-data rules · element-level rules · route/project rules · +cross-cutting docs/severity/messages), every Svelte claim checked against the Svelte MCP +documentation server, every web-standards claim verified by fetching a primary source (tagged +`[fetched]` in the reviewers' records) or labeled `[unverified-knowledge]`, every shipped and +documented snippet run through `svelte-autofixer`, and every detection claim re-run against the +built `packages/core/dist` before this record was written. The coordinator independently reproduced +the element-level and route-scoped Priority 1 rows. + +**Headline: 3 of 15 rules carry no Priority 1 defect; 12 do.** That is a materially worse ratio +than the August review's 38-of-73, and the reason is structural rather than accidental — see the +pattern below. No rule is misconceived: every row is fixable by narrowing detection, extending a +data table, or correcting prose. + +## The pattern worth naming + +The category's own design principle is **"false negatives are acceptable, false positives are +not"** (`2026-08-14-a11y-category-design.md`). **Twelve of the fourteen Priority 1 rows are false +positives** — every row but 2 (a false negative) and 14 (a docs defect) — and they cluster in three +mechanisms: + +1. **A pinned data source that has drifted from the spec.** `aria-query@5.3.2` is not the clean + ARIA 1.2 snapshot it was taken for — it is a mixed one (1.2's 48 attributes plus three 1.3 + additions, and the 1.3-only role `mark`), while missing five roles and two attributes that + ship in browsers today. Three P1 rows come from this alone. The v1 roadmap already flags the + data-source decision as a pre-1.0 dependency question (Phase C item 9); this review is the + evidence for taking it seriously. +2. **A literal-only read where the sibling read accepts an expression.** `accessible-name` accepts + any expression for `aria-label`, but only a literal for `alt` — so the idiomatic + `{siteName}` inside a link is "unnamed". The rule contradicts its own principle + within one function. +3. **"Absent" conflated with "unknowable".** Legacy ``, ``, and + form-associated custom elements are treated as empty content rather than content the analyzer + cannot see — the exact distinction the same functions already draw correctly for components. + +## Priority 1 — detection/premise defects with user-facing harm + +| # | rule | defect | disposition | +| --- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 1 | `duplicate-landmark` + `id-duplication` | **The docs promise something the engine cannot deliver, verbatim in en and ja:** "so mutually exclusive branches never fire a false duplicate". Exclusivity is syntactic only — `{:else if}` chains fold into one group, but two adjacent guards do not: `{#if a}
{/if}{#if !a}
{/if}` reports `Duplicate main landmark (2 of 2)`. | No cheap sound test for `!a` vs `a` exists. Delete the promise from all four pages and fold it into the existing "can pick a branch that would not actually render" caveat, which is the true statement. | +| 2 | `top-level-landmark` | **A bare `