Add on-demand validation trigger and diff reporting to Bufdir Validator
epic-activity-statistics-dashboard-state-and-realtime-task-008 — Extend the BufdirAlignmentValidator with an on-demand trigger method that can be called from a hidden developer settings menu or from automated integration tests. The method must run the cross-check, format any discrepancies as human-readable text (e.g., 'Dashboard shows 47 activities for type X, export aggregation shows 45'), and return a structured AlignmentReport object. Include a passing/failing status field so CI can assert zero discrepancies.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Add a `toFormattedSummary()` method on `AlignmentReport` that iterates `discrepancies` and uses string interpolation: `'Dashboard shows ${d.dashboardTotal} activities for type ${d.activityTypeName}, export aggregation shows ${d.exportTotal}'`. Implement `toJson()` on `AlignmentReport` and `AlignmentDiscrepancy` for CI consumption. For the developer menu, create a `DevToolsScreen` widget (only compiled in debug builds via `kDebugMode` guard) with a button that calls `runValidation()` and displays the `formattedSummary` in a scrollable dialog. For CI, expose a Flutter integration test that instantiates the validator with real (test-env) repos and asserts `report.isPassing`.
Do not use `debugPrint` as the primary output — return structured data that callers can format as needed.
Testing Requirements
Write unit tests: (1) given a passing AlignmentReport, formattedSummary equals the expected passing message; (2) given a report with 2 discrepancies, formattedSummary contains exactly 2 lines with correct values and names; (3) given an exception during validate(), runValidation() returns a failing AlignmentReport with error field set rather than throwing; (4) AlignmentReport.toJson() produces parseable JSON with isPassing, discrepancies, and formattedSummary fields. Include one integration test that calls runValidation() with mock repos returning known divergent data and asserts the structured output.
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.