Files
kvitt/PLAN-M5.md
T
jacobh2 5e1be38364 Plans and start prompts for every milestone; SPEC v0.2
PLAN-<id>.md for U1, U2, M1, M2a, M2b, M2c-dev, M2c-pi, M3, M4, M5, M6,
M7a-M7f, M8, M9, M10, M11, each with the eleven fixed sections, and a
start prompt per plan in prompts/ plus prompts/README.md (dependency
order and parallel waves).

SPEC v0.2: owner answers D-001 (development-first, M2c split into
M2c-dev and M2c-pi, Gitea registry), D-002 (local backups deleted once
on Drive), D-003 (Splitwise CSV format, English and Swedish variants,
category mapping), OQ-1, OQ-2, OQ-4 answered; M1's readings of
ambiguous contract points stated explicitly in §14.3 to §14.5.
HANDOFF §2 lists the plans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:05:34 +02:00

9.6 KiB

PLAN-M5: Android data layer

1. Goal

Implement :data: the Room database v1 with exported schema, DAOs, the Ktor API client and DTOs, the encrypted token store, the sync engine and its WorkManager workers, every real repository behind the :core:model interfaces, and the APK updater. The phone then works fully offline and syncs per §14.4, proven by tests with Ktor MockEngine and the shared JSON examples.

