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 audit —
behaviorally complete (fields + SDK service rules + schema + test doubles),
SDK-only (no UI).
Program context
- SDK domain completion ← this spec
- Auth + Supabase app project + config
- Cloud data adapter (StoragePort vs Supabase, RLS, JWT)
- 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 (+
@JsonSerializablecodegen) → Drift column +if (from < 11)step (bumpschemaVersion10→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 inHouseholdService.bool watchOnly(defaultfalse). 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>> roomAssignees—placeId → [memberId]. Rules (POC invariant, inChoreService): room keys must be validplaces;assignedMemberIdsis the union of all per-room lists; a resolution helper answers "who owns this chore inplaceId" (explicit room list → else members whosehomePlaceId == placeId→ else all assignees).
C3 — Chore: per-member steps (closes #206)
Map<String, List<Subtask>> stepsPerMember(default{}) + helperList<Subtask> stepsForMember(String memberId)(returns the override if present, else the sharedsubtasks). 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) + computedbool hasTempBonus/int effectiveTokens(tokenValue + active bonus).- Rules in
ChoreService: enforce the per-day cap (instances/day ≤maxPerDay) formultiPerDay; a temp-bonus expiry sweep that clears expired bonuses (append-only equivalent of the POC's expiry pass).effectiveTokensfeeds 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 storedis_activecolumn migrates tostatus(backfill:true→active,false→archived);bool get isActive => status != GoalStatus.archivedkeeps existing reads compiling.copyWith(isActive:)callers migrate tosetStatus.String? imageUrl+String? dueLabel.EconomyServicetransitions: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
@JsonSerializablemodels, no table, no persistence. Arecommend()facade method may be stubbed (returns an emptyChoreRecommendation) until the AI sub-project.
Schema & migrations
- Drift (local): new columns on
chores/household_members/goals; bumpschemaVersion10→11; add oneif (from < 11)block (mirror the v3 precedent). JSON columns for the maps. - Supabase: new migrations
20260612000012+, one per entity, withCHECKconstraints (multiPerDay/maxPerDay ≥ 0; goalstatusenum) + theis_active→statusbackfill. RLS unchanged (samemember_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,
stepsForMemberfallback,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 pipedtail. - Codegen discipline: scoped
--build-filter; after Drift regen, restore clobbered.g.dart/router.gr.dart/ blocstate.g.dartsiblings 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_active→statusmigration is the only change that touches existing reads; theisActivegetter contains the blast radius. Verify the money/goals code paths compile.- Drift
--delete-conflicting-outputsclobbers hand-maintained.g.dart— use--build-filterand restore siblings (known hazard).