Files
kvitt/PLAN-M2b.md
T

111 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PLAN-M2b: Android skeleton and `:domain`
On record: landed on `main` as `c277ee3`. As built (SPEC v0.4, D-010 to D-014): Roborazzi 1.76.0 on Robolectric 4.17 for screenshots; `IncompleteCustomRangeException`; Room 3 with the entities, `KvittDatabase` v1 and the exported `1.json` in `:data`; JDK 25 test flags and `failOnNoDiscoveredTests` in `build-logic`; `minSdk` 36, `compileSdk`/`targetSdk` 37; the feature convention in `android/README.md`. `installDebug` on the phone is still outstanding (owner). Where this plan and SPEC differ, SPEC wins.
## 1. Goal
Create the Gradle project in `android/` with every module of §6.2, the version catalog, the architecture check, the `:core:model` UI contract and domain types, a `:domain` that passes every shared test vector including analytics, empty fakes, and an `:app` that launches to an empty themed screen. After it lands, M5 (data), M6 (design) and M7a to M7f (features) build against the same interfaces in parallel.
## 2. Read first
1. SPEC §6.2 Android app (stack table, module graph, the paragraph on the architecture check).
2. SPEC §14.2.4 UI contract: types and repository interfaces in `:core:model`.
3. SPEC §14.5 Domain rules as pure functions (14.5.1 Types, 14.5.2 Functions, 14.5.3 Error tokens, 14.5.4 Analytics, 14.5.5 Test-vector format).
4. SPEC §9.1 Splits, §9.3 Balances, §9.4 Settlement allocation proposal, §9.6 Limits and validation.
5. SPEC §4 FR-14 Analytics (AC-14.2, AC-14.7, AC-14.10) and FR-19 Localisation and formatting.
6. SPEC §14.2 Room entities (only to know what `:data` will hold; M5 writes it).
7. SPEC §11.2 (row M2b), §11.3 (notes M2b, M7x, M8), §14.7 Module layout and ownership map, §14.8.
8. SPEC §6.2 "Feature convention (D-017)" and "Fakes" paragraphs, §14.5.4 (analytics package, `resolveRange` exception), §14.7 "Module shells" and "Strings" paragraphs; `docs/decisions.md` D-004, D-017, D-018, D-020.
9. Files: `contracts/test-vectors/*.json`, `android/gradle.properties`, `android/gradle/gradle-daemon-jvm.properties`, `tools/gradle-serial.sh`, `tools/dependency-report/latest.py`, `tools/git-hooks/pre-commit`, `CLAUDE.md` (BUILD AND VERIFY, ARCHITECTURE RULES, ANTI-PATTERNS, THE PHONE).
## 3. Already established
- Kotlin 2.x latest stable; Compose with Material 3; `minSdk` 36; `targetSdk` and `compileSdk` the latest stable API; Room with KSP; Hilt; Ktor client (OkHttp engine) with kotlinx.serialization; WorkManager; kotlinx-datetime; Coil; Vico or Canvas charts (decided by M6); FCM only in `:adapter:fcm`; version catalog `android/gradle/libs.versions.toml`; Gradle daemon on JDK 25 (pinned, existing files); bytecode target Java 17 (§6.2).
- Screenshot tests use Roborazzi or AGP Compose Preview Screenshot Testing, whichever is stable at implementation time; M2b chooses, wires it into `check` and reports the choice with its reasons; the orchestrator records it as D-010 (§6.2).
- Modules and allowed edges exactly as the §6.2 table; features never depend on `:data` or `:domain`; `:core:fakes` only for tests, screenshot tests and debug previews (§6.2, CLAUDE.md Layering).
- The architecture check is a Gradle task in `check`, owned by M2b; it fails on any other dependency edge and on `java.time`, `java.io` or `android.*` imports in `:core:model` and `:domain` (§6.2).
- `:core:model` package `se.hagstedt.kvitt.model` holds the §14.2.4 types and interfaces verbatim; domain input/output types in `se.hagstedt.kvitt.model.money` (§14.5.1); `:domain` package `se.hagstedt.kvitt.domain` with the §14.5.2 functions and §14.5.4 analytics (§14.5).
- Wire value of every enum = lowercase name; icon tokens are Strings from `CategoryIcons.ALL`, the 30 tokens of §9.7 in that order (§14.2.4).
- `:domain` passes every vector file in `contracts/test-vectors/`, including `spend_series.json` and `resolve_range.json` (Kotlin only) (§14.5, §11.2).
- Strings live in `values/strings.xml` (English fallback) and `values-sv/strings.xml`; a lint or test in the gate fails on a key missing from either file (AC-19.1).
- Application ids: release `se.hagstedt.kvitt`, debug `se.hagstedt.kvitt.debug`, instrumented tests `se.hagstedt.kvitt.<module>.test` (CLAUDE.md THE PHONE).
- No mocking libraries, no `!!`, no `@Suppress`, no lint-baseline additions (CLAUDE.md).
- M1 has landed (`c6df2ab`); its readings are explicit in SPEC v0.2: domain functions receive only non-deleted rows; analytics vector keys per §14.5.5; `bp_out_of_range` means above 10000; `resolveRange` edge cases per §14.5.4 (`CUSTOM` with a null date throws a typed `IllegalArgumentException` subclass declared in `:domain`, named by M2b, D-018).
- `:core:model/**` and `:domain/**` become contract files once landed (§14.8).
## 4. Owned paths
From SPEC §14.7 (v0.3):
- `android/settings.gradle.kts`, `android/build.gradle.kts`, `android/gradle.properties`, `android/gradle/**`, `android/build-logic/**` (after landing: "later plans may add catalog entries and module includes (rebase resolves)").
- `android/core/model/**`, `android/domain/**` (after landing: "contract-change plans only").
- `android/core/fakes/**` (after landing: "M7x add files only under `fakes/<feature>/`").
- `android/app/**` up to and including "the M2b shell" (afterwards M8).
- Module shells (D-004), which pass to their owners as soon as M2b lands: build file, manifest and empty string files of `:core:design` (M6), `:data` (M5), `:adapter:fcm` (M8), and of every `:feature:*` (M7a to M7f) with one empty entry composable per feature.
Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries in the report (CLAUDE.md › Orchestration).
Resolved open points (SPEC v0.3; the start prompt may repeat them):
- (a) M2b creates the module shells above, including one empty entry composable per feature module (D-004).
- (b) The empty themed screen uses `MaterialTheme` with Material 3 defaults inside `:app`'s shell; `KvittTheme` in `:core:design` is M6's.
- (c) Strings: `:app` holds only the app name (`values/strings.xml` and `values-sv/strings.xml`); `:core:design` will hold shared strings, each feature its own; lint `MissingTranslation` is an error in every module, set once in the convention plugins (D-017).
- (d) Screenshot tool: M2b chooses Roborazzi or AGP Compose Preview Screenshot Testing, wires it into `check`, and reports the choice with reasons; the orchestrator records it as D-010.
- (e) Feature convention (D-017, §6.2): one composable entry point per screen taking a `hiltViewModel()`-provided ViewModel and navigation as plain lambdas; features never reference other features or the `NavHost`; `:app` owns the `NavHost`; repositories are injected through the `:core:model` interfaces, bound in `:app` to `:data` (fakes only in test and debug-preview source sets). The feature convention plugin applies Hilt and the Hilt navigation Compose dependency.
- (f) The §14.5.4 analytics types live in `:core:model`, package `se.hagstedt.kvitt.model.analytics`; the functions in `:domain` (D-017).
- (g) Empty scenario (D-018): household complete (two named users, no pictures), no accounts, exactly the ten default categories of AC-7.2, no expenses, settlements, templates, rejections or pending changes.
- Also (D-018): `resolveRange` with `CUSTOM` and a null start or end throws a typed subclass of `IllegalArgumentException` declared in `:domain`; callers map it to `validation_failed`. Name the class in the report.
- Also (D-020): no font is bundled; the app uses the Material 3 default typeface.
## 5. Deliverables
- `android/settings.gradle.kts`, `android/build.gradle.kts`: the project with every module of §6.2.
- `android/gradle/libs.versions.toml`: every library and plugin of §6.2 with the versions chosen.
- `android/gradle/wrapper/` (`gradle-wrapper.properties`, `gradle-wrapper.jar`) and `android/gradlew`, `android/gradlew.bat`.
- `android/gradle.properties`, `android/gradle/gradle-daemon-jvm.properties`: the existing JDK 25 pin and flags kept, merged with what the project needs.
- `android/build-logic/**`: convention plugins (pure JVM module, Android library, feature module, application) and the architecture check task wired into `check`.
- `android/core/model/**`: §14.2.4 and §14.5.1 types and interfaces, verbatim, plus unit tests for enum wire values and `CategoryIcons.ALL`.
- `android/domain/**`: §14.5.2 functions and §14.5.4 analytics; a vector test runner reading `../contracts/test-vectors/*.json` for all seven files.
- `android/core/fakes/**`: one fake per repository interface of §14.2.4 driven by a scenario type, with the empty scenario of resolved point (g) each; fakes compute derived values through `:domain`.
- `android/app/**`: `Application` class, `MainActivity` showing an empty themed screen, debug `applicationIdSuffix` `.debug`, manifest.
- Module shells for `:core:design`, `:data`, `:adapter:fcm`, `:feature:{onboarding,home,account,expense,settle,analytics,settings}`, one empty entry composable per feature (D-004).
- `android/app/src/main/res/values/strings.xml` and `values-sv/strings.xml` with the app name only, and lint `MissingTranslation` as an error in every module, in `check` (D-017).
- Screenshot-test tooling wired into `check` (resolved point d) with one screenshot of the empty screen in light and dark.
- The proposed D-010 text for the screenshot tool, in the report (the orchestrator has it recorded).
## 6. Steps
1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m2b -b agent-m2b main`, then in `.claude/worktrees/agent-m2b`: `git reset --hard main`. Copy `android/local.properties` from the main checkout if it exists (`cp /Users/jhag/projects/kvitt/android/local.properties android/`); if it does not, create it with the single line `sdk.dir=/opt/homebrew/share/android-commandlinetools`. Never overwrite an existing one, never commit it. Run `tools/install-git-hooks.sh`.
2. Read §2 in order. Confirm `contracts/test-vectors/` exists on `main`; if not, stop and report.
3. Versions ("latest stable" in §6.2): resolve from official sources and record each with its source: Kotlin (kotlinlang.org releases), Android Gradle Plugin and the latest stable `compileSdk`/`targetSdk` API level (developer.android.com release notes), Gradle wrapper (gradle.org releases; must support JDK 25 daemon and the AGP chosen), Compose BOM, Material 3 (developer.android.com/jetpack/androidx/releases), KSP matching the Kotlin version, Room, Hilt, Ktor, kotlinx.serialization, kotlinx.coroutines, kotlinx-datetime, WorkManager, DataStore, Coil, Roborazzi or the AGP screenshot plugin. Check whether the AGP Compose Preview Screenshot Testing plugin is stable; report the finding for resolved point (d).
4. Generate the Gradle wrapper with the chosen version; keep `android/gradle.properties` and `android/gradle/gradle-daemon-jvm.properties` content (JDK 25 pin, auto-download off).
5. Write `build-logic` convention plugins and the architecture check (allowed edges per the §6.2 table; forbidden imports `java.time`, `java.io`, `android.*` in `:core:model` and `:domain`); add a failing-case test of the check if `build-logic` supports it.
6. Write `:core:model` exactly from §14.2.4 and §14.5.1 (no added fields). Write `:domain` test-first against all seven vector files, then implement §14.5.2 and §14.5.4.
7. Write `:core:fakes` (one fake per interface, scenario-driven, empty scenario each) and the module shells.
8. Write `:app`: themed empty screen, string resources in both languages, missing-translation check, screenshot tooling with one golden in light and dark; open the PNGs before committing (CLAUDE.md Screenshots).
9. Run `python3 tools/dependency-report/latest.py` and record its lines.
10. Run the gate (§7). If the owner's phone is attached and the start prompt names this plan as the one on the phone: `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH tools/gradle-serial.sh installDebug`, after `/opt/homebrew/share/android-commandlinetools/platform-tools/adb shell pm list packages | grep se.hagstedt.kvitt` to confirm the ids.
11. Commit on `agent-m2b`, `git rebase main`, rerun the gate. Never merge, push or remove the worktree.
## 7. Gate
From SPEC §11.2, verbatim: "Android gate".
Command, from `android/`: `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check`.
Green means: `BUILD SUCCESSFUL` in this session; the architecture check, `:core:model` and `:domain` unit tests (all seven vector files), lint with the missing-translation rule, and the screenshot test all ran (task names listed in the report); test counts from fresh XML under `build/test-results/`.
## 8. HUMAN steps
From SPEC §11.2, verbatim: "Plug the phone in once to confirm `installDebug` works".
Where in the flow: after the gate is green (step 10). The orchestrator relays the step; the agent runs the package check and `installDebug` itself and reports the result. If the phone is not attached, report the step as outstanding.
## 9. Out of scope
- Room, DAOs, Ktor client, sync engine, token store, workers, real repositories, APK updater (M5).
- Theme tokens, typography, shared components, charts, `MoneyFormatter`, `DateFormatter` (M6).
- Any feature screen (M7a to M7f) and fake scenarios beyond the empty one (M7x add theirs).
- Navigation graph, Hilt bindings to `:data`, deep links, FCM, notifications, network security config, release signing (M8).
- Server work (M2a and later), `infra/` (M2c-dev).
- Adding fields to §14.2.4 types (contract change).
## 10. Report
- Branch `agent-m2b`, head commit hash after rebasing on `main`.
- Gate output: BUILD line, actionable tasks line, the names of the architecture-check, lint and screenshot tasks that ran, unit test counts from fresh XML (per module).
- Versions chosen (Kotlin, AGP, Gradle, compile/target SDK, every catalog entry) with the official source; dependency-report lines.
- Screenshot tool chosen and why, as the proposed D-010 text.
- How each resolved point (a) to (g) was applied; the name of the `resolveRange` exception class.
- `installDebug` result on the phone, or "outstanding".
- Proposed decision entries and spec text, never written to `SPEC.md` or `docs/decisions.md` by this agent; or `TODO(spec)` occurrences.
- Contract requests, 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.