2. Read first

  1. SPEC §14.2 Room entities (14.2.1 Mirrored tables, 14.2.2 Local-only tables, 14.2.3 Migrations, 14.2.4 UI contract).
  2. SPEC §14.4 Sync protocol v1, phone side: 14.4.1, 14.4.2 (phone steps), 14.4.3 (request shape), 14.4.5 Phone push procedure, 14.4.6, 14.4.8 Epoch recovery and re-pairing, 14.4.9 Limits and timing.
  3. SPEC §14.3 OpenAPI outline (14.3.1 headers, 14.3.2 error codes, 14.3.3 wire objects, 14.3.4 endpoints the app calls, 14.3.5).
  4. SPEC §8.2 Device tokens (phone storage), §8.5 Transport, §8.7 Privacy and logging.
  5. SPEC §4 FR-2 AC-2.4, AC-2.7 to AC-2.9; FR-3 AC-3.3; FR-4 AC-4.2, AC-4.3, AC-4.6; FR-6 to FR-9 (online-only writes); FR-8 AC-8.6 (defaults), AC-8.10; FR-10 AC-10.4; FR-11 AC-11.3 to AC-11.6; FR-12 AC-12.4, AC-12.5; FR-14 AC-14.1; FR-15 Offline and sync; FR-16 In-app update.
  6. SPEC §9.1 to §9.4, §9.6, §9.9 Online-only actions, §9.10.
  7. SPEC §6.2 (Local DB, DI, HTTP, Background, Images, Secrets, Tests rows; module table).
  8. SPEC §11.2 (row M5), §11.3, §14.7, §14.8.
  9. Files: contracts/examples/**, android/core/model/**, android/domain/**, android/core/fakes/**, android/build-logic/**, android/gradle/libs.versions.toml, CLAUDE.md (BUILD AND VERIFY, ARCHITECTURE RULES, ANTI-PATTERNS, THE PHONE).

3. Already established

  • KvittDatabase in package se.hagstedt.kvitt.data.db, file kvitt.db, version 1, exportSchema = true to android/data/schemas/ (committed; a contract file once landed); no foreign keys; names identical to the server's; camelCase properties with @ColumnInfo(name = "snake_case"); server-only columns not mirrored; timestamps String, dates String, money Long, bp Int, booleans Boolean; local_updated_at on every write (§14.2).
  • Entity classes, tables, columns, keys and indices exactly as §14.2.1 and §14.2.2; per-phone preferences in DataStore prefs.preferences_pb (§14.2.2).
  • Local write semantics: pending = true, new UUIDv7 pending_mutation_id, updated_at = now in the fixed form (and created_at on create), local delete sets deleted_at = updated_at = now; pending count = pending expenses + pending settlements (§14.2.1).
  • Pull per page in one Room transaction: epoch check, apply arrays in the fixed order, skip rows with local server_version ≥ incoming and pending expenses/settlements, upsert and replace children, audit insert-or-ignore, cursor and epoch update; repeat while has_more; then last_success_at (§14.4.2).
  • Push procedure steps 1 to 4 of §14.4.5, including the re-edit-during-request rule, equal-content rejected treated as accepted, clock_skew handling, 401 handling (device_revoked → AC-3.3 wipe; unauthorized → SessionState.Unrecognised) (§14.4.5, §14.4.8).
  • Sync triggers: app start, every foreground, 2 s after the last local write (debounced), every 15 minutes in the background (network constraint), every FCM data message (expedited); one cycle at a time (Mutex; unique work kvitt-sync, KEEP periodic, APPEND_OR_REPLACE one-off); backoff exponential from 30 s (AC-15.3, §14.4.1, §14.4.9).
  • Headers on every call: X-Kvitt-Protocol: 1, X-Kvitt-App-Version, Accept-Language, Authorization: Bearer on authenticated calls; 426 → stop syncing and surface update (§14.3.1).
  • Token: Android Keystore AES-256-GCM key alias kvitt_token_key, setUserAuthenticationRequired(false), non-exportable; ciphertext and IV in DataStore auth.preferences_pb; base URL, user id, device id in plain text beside it; never logged or put in a URL (§8.2).
  • Online-only actions return OnlineResult.Offline without a validated network or within 30 s of a network error; a REST network failure changes nothing locally (§9.9).
  • Balances, previews, proposals and analytics are computed in :data with :domain and include pending changes; features never call :domain (§6.2, AC-10.4, AC-14.1).
  • Editor defaults from DataStore: last used account if active else oldest active; last used category in that account if active else other (AC-8.6).
  • APK updater: release builds call GET /apk/latest on cold start and at most every 6 hours while resumed; download to cache, verify size and SHA-256, install through PackageInstaller; failure deletes the file; debug builds never check (FR-16).
  • Pictures through Coil with an auth header, cached by user id and picture_version (AC-4.6, §6.2).
  • Tests use Ktor MockEngine and typed fakes; no mocking libraries (§6.2, CLAUDE.md).

4. Owned paths

From SPEC §14.7, verbatim: android/data/** (owner M5; after landing: "later data plans"). Also from §14.7 (row android/settings.gradle.kts, android/build.gradle.kts, android/gradle.properties, android/gradle/**, android/build-logic/**): "later plans may add catalog entries and module includes (rebase resolves)".

Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.

5. Deliverables

  • android/data/build.gradle.kts: dependencies :core:model, :domain, Room, KSP, Ktor, kotlinx.serialization, WorkManager, DataStore, Hilt, Coil.
  • android/data/src/main/kotlin/se/hagstedt/kvitt/data/db/**: KvittDatabase, every entity of §14.2.1 and §14.2.2, DAOs.
  • android/data/schemas/**: the exported Room schema for version 1.
  • .../data/api/**: Ktor client with the §14.3.1 headers, DTOs for every wire object and endpoint body, error mapping to §14.3.2 codes.
  • .../data/auth/**: Keystore token store, session state.
  • .../data/sync/**: sync engine (push, pull, epoch recovery, rejection records), debounce, WorkManager workers.
  • .../data/repo/**: real implementations of every repository interface of §14.2.4.
  • .../data/update/**: APK updater.
  • .../data/connectivity/**: ConnectivityRepository per §9.9.
  • .../data/di/**: Hilt modules that :app (M8) can install.
  • android/data/src/test/**: DTO round-trip tests over every file in contracts/examples/; sync engine tests with MockEngine for each §14.4 rule including AC-15.6 (14 days offline, 200 changes, scripted clock); repository tests (balances with pending rows, editor defaults, online-only Offline results).
  • android/data/src/androidTest/** (if Room needs a device for the schema test; only on the phone the start prompt names).

6. Steps

  1. Worktree. From the main checkout /Users/jhag/projects/kvitt: git worktree add .claude/worktrees/agent-m5 -b agent-m5 main, then in .claude/worktrees/agent-m5: git reset --hard main. Copy android/local.properties from the main checkout (add sdk.dir=/opt/homebrew/share/android-commandlinetools if missing; never overwrite or commit). Run tools/install-git-hooks.sh.
  2. Read §2 in order. Confirm M2b has landed (android/core/model/ on main); if not, stop and report.
  3. Run python3 tools/dependency-report/latest.py; record the lines. Confirm current Room, KSP, Ktor, WorkManager, DataStore and Android Keystore APIs from official documentation (developer.android.com, ktor.io); any new catalog entry at its latest stable version; record versions.
  4. Entities and DAOs; export the schema; check it against §14.2.1 column by column.
  5. DTOs and the round-trip test over every contracts/examples/** file (decode, re-encode, compare JSON values) before writing the client.
  6. Ktor client and error mapping; token store.
  7. Sync engine test-first with MockEngine: each pull step, each push result case, epoch recovery, Unrecognised and revoked paths, clock skew, no-progress stop, the AC-15.6 scenario.
  8. Repositories, one by one, with tests; workers and triggers; APK updater (verification logic unit-tested; PackageInstaller call isolated behind an interface).
  9. Run the gate (§7). Commit on agent-m5, git rebase main, rerun the gate. Never merge, push or remove the worktree.

7. Gate

From SPEC §11.2, verbatim: "Android gate; DTO round-trip tests over every file in contracts/examples/; sync engine tests with Ktor MockEngine covering §14.4 including the 14-day offline case (AC-15.6)". Command, from android/: PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check. Green means: BUILD SUCCESSFUL in this session; fresh XML shows the round-trip test covering every example file (count equals the number of files under contracts/examples/) and the sync tests including AC-15.6, 0 failures.

8. HUMAN steps

From SPEC §11.2, verbatim: "none".

9. Out of scope

  • Any UI, ViewModel or string (M7a to M7f; M6 for :core:design).
  • :app wiring, navigation, Hilt component installation, FCM service and token registration, notification building, deep links (M8).
  • Changing :core:model types or interfaces (contract change via the orchestrator).
  • Server code; infra/.

10. Report

  • Branch agent-m5, head commit hash after rebasing on main.
  • Gate output: BUILD line, actionable tasks line, test counts from fresh XML (round-trip count vs. example file count, sync tests, repository tests).
  • Versions added or changed, with sources; dependency-report lines.
  • Mapping of §14.4.2 and §14.4.5 rules to test names.
  • Numbering used or TODO(spec) occurrences.
  • Contract requests (any missing field in §14.2.4), open questions, spec gaps (with section numbers).

11. Numbering

Decision ids, spec version and schema version are assigned by the orchestrator in the start prompt; use TODO(spec) if none was given.