3ef2c15764
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
111 lines
14 KiB
Markdown
111 lines
14 KiB
Markdown
# 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.
|