From dd41ed2c51beae07288fdfa724e410e0d321d278 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 22 Jul 2026 01:01:05 -0500 Subject: feat(a11y): engage the audit enforcement ratchet (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The point of the Phase 0 harness finally lands: named findings in textClipped, dynamicType, hitRegion, elementDetection, sufficientElementDescription, and trait now FAIL the build on the empty-state suite. Regressions in five phases of accessibility work are gated, not narrated. Three carve-outs, each earned by evidence rather than convenience: - contrast stays report-only. The two long-standing Settings findings are rows scrolled under the translucent tab bar, and their attribution flips between a row name and nil run-to-run — no suppression is narrow enough to keep CI stable. The centralised palette is the real guard. - The seeded dense-row tests run reportOnly. Bisection showed the audit degrades on children-ignored content — the correct VoiceOver treatment for dense rows — emitting unattributed contrast/dynamicType failures on rows that measure 6-7:1 and render correctly. - Characterised noise is suppressed narrowly and always logged as [noise: reason]: disabled controls (WCAG 1.4.3 exempt), "nearly passed" near-misses, system field placeholders (flagged at any length — proven by shortening them to no effect), and unattributed clipped/dynamic-type artifacts. noiseReason(for:) records each rule's provenance inline. Validated in both directions. Positive: the full 11-test suite passes with enforcement live, dark and light, on an erased simulator. Negative: re-injecting the Phase 3 icon-exposure regression produced two named [FAIL] findings and a failed suite — on both screens sharing the component — then went green again on revert. Docs and the CI workflow comment updated to describe the engaged state. --- Docs/ACCESSIBILITY.md | 53 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 21 deletions(-) (limited to 'Docs') diff --git a/Docs/ACCESSIBILITY.md b/Docs/ACCESSIBILITY.md index b35f02c..3f8b3ba 100644 --- a/Docs/ACCESSIBILITY.md +++ b/Docs/ACCESSIBILITY.md @@ -110,27 +110,38 @@ Users override it under Settings → Display. Dark mode reports 18 findings and light mode 21; the three extra are the section headers above. Everything the app actually controls passes in both schemes. -## Findings are reported, not enforced - -The audit surfaces violations that exist today, so failing on all of them would -block every unrelated change until the whole pass lands. Instead, findings are -logged and attached to the result bundle tagged `[report]` or `[FAIL]`. - -Enforcement is the committed constant -`AccessibilityAuditHarness.enforcedAuditTypes`. Widen it as each phase clears a -category: - -| After phase | Enforce | -| --- | --- | -| 2 — semantic colors + light mode | `.contrast` | -| 3 — Dynamic Type + reflow | `.textClipped`, `.dynamicType`, `.hitRegion` | -| 4 — VoiceOver | `.elementDetection`, `.sufficientElementDescription`, `.trait` | - -A constant rather than a CI setting, for two reasons. Environment variables do -not work: neither a plain `xcodebuild` env var nor a `TEST_RUNNER_`-prefixed -build setting reaches the UI test process, so the toggle silently did nothing. -And a committed value makes "when did contrast become enforced?" answerable with -`git blame` instead of CI tribal knowledge. +## Enforcement — the ratchet is engaged + +With phases 1–5 landed, `AccessibilityAuditHarness.enforcedAuditTypes` enforces +**`.textClipped`, `.dynamicType`, `.hitRegion`, `.elementDetection`, +`.sufficientElementDescription`, `.trait`** on the empty-state test suite. A +named finding in any of these fails CI — regressions in five phases of work are +now gated, not merely reported. + +Three deliberate carve-outs, each with its evidence: + +1. **`.contrast` stays report-only.** The two long-standing Settings findings + are rows scrolled under the translucent tab bar; their attribution flips + between a row name and nil run-to-run, so no suppression is narrow enough to + keep CI stable. The centralised palette in `Shared/Colors.xcassets` is the + actual guard against contrast regressions. +2. **The seeded tests run `reportOnly`.** Bisecting the row/badge accessibility + modifiers showed the audit degrades on `children: .ignore` content — the + *correct* VoiceOver treatment for dense rows — emitting unattributed + contrast/dynamicType failures on rows that measure 6–7:1 and render + correctly. Their burndown still prints; it just doesn't gate. +3. **Characterised noise is suppressed narrowly and always logged** with a + `[noise: reason]` marker — disabled controls (WCAG 1.4.3 exempt), "nearly + passed" near-misses, system field placeholders (clipped at any length — + proven by shortening them to no effect), and unattributed + clipped/dynamic-type artifacts. Nothing disappears silently; see + `noiseReason(for:)` for each rule's provenance. + +Enforcement is a committed constant rather than a CI setting, for two reasons. +Environment variables do not work: neither a plain `xcodebuild` env var nor a +`TEST_RUNNER_`-prefixed build setting reaches the UI test process, so the toggle +silently did nothing. And a committed value makes "when did clipping become +enforced?" answerable with `git blame` instead of CI tribal knowledge. ## Why coverage is split between local and CI -- cgit v1.2.3