Files

14 KiB
Raw Permalink Blame History

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.