Design-capstone accessibility pass + parental-write-gate generalization
Date: 2026-07-11 Status: Accepted — both shipped. Related: design-system decisions, actor-resolution invariant ADR, authored Authorization page §2.
Two load-bearing bodies of work worth a durable record.
1. Design-capstone pass (S1–S8) — accessibility & polish
A comprehensive design/usability/a11y audit across DS tokens + all top-level surfaces + key sheets, triaged must-fix / nice-to-have, then fixed in a wave. Shipped slices:
| Slice | Commit | What |
|---|---|---|
| S1 | f5ab211 | Light-theme AA contrast pass (tokens + bucket-ink + light contrast test). |
| S2 | 9a2d4a2 | Keyboard focus + selected semantics for nav items, pickers, search-more. |
| S3 | fb7fd7b | 44dp minimum touch targets (emoji, timer start, bucket chips, color swatches, clear-icon). |
| S4 | f86c0ad | Error liveRegion + loading/error/retry states + pull-to-refresh (Today/Earn/Rewards/sign-in). |
| S5 | fe042c8 | Success feedback + consent-capture guard + member-remove confirm + consent copy weight. |
| S6 | 49c40d0, 2c928e3 | Screen-reader labels + unique action labels + atom label safety (+ restore tap action on relabeled buttons). |
| S7 | 5df174b | Token/size compliance + text-scale robustness + focus-ring + a11y-settings deep-link. |
| S8 | 62e4b22 | Typographic hierarchy — tabular numerics + lifted headers + readable-content type. |
This is the natural home for the carried a11y/polish debts (switcher affordance cue, keyboard-focus parity on picker tiles, chip-gap extreme case). The DS a11y floor (WCAG AA contrast in both themes, Semantics coverage, reduced-motion, focus) is now audited, not assumed.
2. Parental-write-gate generalization (SDK ↔ RLS parity)
Decision: the SDK service must fail before the network round-trip wherever a
Postgres RLS policy gates on parental_household_ids() — converting a raw
PostgrestException(42501) into a typed AuthorizationFailure, and closing the class
where an admin-role but non-parental member (e.g. an otherAdult granted admin)
clears the app's admin-role guard but is denied at the wire.
- The single shared helper
EconomyService._requireParentalWriter([action])resolves the acting member only from the authenticated account (memberByAuthUserId(auth.uid()), the SDK twin ofCurrentMemberRepository) — never theviewingAslens, neverisParental.firstOrNull— and throws when the actor'skindis notisParental. It no-ops when the resolver is null (offline / free tier / unit slice) so RLS stays the sole backstop (online-enforced / offline-trusted parity with the child-PII gate). - Applied (each verified: parental-KIND RLS and intended parent-only): activities, rewards, houses (create/update/delete), places (create/update/delete), budget categories, spend gates (create/delete), goal lifecycle (create/update/archive/restore/complete/ approve).
- Deliberately skipped (member-initiated — gating would break them):
requestGoal,requestRewardRedemption/requestActivityRedemption,cancelSpendRequest, chore self-service verbs; and the ledger-internaltoken_batches/redemptions(no direct parental verb — downstream of already-parent-gated approval). - Deferred (dashed):
ChoreServicechore-catalog writes — that service has nocurrentMemberresolver and shares thechorestable with member self-service verbs, so it needs separate wiring + surgical per-verb gating.
| Slice | Commit | What |
|---|---|---|
| D | da4adbe | Introduced the gate for catalog writes (activities/rewards) — the reproduced SDK↔RLS mismatch. |
| F | bc6f812 | Generalized _requireCatalogWriter → _requireParentalWriter and applied it to houses/places/budget/goals/gates (25 call sites). |
No RLS / schema / migration change; no gate weakened — the fix makes the SDK match the RLS truth and surface a clean typed error.