SPEC v0.4: M2a and M2b decisions, handoff to the real state
Records the decisions proposed in the M2a (fdf2093) and M2b (fad6fc9) reports: D-005 OpenAPI equality normalisations and CommonRules, D-006 bp_sum_not_10000 token, D-007 per-crate no-unwrap deny, D-010 Roborazzi, D-011 IncompleteCustomRangeException, D-012 Room 3 with M2b's entities and 1.json as a contract file, D-013 JDK 25 Robolectric flags, D-014 failOnNoDiscoveredTests (D-008, D-009 unused). Clarifications in §6, §8.2, §8.6, §9.6, §10.3, §10.4, §11, §14.2, §14.3, §14.5, §14.7, §14.8 and AC-11.7. PLAN-M5 builds on the existing entities; v0.4 readings added to PLAN-M3, M5, M7b to M7f. HANDOFF rewritten: next free D-040, spec v0.5, server schema 2, Room schema 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+9
-8
@@ -1,7 +1,7 @@
|
||||
# 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.
|
||||
Implement `:data` on top of the Room 3 entities and `KvittDatabase` v1 that M2b created (D-012): 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).
|
||||
@@ -12,10 +12,10 @@ Implement `:data`: the Room database v1 with exported schema, DAOs, the Ktor API
|
||||
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).
|
||||
9. Files: `android/data/**` as M2b left it (`build.gradle.kts`, `db/Entities.kt`, `db/KvittDatabase.kt`, `schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`), `android/README.md`, `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).
|
||||
- Room 3 (`androidx.room3` 3.0.3, KSP only, `SQLiteDriver` with `sqlite-bundled`; migrations take an `SQLiteConnection`) (D-012). M2b already created every entity of §14.2.1 and §14.2.2 and `KvittDatabase` (package `se.hagstedt.kvitt.data.db`, file `kvitt.db`, version 1, `exportSchema = true`); the exported `android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json` is a contract file (§14.8). M5 adds DAOs to the existing database and does not change the entities or the exported schema; a change it needs is a contract request to the orchestrator (spec bump, decision, next Room version 2 with a migration and migration test). Established for those entities: 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).
|
||||
@@ -25,6 +25,7 @@ Implement `:data`: the Room database v1 with exported schema, DAOs, the Ktor API
|
||||
- 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).
|
||||
- Readings fixed in v0.4 (§14.2.4): `FieldError.error` is a §14.5.3 token, `required`, or `validation_failed` for a §9.6 rule without a domain token; `analytics()` resolves a missing `CUSTOM` date as today before calling `resolveRange` (`AnalyticsState` has no error field; `IncompleteCustomRangeException`, D-011, is never surfaced); `DrillDownState.title` is the unlocalised parts joined by ", " (category custom name or lowercase default key, account name, bucket key); a `SettlementRow` in an account's ledger carries the allocation to that account, elsewhere the settlement total, with `accountCount` of the whole settlement (AC-11.7).
|
||||
- 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).
|
||||
@@ -37,9 +38,9 @@ Also from §14.7 (row `android/settings.gradle.kts`, `android/build.gradle.kts`,
|
||||
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.
|
||||
- `android/data/build.gradle.kts` (exists, with Room 3 and KSP): add dependencies `:core:model`, `:domain`, Ktor, kotlinx.serialization, WorkManager, DataStore, Hilt, Coil.
|
||||
- `android/data/src/main/kotlin/se/hagstedt/kvitt/data/db/**`: DAOs, registered on the existing `KvittDatabase` (entities as M2b wrote them).
|
||||
- `android/data/schemas/**`: unchanged; the build must re-export an identical `1.json` (adding DAOs does not change it).
|
||||
- `.../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.
|
||||
@@ -54,7 +55,7 @@ Edit nothing outside these paths; a needed change elsewhere is a contract reques
|
||||
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.
|
||||
4. Check M2b's entities and `1.json` against §14.2.1 and §14.2.2 column by column (report any mismatch as a contract request, do not fix it silently); write the DAOs; confirm `1.json` is unchanged after the build.
|
||||
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.
|
||||
@@ -81,7 +82,7 @@ From SPEC §11.2, verbatim: "none".
|
||||
- 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).
|
||||
- Contract requests (any missing field in §14.2.4, any entity or schema change), 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.
|
||||
|
||||
Reference in New Issue
Block a user