Add Debug Assertions to Token Enforcer
epic-visual-design-accessibility-services-and-widgets-task-013 — Extend the TokenAccessibilityEnforcer with comprehensive debug assertions that fire immediately when a non-compliant usage is detected during development. Implement assert() calls with descriptive failure messages that include the failing component name, the actual versus required value, and a link to the WCAG criterion violated. Ensure assertions are stripped from release builds via Dart's assertion semantics.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Add assertions as the very first statements inside each validation method in TokenAccessibilityEnforcer, before any existing logic. Use a private helper `_buildAssertMessage(String component, String criterion, String criterionUrl, num actual, num required, String unit)` to produce consistent message formatting and avoid string duplication. Example for touch target: `assert(size.width >= 44.0 && size.height >= 44.0, _buildAssertMessage(componentName, 'WCAG 2.5.5 Target Size', 'https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum', min(size.width, size.height), 44.0, 'pt'))`. Never use `if (kDebugMode) throw` — always use bare `assert()` so the Dart compiler can tree-shake message construction.
WCAG criterion URLs to reference: 1.4.3 (Contrast Minimum), 1.4.11 (Non-text Contrast), 1.4.4 (Resize Text), 2.5.5 (Target Size), 1.4.12 (Text Spacing).
Testing Requirements
Unit tests (flutter_test): for each of the 8+ assertion points, write one test with a known-bad value and verify `expect(() => enforcer.validate(...), throwsAssertionError)` in debug mode; write one test with a known-good value and verify it completes without error. Verify assert message content: use `expect(() => ..., throwsA(predicate((e) => e.toString().contains('WCAG'))))` to confirm WCAG criterion IDs appear in messages. Release-mode simulation: use `flutter test --dart-define=dart.vm.product=true` or equivalent to confirm no assertion errors surface with known-bad values. Regression test: run all existing TokenAccessibilityEnforcer tests and confirm none are broken by the new assertions.
Flutter's textScaleFactor behaviour differs between iOS and Android, and third-party widgets used across the app (date pickers, bottom sheets, chips) may not respect the per-role scale caps applied by the dynamic-type-scale-service, causing overflow in screens this epic cannot directly control.
Mitigation & Contingency
Mitigation: Enumerate all third-party widget usages that render text. For each, verify whether they honour the inherited DefaultTextStyle and MediaQuery.textScaleFactor or use hardcoded sizes. File issues with upstream packages and wrap non-compliant widgets in MediaQuery overrides scoped to the safe cap for that role.
Contingency: If upstream packages cannot be patched within the sprint, implement a global MediaQuery wrapper at the app root that clamps textScaleFactor to the highest per-role safe value (typically 1.6–2.0), accepting that users at extreme OS scales see a safe cap rather than full scaling for those widgets.
The CI accessibility lint runner depends on the Dart CLI toolchain and potentially custom_lint or a bespoke Dart script. CI environments differ from local dev environments in Dart SDK version, pub cache configuration, and platform availability, risking intermittent CI failures that block all pull requests.
Mitigation & Contingency
Mitigation: Pin the Dart SDK version in the CI workflow configuration. Package the lint runner as a self-contained Dart script with all dependencies vendored or declared in a dedicated pubspec.yaml. Add a CI smoke test that runs the runner against a known-compliant fixture and a known-violating fixture to verify the exit codes are correct.
Contingency: If the custom runner proves too fragile, fall back to running dart analyze with the flutter-accessibility-lint-config rules as the sole CI gate, and schedule the custom manifest validation as a separate non-blocking advisory check until the runner is stabilised.
Wrapping all interactive widgets with a 44 pt minimum hit area via HitTestBehavior.opaque may cause unintended tap interception in widgets where interactive elements are closely stacked, particularly in the expense type selector, bulk confirmation screen, and notification filter bar.
Mitigation & Contingency
Mitigation: Conduct integration testing of the touch target wrapper specifically in dense layout scenarios (expense selector, filter bars, bottom sheets with multiple buttons). Use the Flutter Inspector to visualise hit areas and confirm no overlaps. Pair with the interactive-control-spacing-system to ensure minimum 8 dp gaps between expanded hit areas.
Contingency: If overlapping hit areas cause mis-tap regressions in specific screens, allow the touch target wrapper to accept an explicit hitAreaSize parameter that can be reduced below 44 pt only in contexts where the interactive-control-spacing-system guarantees sufficient gap, with a mandatory code review flag for any such override.
The contrast-safe-color-palette must guarantee WCAG AA ratios for both light and dark mode token sets. Dark mode color derivation is non-trivial — simply inverting a light palette often produces pairs that pass in one mode but fail in the other, and the token manifest must encode both sets explicitly.
Mitigation & Contingency
Mitigation: Define both light and dark token sets explicitly in the accessibility-token-manifest rather than deriving one from the other programmatically. Run the contrast-ratio-validator against both sets as part of the token manifest generation process and include both in the CI lint runner's validation scope.
Contingency: If time pressure forces a dark mode deferral, ship with light mode only and add a prominent in-app notice. Gate dark mode colour tokens behind a feature flag until the full dual-palette validation is complete.