Skip to main content

SDK domain completion — design spec (2026-06-22)

Brainstormed + approved 2026-06-22. Sub-project 1 of 4 toward real Supabase auth + cloud-persisted, e2e-repeatable accounts (see the program). This spec closes the domain-model gaps so the cloud schema is built once, complete.

Goal

Bring the client_sdk domain model to parity with the North Star + POC — every gap from the domain-coverage auditbehaviorally complete (fields + SDK service rules + schema + test doubles), SDK-only (no UI).

Program context

  1. SDK domain completionthis spec
  2. Auth + Supabase app project + config
  3. Cloud data adapter (StoragePort vs Supabase, RLS, JWT)
  4. e2e harness on real auth + data

Sub-project 1 is first because the cloud schema (sub-projects 2–3) must reflect the complete domain, and these are also the "do chores really well" gaps.

Approach

Extend the domain end-to-end per gap, grouped by entity, following the approvalPolicy precedent (added at Drift v3 + Supabase …000003):

model (+ @JsonSerializable codegen) → Drift column + if (from < 11) step (bump schemaVersion 10→11) → Supabase migration (…000012+) → in-memory adapter row-mapping + seed factory → SDK service rule → unit test.

The SDK stays pure Dart; presentation is untouched. Maps (roomAssignees, stepsPerMember) are stored as JSON columns exactly like subtasks already is.

The six entity clusters (the work)

C1 — HouseholdMember

  • String? homePlaceId — FK → places.id, ON DELETE SET NULL. Validated against the household's places in HouseholdService.
  • bool watchOnly (default false). Rule: watch-only members are excluded from assignment + active-roster surfaces in services (they're tracked, not actors).

C2 — Chore: assignment & rooms

  • List<String> assignedMemberIds (default []; empty = all eligible, by minAge).
  • Map<String, List<String>> roomAssigneesplaceId → [memberId]. Rules (POC invariant, in ChoreService): room keys must be valid places; assignedMemberIds is the union of all per-room lists; a resolution helper answers "who owns this chore in placeId" (explicit room list → else members whose homePlaceId == placeId → else all assignees).

C3 — Chore: per-member steps (closes #206)

  • Map<String, List<Subtask>> stepsPerMember (default {}) + helper List<Subtask> stepsForMember(String memberId) (returns the override if present, else the shared subtasks). Completion/sign-off uses the resolved list.

C4 — Chore: recurrence & incentives

  • ChoreFrequency.multiPerDay (new enum value) + int? maxPerDay (null = unlimited).
  • int? estimateMin.
  • int? tempBonusTokens + TempBonusUntil tempBonusUntil (today | thisWeek | untilOff) + computed bool hasTempBonus / int effectiveTokens (tokenValue + active bonus).
  • Rules in ChoreService: enforce the per-day cap (instances/day ≤ maxPerDay) for multiPerDay; a temp-bonus expiry sweep that clears expired bonuses (append-only equivalent of the POC's expiry pass). effectiveTokens feeds the existing token-award path (expectations still pay 0 — invariant 1 holds: a temp bonus on an expectation is the explicit exception and must be modelled to NOT violate invariant 1 — decision: temp-bonus tokens are allowed on expectations and are paid via the normal approval/ledger path, since they are an explicit, parent-set, time-boxed incentive — document this as an amendment to invariant 1).

C5 — Goal: lifecycle & media

  • GoalStatus status (active | requested | complete | archived). The stored is_active column migrates to status (backfill: true→active, false→archived); bool get isActive => status != GoalStatus.archived keeps existing reads compiling. copyWith(isActive:) callers migrate to setStatus.
  • String? imageUrl + String? dueLabel.
  • EconomyService transitions: requestGoal (→ requested), approveGoal (→ active), completeGoal (→ complete), archiveGoal (→ archived). The request→approve flow mirrors chore approval (validation, parental gate). UI is later; SDK methods + status land here.

C6 — AI recommender models

  • ChoreRecommendation { String balanceNote; List<ChoreSuggestion> suggestions }.
  • ChoreSuggestion { String title; String? emoji; String typeName; int tokens; String reason; String? placeId; String? matchesExistingId }.
  • Pure @JsonSerializable models, no table, no persistence. A recommend() facade method may be stubbed (returns an empty ChoreRecommendation) until the AI sub-project.

Schema & migrations

  • Drift (local): new columns on chores / household_members / goals; bump schemaVersion 10→11; add one if (from < 11) block (mirror the v3 precedent). JSON columns for the maps.
  • Supabase: new migrations 20260612000012+, one per entity, with CHECK constraints (multiPerDay/maxPerDay ≥ 0; goal status enum) + the is_activestatus backfill. RLS unchanged (same member_household_ids() scoping). AI models: no migration.

Testing

  • Test doubles: update packages/client_sdk_testing/seed_factories.dart, demo_seed.dart, and the in-memory adapter row-mapping — for every new field so the demo household exercises them.
  • Unit tests per new service rule: assignment-union invariant, room-owner resolution, stepsForMember fallback, effectiveTokens + expiry sweep, multiPerDay cap, the four goal transitions, watch-only exclusion.
  • Flow goldens should stay stable (no UI change); if any golden moves, regenerate with --dart-define=createScreenshots=true. Capture true flutter exit codes (> /tmp/t.txt 2>&1; echo $?), never a piped tail.
  • Codegen discipline: scoped --build-filter; after Drift regen, restore clobbered .g.dart / router.gr.dart / bloc state.g.dart siblings from HEAD.

Out of scope (later sub-projects)

  • All UI surfaces: by-room Today grouping, the goal-request UI, the recommend dialog, the per-member step editor, watch-only/home-room editors.
  • The cloud data adapter (sub-project 3), real auth (sub-project 2), the AI recommender service.

Sequencing (for the plan)

C1 Member → C2 assignment & rooms → C3 per-member steps → C4 recurrence & incentives → C5 goal lifecycle → C6 AI models. Member first because chore assignment references homePlaceId/watchOnly. Each cluster carries its full layer stack (model → Drift+migration → Supabase migration → test doubles → service rule → tests) and ends green.

Risks / notes

  • Invariant 1 amendment (expectations pay 0): temp-bonus on an expectation is the explicit exception — document it in the economy invariants + the C4 service code.
  • is_activestatus migration is the only change that touches existing reads; the isActive getter contains the blast radius. Verify the money/goals code paths compile.
  • Drift --delete-conflicting-outputs clobbers hand-maintained .g.dart — use --build-filter and restore siblings (known hazard).