Implement Bufdir Alignment Validator service scaffold
epic-activity-statistics-dashboard-state-and-realtime-task-007 — Create the BufdirAlignmentValidator service class that provides a diagnostic API for comparing dashboard aggregation results against the export aggregation logic. The service must accept a time window and chapter scope, independently invoke both the stats repository query path and the export aggregation query path, and return a diff object listing any discrepancies in totals per activity type. This is a development and diagnostic tool, not part of the production UI render path.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Define three model classes: `AlignmentDiscrepancy`, `AlignmentReport`, and `DateRange` (if not already defined). Use `Future.wait([statsRepo.getAggregated(chapterId, timeWindow), exportRepo.getAggregated(chapterId, timeWindow)])` for parallel fetching. Compare results by activity type ID — iterate union of keys from both maps. Compute delta as `dashboardTotal - exportTotal`.
Set `isPassing = discrepancies.isEmpty`. This class should live in a `diagnostic/` or `dev_tools/` folder to make its non-production status explicit. Consider using a `kDebugMode` guard or a separate Riverpod provider scope so the class is tree-shaken from production builds.
Testing Requirements
Write unit tests using flutter_test with mock repositories: (1) both paths return identical totals → AlignmentReport.isPassing is true, discrepancies list is empty; (2) one path returns different total for one activity type → isPassing is false, discrepancy has correct delta; (3) one path throws → isPassing is false, error field is set. Use Mockito or manual stubs for StatsRepository and ExportAggregationRepository. Do not use real Supabase connections in unit tests.
Supabase realtime channel subscriptions that are not properly disposed on screen close can accumulate in memory across navigation events, causing duplicate invalidation calls, ghost fetches, and eventual memory leaks on long sessions.
Mitigation & Contingency
Mitigation: Implement StatsCacheInvalidator as a Riverpod provider with an explicit ref.onDispose callback that cancels the realtime channel subscription. Write a widget test that navigates away and back multiple times and asserts that only one subscription is active at any given time.
Contingency: If subscription leaks are found in production, add a global subscription registry that enforces at-most-one subscription per channel key, and schedule a dispose sweep on app background events.
Debouncing rapid inserts may swallow the invalidation signal if the debounce window outlasts the Supabase realtime event delivery window, resulting in the dashboard showing stale totals after a bulk registration completes.
Mitigation & Contingency
Mitigation: Set the debounce window to 800ms (shorter than the typical Supabase realtime delivery latency of 1-2s for batched events) and ensure the leading-edge invalidation fires immediately while trailing duplicates are suppressed. Integration-test with a 20-record bulk insert.
Contingency: If debounce timing proves unreliable, replace debounce with a trailing-edge timer reset on each event and add a guaranteed invalidation 5 seconds after the last event regardless of subsequent events.