critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

ContrastRatioValidator class is implemented as a pure Dart class with no Flutter dependencies so it can run in CLI contexts
validatePair(Color foreground, Color background) is synchronous and returns a ContrastResult object within <1ms for any color pair
ContrastResult contains: double ratio (rounded to 2 decimal places), bool passes4_5 (AA normal text), bool passes3_0 (AA large text / UI components)
Relative luminance calculation follows WCAG 2.1 formula exactly: linearize each channel (c/12.92 for c<=0.04045, else ((c+0.055)/1.055)^2.4), then L = 0.2126*R + 0.7152*G + 0.0722*B
Contrast ratio formula: (L1 + 0.05) / (L2 + 0.05) where L1 is the lighter luminance
Black (#000000) vs White (#FFFFFF) returns ratio 21.0
Mid-grey (#777777) vs white returns ratio approximately 4.48 (passes3_0 true, passes4_5 false)
Method handles fully transparent colors (alpha=0) without throwing; documents behavior (treats as opaque for calculation)
All public API is documented with dartdoc comments
Unit tests cover: pure black/white pair, identical colors (ratio 1.0), known AA-passing pair, known AA-failing pair, large-text-passing pair

Technical Requirements

frameworks
Dart (latest)
flutter_test
data models
ContrastResult (ratio: double, passes4_5: bool, passes3_0: bool, passes7_0: bool for AAA)
performance requirements
validatePair must complete synchronously in under 1ms — no async operations
No heap allocations beyond the ContrastResult return object per call
security requirements
No external network calls — purely computational
Input validation: clamp RGB channel values to 0–255 range before calculation to prevent NaN/Infinity

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Place in lib/core/accessibility/contrast_ratio_validator.dart. Keep it a plain class (not a Riverpod provider) — consumers will wrap it themselves. The WCAG 2.1 linearization formula has a common off-by-one pitfall: the threshold is 0.04045 (not 0.03928 from an older draft). Use the 0.04045 value.

Implement _linearize(double channel) as a private static helper to keep validatePair readable. Return ContrastResult as an immutable value object (use @immutable annotation or const constructor). For the passes3_0 field, threshold is ratio >= 3.0, not > 3.0 — equality passes. Consider adding a passes7_0 bool for WCAG AAA even if not required now, as it costs nothing and avoids a future breaking change.

This class will be imported by both Flutter runtime code (task-005) and a pure Dart CLI (task-007), so avoid any flutter/material imports.

Testing Requirements

Unit tests only (no widget tests needed — pure Dart). Use flutter_test package. Test file: test/contrast_ratio_validator_test.dart. Required scenarios: (1) black/white = 21.0, (2) white/white = 1.0, (3) #767676 on white = ~4.54 passes4_5, (4) #949494 on white = ~2.85 fails both, (5) #0000FF on white passes3_0 but not4_5, (6) AAA threshold pass (ratio >= 7.0).

Use expect(result.ratio, closeTo(expected, 0.05)) for floating-point comparisons. Target 100% line coverage for the validator class.

Epic Risks (4)
medium impact high prob integration

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.

high impact medium prob dependency

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.

medium impact medium prob technical

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.

high impact medium prob scope

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.