Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
14 KiB
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
- SPEC §6.2 Android app (stack table, module graph, the paragraph on the architecture check).
- SPEC §14.2.4 UI contract: types and repository interfaces in
:core:model. - 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).
- SPEC §9.1 Splits, §9.3 Balances, §9.4 Settlement allocation proposal, §9.6 Limits and validation.
- SPEC §4 FR-14 Analytics (AC-14.2, AC-14.7, AC-14.10) and FR-19 Localisation and formatting.
- SPEC §14.2 Room entities (only to know what
:datawill hold; M5 writes it). - SPEC §11.2 (row M2b), §11.3 (notes M2b, M7x, M8), §14.7 Module layout and ownership map, §14.8.
- SPEC §6.2 "Feature convention (D-017)" and "Fakes" paragraphs, §14.5.4 (analytics package,
resolveRangeexception), §14.7 "Module shells" and "Strings" paragraphs;docs/decisions.mdD-004, D-017, D-018, D-020. - 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;
minSdk36;targetSdkandcompileSdkthe 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 catalogandroid/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
checkand 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
:dataor:domain;:core:fakesonly 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 onjava.time,java.ioorandroid.*imports in:core:modeland:domain(§6.2). :core:modelpackagese.hagstedt.kvitt.modelholds the §14.2.4 types and interfaces verbatim; domain input/output types inse.hagstedt.kvitt.model.money(§14.5.1);:domainpackagese.hagstedt.kvitt.domainwith 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). :domainpasses every vector file incontracts/test-vectors/, includingspend_series.jsonandresolve_range.json(Kotlin only) (§14.5, §11.2).- Strings live in
values/strings.xml(English fallback) andvalues-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, debugse.hagstedt.kvitt.debug, instrumented testsse.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_rangemeans above 10000;resolveRangeedge cases per §14.5.4 (CUSTOMwith a null date throws a typedIllegalArgumentExceptionsubclass 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 underfakes/<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
MaterialThemewith Material 3 defaults inside:app's shell;KvittThemein:core:designis M6's. - (c) Strings:
:appholds only the app name (values/strings.xmlandvalues-sv/strings.xml);:core:designwill hold shared strings, each feature its own; lintMissingTranslationis 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 theNavHost;:appowns theNavHost; repositories are injected through the:core:modelinterfaces, bound in:appto: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, packagese.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):
resolveRangewithCUSTOMand a null start or end throws a typed subclass ofIllegalArgumentExceptiondeclared in:domain; callers map it tovalidation_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) andandroid/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 intocheck.android/core/model/**: §14.2.4 and §14.5.1 types and interfaces, verbatim, plus unit tests for enum wire values andCategoryIcons.ALL.android/domain/**: §14.5.2 functions and §14.5.4 analytics; a vector test runner reading../contracts/test-vectors/*.jsonfor 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/**:Applicationclass,MainActivityshowing an empty themed screen, debugapplicationIdSuffix.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.xmlandvalues-sv/strings.xmlwith the app name only, and lintMissingTranslationas an error in every module, incheck(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
- 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. Copyandroid/local.propertiesfrom the main checkout if it exists (cp /Users/jhag/projects/kvitt/android/local.properties android/); if it does not, create it with the single linesdk.dir=/opt/homebrew/share/android-commandlinetools. Never overwrite an existing one, never commit it. Runtools/install-git-hooks.sh. - Read §2 in order. Confirm
contracts/test-vectors/exists onmain; if not, stop and report. - 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/targetSdkAPI 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). - Generate the Gradle wrapper with the chosen version; keep
android/gradle.propertiesandandroid/gradle/gradle-daemon-jvm.propertiescontent (JDK 25 pin, auto-download off). - Write
build-logicconvention plugins and the architecture check (allowed edges per the §6.2 table; forbidden importsjava.time,java.io,android.*in:core:modeland:domain); add a failing-case test of the check ifbuild-logicsupports it. - Write
:core:modelexactly from §14.2.4 and §14.5.1 (no added fields). Write:domaintest-first against all seven vector files, then implement §14.5.2 and §14.5.4. - Write
:core:fakes(one fake per interface, scenario-driven, empty scenario each) and the module shells. - 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). - Run
python3 tools/dependency-report/latest.pyand record its lines. - 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.kvittto confirm the ids. - 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 onmain. - 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
resolveRangeexception class. installDebugresult on the phone, or "outstanding".- Proposed decision entries and spec text, never written to
SPEC.mdordocs/decisions.mdby this agent; orTODO(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.