Skip to main content

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)

  1. 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.
  2. Chore → multiple rooms. A chore is tagged to a set of rooms (multi-select), replacing today's single placeId.
  3. 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.
  4. 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> to Place (member ids), persisted. New column owner_ids uuid[] on places (or a place_owners join if RLS/history warrants — default: array column, mirroring chore.assignedMemberIds). Household-scoped RLS (same as places).
  • Chore multi-room: introduce roomIds: List<String> on Chore — the rooms the chore applies to (replaces the single placeId as the assignment set; the print-tag use of placeId folds into "the chore's rooms"). Keep roomAssignees (Map roomId→memberIds) as the manual per-room override map, and assignedMemberIds as the chore-level fallback. Migration maps any existing placeIdroomIds:[placeId].
  • Resolution in ChoreService (the single source of truth):
    membersForRoom(chore, roomId, place):
    if chore.roomAssignees[roomId] is non-empty → return it // (a) manual override
    else if place.ownerIds is non-empty → return place.ownerIds // (b) room owner(s)
    else → return chore.assignedMemberIds // (c) chore fallback
    The chore's overall effective assignees = union over its roomIds of membersForRoom(...) (plus any roomless assignedMemberIds). This replaces/extends the current assignedMemberIdsUnion getter.

2. The shared multi-select room picker (design_system + app)

  • A DS component mirroring DsEmojiPicker's modal shape (a showDsSheet-hosted grid): floor-grouped room chips (reuse groupPlacesByFloor/shouldShowFloorHeaders), multi-select via FilterChip (toggle inclusion in a Set<String>), a "clear" affordance, optional search-expand. Returns the selected List<String> room ids. Test-key convention keyPrefix like the current RoomPicker.
  • 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 membersForRoom so a room shows its resolved doers.

4. Error handling & constraints

  • Typed errors only (on <SpecificException>; never bare catch, never Error). 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_ids default '{}'; backfill roomIds from placeId.

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: membersForRoom resolution across all three branches (override / owner / chore fallback), multi-owner rooms, the effective-union getter, the placeIdroomIds migration mapping.
  • Adapters: ownerIds/roomIds round-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)

  1. Room-owner storage: array column places.owner_ids uuid[] (default '{}') — mirrors chore.assigned_member_ids; add a GIN index if "rooms owned by X" becomes a hot query. A place_owners join table is deferred until per-owner metadata (primary owner, shares, history) is actually needed.
  2. placeId: fully folded into Chore.roomIds (a chore's rooms are its rooms). Migrate placeId → roomIds:[placeId]; room-print lists iterate roomIds.
  3. 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.