Files
kvitt/PLAN-M2b.md
T
jacobh2 5e1be38364 Plans and start prompts for every milestone; SPEC v0.2
PLAN-<id>.md for U1, U2, M1, M2a, M2b, M2c-dev, M2c-pi, M3, M4, M5, M6,
M7a-M7f, M8, M9, M10, M11, each with the eleven fixed sections, and a
start prompt per plan in prompts/ plus prompts/README.md (dependency
order and parallel waves).

SPEC v0.2: owner answers D-001 (development-first, M2c split into
M2c-dev and M2c-pi, Gitea registry), D-002 (local backups deleted once
on Drive), D-003 (Splitwise CSV format, English and Swedish variants,
category mapping), OQ-1, OQ-2, OQ-4 answered; M1's readings of
ambiguous contract points stated explicitly in §14.3 to §14.5.
HANDOFF §2 lists the plans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:05:34 +02:00

12 KiB

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.<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 (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/<feature>/").
  • 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.