Rooms — Room Ownership + Chore Multi-Room + Multi-Select Picker — Design Spec
Date: 2026-07-25
Status: Draft for owner review
Independent of: the My Cosmos companion build (separate sub-project). Branch: feat/mvp1-personas-authz.
Codebase map: the current RoomPicker is single-select; Place = {id, householdId, houseId, name, floor?}; the chore editor holds placeId (single primary/print-tag room) + roomAssignees (Map placeId→memberIds, per-room overrides) + assignedMemberIds (flat chore-level); ChoreService.membersForRoom resolves per-room; the emoji picker (showDsEmojiPicker/DsEmojiPicker) is the interaction template.
Goal
Replace the single-select "primary room" model with rooms as first-class, owned spaces. A room can have persistent owner(s) (who's responsible for it). A chore can be tagged to multiple rooms, and who actually does the chore in a given room inherits from the room's owners unless overridden — so a "tidy your room" chore naturally routes to whoever owns each room. One reusable emoji-picker-style multi-select room picker drives both surfaces.
Locked decisions (owner, 2026-07-25)
- Persistent, multi-owner room ownership. Each room can have several owner members (household-level, not chore-scoped). Set via the multi-select picker on the Rooms/Household surface.
- Chore → multiple rooms. A chore is tagged to a set of rooms (multi-select), replacing today's single
placeId. - Resolution order for a chore's doers in a given room: (a) explicit per-room override on the chore → (b) the room's owner(s) → (c) the chore's overall assignees. A room tagged on a chore with no override and no owner falls back to the chore's assignees.
- One reusable picker. An emoji-picker-style multi-select room picker component, used in two places: the chore editor (tag rooms + optional per-room override) and Rooms/Household (set room ownership).
Architecture
1. Data model
- Room ownership (new): add
ownerIds: List<String>toPlace(member ids), persisted. New columnowner_ids uuid[]onplaces(or aplace_ownersjoin if RLS/history warrants — default: array column, mirroringchore.assignedMemberIds). Household-scoped RLS (same asplaces). - Chore multi-room: introduce
roomIds: List<String>onChore— the rooms the chore applies to (replaces the singleplaceIdas the assignment set; the print-tag use ofplaceIdfolds into "the chore's rooms"). KeeproomAssignees(Map roomId→memberIds) as the manual per-room override map, andassignedMemberIdsas the chore-level fallback. Migration maps any existingplaceId→roomIds:[placeId]. - Resolution in
ChoreService(the single source of truth):The chore's overall effective assignees = union over itsmembersForRoom(chore, roomId, place):if chore.roomAssignees[roomId] is non-empty → return it // (a) manual overrideelse if place.ownerIds is non-empty → return place.ownerIds // (b) room owner(s)else → return chore.assignedMemberIds // (c) chore fallbackroomIdsofmembersForRoom(...)(plus any roomlessassignedMemberIds). This replaces/extends the currentassignedMemberIdsUniongetter.
2. The shared multi-select room picker (design_system + app)
- A DS component mirroring
DsEmojiPicker's modal shape (ashowDsSheet-hosted grid): floor-grouped room chips (reusegroupPlacesByFloor/shouldShowFloorHeaders), multi-select viaFilterChip(toggle inclusion in aSet<String>), a "clear" affordance, optional search-expand. Returns the selectedList<String>room ids. Test-key conventionkeyPrefixlike the currentRoomPicker. - Chore editor consumes it to pick the chore's
roomIds; each picked room then optionally gets a per-room override (the existing member-chip grid, now shown per selected room) — override present ⇒ manual; absent ⇒ inherits (owner → chore). Show the resolved effective doers per room (read-only), and keep the effective-assignees summary. - Rooms/Household consumes the same picker (or its per-room member grid) to set each room's
ownerIds.
3. Surfaces
- Chore editor: the single
RoomPicker(primary room) + the per-room override section are replaced by: multi-select "Rooms this applies to" + per-room override chips (only for selected rooms) + the read-only resolved doers. The chore-level "Assigned to" stays as the fallback. - Rooms page / Household: each room row gains an owner editor (the multi-select member picker) — "who owns this room."
- Today / printables: grouping "by room" and the room-print lists use
membersForRoomso a room shows its resolved doers.
4. Error handling & constraints
- Typed errors only (
on <SpecificException>; never barecatch, neverError). Domain rules (e.g., owner must be a household member) enforced in the SDK service + schema (FK/RLS). - One data path: Bloc → Repository → Client facade → Service → Adapter; presentation never imports drift/supabase.
- Household-scoped RLS on room ownership; owners must belong to the room's household.
- Migration file-only under
infra/supabase/migrations/; prod apply DEPLOY-GATED.owner_idsdefault'{}'; backfillroomIdsfromplaceId.
Scope boundary
In scope: Place.ownerIds + schema; Chore.roomIds (+ migration from placeId); the membersForRoom resolution chain (manual → owner → chore) in the SDK service; the reusable multi-select room picker (DS); chore-editor rewire (multi-room + per-room override + resolved doers); the Rooms/Household room-owner editor; Today/printables using the resolver; tests (SDK resolution + picker widget + a chore-editor flow story + a room-ownership flow story).
NOT in scope: named teams/saved groups (owner picked the per-member multi-owner model, not a team abstraction); single→multi-owner beyond the array; any change to the token/economy or companion; reworking floors.
Testing
- SDK unit:
membersForRoomresolution across all three branches (override / owner / chore fallback), multi-owner rooms, the effective-union getter, theplaceId→roomIdsmigration mapping. - Adapters:
ownerIds/roomIdsround-trip across the 5 adapters (one compile unit). - DS widget/golden: the multi-select picker (floor-grouped, multi-toggle, clear, search).
- Flow (ONE flowTest, multiple stories): Rooms epic — tag-a-chore-to-multiple-rooms, per-room-override-wins, room-owner-inherits, set-room-ownership. Baselines must not drop.
Resolved decisions (owner, 2026-07-25)
- Room-owner storage: array column
places.owner_ids uuid[](default'{}') — mirrorschore.assigned_member_ids; add a GIN index if "rooms owned by X" becomes a hot query. Aplace_ownersjoin table is deferred until per-owner metadata (primary owner, shares, history) is actually needed. placeId: fully folded intoChore.roomIds(a chore's rooms are its rooms). MigrateplaceId → roomIds:[placeId]; room-print lists iterateroomIds.- Today "by room": a multi-room chore lists under EACH of its tagged rooms; the resolver (
membersForRoom) shows the right doers per room. Completion stays per-chore (one chore = one completion) in v1 — per-room completion is a noted FUTURE refinement, explicitly out of scope for v1.