# PLAN-M2b: Android skeleton and `:domain` ## 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. 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, recorded in decisions (§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..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 (how `CUSTOM` with a null date signals `validation_failed` is for M2b to fix and report). - `:core:model/**` and `:domain/**` become contract files once landed (§14.8). ## 4. Owned paths From SPEC §14.7, verbatim: - `android/settings.gradle.kts`, `android/build.gradle.kts`, `android/gradle.properties`, `android/gradle/**`, `android/build-logic/**` (owner M2b; after landing: "later plans may add catalog entries and module includes (rebase resolves)"). - `android/core/model/**`, `android/domain/**` (owner M2b; after landing: "contract-change plans only"). - `android/core/fakes/**` (owner M2b; after landing: "M7x add files only under `fakes//`"). - `android/app/**` up to and including "the M2b shell" (the row `android/adapter/fcm/**`, `android/app/**` (after the M2b shell) | M8). Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. Open points the orchestrator must settle in the start prompt's notes (stop and ask if they are not settled): (a) "every module of §6.2" means build files in `android/core/design/`, `android/data/`, `android/adapter/fcm/` and `android/feature/*/`, which §14.7 gives to M6, M5, M8 and M7x: whether M2b creates them as empty module shells that ownership then passes on; (b) where the theme of the "empty themed screen" lives, given `:core:design` belongs to M6; (c) which module holds the "string resource files for both languages" M2b creates; (d) which screenshot-test tool, and whether M2b wires it into `check` (needs a decision id); (e) whether feature modules use Hilt (`hiltViewModel`) or receive repositories as parameters, and how cross-feature navigation is expressed, since the feature convention plugin in `build-logic` fixes it for M7a to M7f; (f) the package of the §14.5.4 analytics types (`:core:model` money package or `:domain`); (g) what an "empty scenario" is for repositories whose state has required fields (for example `HomeState.me`). ## 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 an empty scenario 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}` (per open point a). - `values/strings.xml` and `values-sv/strings.xml` with the app name (per open point c), and the missing-translation check (Android lint `MissingTranslation` as an error, or a test) in `check`. - Screenshot-test tooling wired into `check` (per open point d) with one screenshot of the empty screen in light and dark. - `docs/decisions.md` entry for the screenshot tool only if the start prompt grants a decision id and the path (the file is not owned by M2b; otherwise put the text in the report as a contract request). ## 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 open 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 (and the decisions text if no decision id was granted). - How each open point (a) to (g) was resolved, as given in the start prompt. - `installDebug` result on the phone, or "outstanding". - Numbering used (decision ids, spec version) 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.