high priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

A `BufdirAlignmentValidator` Dart class exists with a constructor accepting `StatsRepository` and `ExportAggregationRepository` as dependencies
The class exposes a `validate({required String chapterId, required DateRange timeWindow})` async method
The validate method independently invokes the stats repository query path and the export aggregation query path in parallel using Future.wait
The method returns an `AlignmentReport` object containing: totals per activity type from both paths, a list of `AlignmentDiscrepancy` objects, and an `isPassing` boolean
An `AlignmentDiscrepancy` has fields: `activityTypeId`, `activityTypeName`, `dashboardTotal`, `exportTotal`, `delta`
`isPassing` is true only when all activity type totals match exactly between the two paths
The class is annotated or documented as a diagnostic/dev tool — not instantiated in production provider graph
If either query path throws, the validator catches and wraps the error in an `AlignmentReport` with `isPassing: false` and an error field
The service does not mutate any database state — it is read-only
Both query paths use the same chapterId and timeWindow parameters to ensure a fair comparison

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase PostgreSQL 15 (via Supabase SDK)
Bufdir Reporting API (indirectly — export aggregation logic)
data models
activity
activity_type
annual_summary
bufdir_column_schema
bufdir_export_audit_log
performance requirements
Both query paths must run in parallel (Future.wait) to keep total validation time under 5 seconds
No caching of validation results — always fresh reads for diagnostic accuracy
security requirements
Validator must only be accessible from developer settings menu or test harness — never from production navigation
Supabase RLS enforces that only authorised users can read the underlying activity data
No validation results are persisted to the database — transient in-memory only

Execution Context

Execution Tier
Tier 1

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.

Component
Bufdir Alignment Validator
service medium
Epic Risks (2)
medium impact medium prob technical

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.

medium impact low prob scope

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.