SPEC v0.11: M7 wave and M6b outcomes (D-069 to D-080), HANDOFF, prompts

Records the readings of M7a to M7f (b9b061a, 6ac711d, 082b8dc, 31760f9,
1e42f76, a54eb7c) and the M6b design-system fix pass (3a1bd14): S-11/S-12
captions, titles and states; navigation wiring and result snackbars (§13.2);
new template day default (AC-9.8); manual update check (AC-16.6); FieldError
field names and settlement editing (AC-11.6); quickRowCategories; two-line
category labels; long day ranges (§9.8); amounts open with two decimals
(§9.6); string keys (strings.csv 578 rows); "Synka nu" online only (§9.9).
§11 marks M7a-M7f and M6b landed, M8 and M7g started; §11.3 lists contract
requests for a later contract pass (M12e). Docs only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 11:44:13 +02:00
parent b24c8b5ba3
commit f4dc10c7ae
6 changed files with 131 additions and 78 deletions
+3 -3
View File
@@ -18,7 +18,7 @@ Assemble the app: `:app` navigation and deep links, Hilt bindings from the `:cor
- Feature convention (D-017): each feature exposes one composable entry point per screen with a `hiltViewModel()`-provided ViewModel and navigation as plain lambdas; `:app` owns the `NavHost` and connects every lambda to a route; fakes are bound only in test and debug-preview source sets.
- Strings (D-017): `:app` holds only the app name; the notification copy and the channel name come from `:core:design`'s shared strings.
- `google-services.json` is in `infra/secrets/` of the main checkout (M2c-dev, D-022); M8 copies it into `android/app/` (git-ignored) with `infra/dev/copy-google-services.sh`.
- Navigation: bottom navigation Home (Översikt), Analytics (Statistik), Settings (Inställningar); FAB "Add expense" on Home and Account detail; account detail, editors, settle up, details and settings sub-screens are pushed screens with back; the expense editor is full screen (§13.2).
- Navigation: bottom navigation Home (Översikt), Analytics (Statistik), Settings (Inställningar); FAB "Add expense" on Home and Account detail; account detail, editors, settle up, details and settings sub-screens are pushed screens with back; the expense editor is full screen (§13.2). Since SPEC v0.11 (D-071): the top-level screens S-3, S-4, S-11 and S-13 draw their own `KvittNavigationBar` and take `onNavigate: (KvittDestination) -> Unit`, so the `:app` scaffold draws no second bar; argument keys are constants in each feature (`ARG_EXPENSE_ID`/`ARG_ACCOUNT_ID`, `SettleArgs.SETTLEMENT_ID`/`ACCOUNT_ID`, `DrillDownArgs.KEYS`, settings `"categoryId"`/`"templateId"`), read through `SavedStateHandle`; result snackbars of S-7, S-9 and account archiving are shown by the screen returned to (Home's `snackbarHostState`).
- Deep links: `kvitt://invite?server=…&code=…` and `https://icehack.asuscomm.com:8443/i/<CODE>` (unverified link) open S-1 with both fields filled (AC-2.6). Only `MainActivity` and the FCM service are exported (§8.8).
- After pairing: full sync, then the `POST_NOTIFICATIONS` request (AC-2.9); partner pairing goes through S-2 before Home (AC-2.10).
- Push handling: enqueue an expedited sync and wait up to 20 s; build the notification from local data in the phone's language per the §13.3 copy, or the generic text when the sync failed or the entity is missing; channel id `activity`, name "Activity"/"Aktivitet", importance default; notification id = hash of `entity_id`, fixed id for `sync_batch`; tap opens S-8 or S-10, Home for deletions and `sync_batch`; unknown `type` or `v` ≠ `1`: sync only (§14.6, AC-13.5, AC-13.6).
@@ -29,8 +29,8 @@ Assemble the app: `:app` navigation and deep links, Hilt bindings from the `:cor
- Version properties `kvitt.versionCode`, `kvitt.versionName` (debug `1` and `dev`); release signing from `android/local.properties` keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias`, `kvitt.release.keyPassword` (§10.6).
- During development the debug build pairs against the server on the Mac over the home Wi-Fi (D-001); development data on that server is disposable.
- Application ids `se.hagstedt.kvitt.debug` (debug), `se.hagstedt.kvitt` (release) (CLAUDE.md).
- M5's contract requests, landed with SPEC v0.5 (§11.3 note M8, D-041): `:app` provides `AppBuildInfo(versionCode, versionName, debug)` to `:data` and includes `DataBindingsModule` (`@DisableInstallInCheck`) in the release graph, while debug binds the fakes; WorkManager is configured with `KvittWorkerFactory` and the default `WorkManagerInitializer` disabled in the manifest; `:app` calls `SyncTriggers.onAppStart`, `SyncTriggers.onForeground` and `SyncTriggers.syncForPush` (push handling, 20 s wait), and `UpdateRepository.check` on cold start and every resume; the `ImageLoader` provided by `:data` is set as Coil's singleton.
- Open follow-up (D-027): on the first sync attempt after a `clock_skew` rejection the phone re-stamps `updated_at` of each such pending row to the current clock before pushing. Not in `:data` at `c28c52b`; the orchestrator assigns it to M8 (as a contract request on `:data`) or to a small `:data` agent.
- M5's contract requests, landed with SPEC v0.5 (§11.3 note M8, D-041): `:app` provides `AppBuildInfo(versionCode, versionName, debug)` to `:data` and includes `DataBindingsModule` (`@DisableInstallInCheck`) in the release graph (done pre-M8 in `e6ad858`, with `AppBuildInfo` from `BuildConfig`); debug binds the fakes through the temporary `android/app/src/debug/.../DebugFakesModule.kt` (`72f21b2`), which M8 deletes so both variants bind `:data`; WorkManager is configured with `KvittWorkerFactory` and the default `WorkManagerInitializer` disabled in the manifest; `:app` calls `SyncTriggers.onAppStart`, `SyncTriggers.onForeground` and `SyncTriggers.syncForPush` (push handling, 20 s wait), and `UpdateRepository.check` on cold start and every resume; the `ImageLoader` provided by `:data` is set as Coil's singleton.
- Open follow-up (D-027): on the first sync attempt after a `clock_skew` rejection the phone re-stamps `updated_at` of each such pending row to the current clock before pushing. Still not in `:data` at `3a1bd14`; since SPEC v0.11 it is on the M12e contract-pass list (§11.3), not M8's.
## 4. Owned paths
From SPEC §14.7, verbatim: `android/adapter/fcm/**`, `android/app/**` (after the M2b shell) (owner M8; after landing: "later integration plans"). Catalog entries and module includes may be added (§14.7, `android/gradle/**` row).
+72 -40
View File
@@ -1,6 +1,6 @@
# Kvitt: specification
Version **0.10** (2026-10-18). Schema version **2**. Sync protocol version **1**. Push payload version **1**.
Version **0.11** (2026-10-18). Schema version **2**. Sync protocol version **1**. Push payload version **1**.
This file is the single source of truth for Kvitt. It is written for implementation agents that have no other context. If code, a plan or `CLAUDE.md` disagrees with this file, this file is right. Words in `code` are literal identifiers, paths, enum values or error codes and must be used exactly as written. Anything not decided here is listed in §12 as `TODO(spec)`; do not invent a value for it.
@@ -213,7 +213,7 @@ The server starts with an empty database. The household is created from the comm
* AC-9.5 A generated expense that a user deletes is not recreated.
* AC-9.6 Each generated expense writes an audit entry with `source` = `recurring` and sends `recurring_expense_created` to every device of both users.
* AC-9.7 The template list shows each template's description, amount, account, day of month, paused state and next occurrence date (computed by the server, `next_occurrence_date`).
* AC-9.8 The editor's default `start_period` is the current month if `day_of_month` is today or later in the phone's time zone, else the next month. The user may choose a `start_period` up to 12 months in the past (backfill) and any month in the future.
* AC-9.8 The editor's default `start_period` is the current month if `day_of_month` is today or later in the phone's time zone, else the next month. A new template's `day_of_month` defaults to today's day of the month, so the default `start_period` is the current month until the user picks an earlier day (D-072). The user may choose a `start_period` up to 12 months in the past (backfill) and any month in the future.
### FR-10 Balances
@@ -230,7 +230,7 @@ The server starts with an empty database. The household is created from the comm
* AC-11.3 Global settle-up (from Home): the direction defaults to "the user who owes pays", the amount defaults to the absolute global balance (or empty when it is zero). The app computes the proposal with `propose_allocation` (§9.4, §14.5) and shows one editable row per proposed account plus "Add account" to add any other account (active or archived). The confirm button is enabled only when the allocations sum to the amount; the remaining difference is shown live.
* AC-11.4 Per-account settle-up (from the account screen) creates a settlement with exactly one allocation for that account; the amount defaults to the absolute account balance and the direction to "the user who owes in this account pays".
* AC-11.5 An allocation larger than the payer's current debt in that account is allowed (over-settling flips the account's sign) and is marked with a warning per row ("This settles more than is owed in <account>"). The confirm dialog repeats the warning.
* AC-11.6 Settlements can be created, edited and deleted (soft delete) offline, by either user, like expenses. When editing, the proposal and warnings are computed with the edited settlement excluded from the balances.
* AC-11.6 Settlements can be created, edited and deleted (soft delete) offline, by either user, like expenses. When editing, the proposal and warnings are computed with the edited settlement excluded from the balances. An edited settlement opens with its saved allocation rows counted as edited by the user, so changing the amount keeps them; "Suggest again" (`settle_propose_again`, "Föreslå igen") re-proposes (D-074).
* AC-11.7 A settlement row in lists shows "<Payer> paid <Recipient> X" and the account count when more than one allocation exists. In an account's list (S-5) X is the settlement's allocation to that account, not the settlement total; everywhere else X is the total.
* AC-11.8 The server's `POST /settlements/proposal` returns exactly what the phone's `propose_allocation` returns for the same server state (verified by shared vectors). The phone always computes locally; the endpoint exists for parity tests and server-side tooling.
@@ -282,6 +282,7 @@ The server starts with an empty database. The household is created from the comm
* AC-16.3 "Update" downloads the APK to the app's cache, verifies the size and SHA-256 from the metadata, and installs through `PackageInstaller`. The first time, Android asks the user to allow "Install unknown apps" for Kvitt; the app explains this beforehand: `UpdateRepository` emits `UpdateState.NeedsInstallPermission` once (the explanation-shown flag in `prefs.preferences_pb`, §14.2.2), and the next `install()` proceeds to the system prompt.
* AC-16.4 A verification failure deletes the file and shows "Download failed, try again".
* AC-16.5 Debug builds never check for updates.
* AC-16.6 "Check for update" on S-22 calls `UpdateRepository.check()`; when `UpdateState` is still `None` afterwards it reports "You have the latest version" (`settings_about_up_to_date`, "Du har den senaste versionen"). A network failure cannot be told apart because `check()` returns nothing; accepted for v1, with a contract request in §11.3 (D-073).
### FR-17 Splitwise import
@@ -431,7 +432,9 @@ Module graph (directory `android/`):
| `:feature:settings` | Android library | Settings hub, profile, categories, recurring templates, devices, invites, about | `:core:model`, `:core:design` |
| `:app` | Android application | `MainActivity`, navigation graph, Hilt wiring, deep links, notification channel and building, `Application` class | everything |
**Screenshot tests (D-010).** `roborazzi.test.verify=true` in `android/gradle.properties` makes every unit-test run, and so `check`, compare renders with the committed goldens. An intended change is re-recorded with `tools/gradle-serial.sh recordRoborazziDebug`, and every changed PNG is opened before committing. AGP's Compose Preview Screenshot Testing plugin was still alpha when M2b chose. Golden layout (D-066, M6): one golden per component sheet (or screen state) × light/dark × sv/en, named `<sheet>_<lang>_<theme>.png`, plus a `_200` variant at 200 % font scale for the long states; the content is composed once and language, theme and font scale are switched through a configuration context, so Compose's non-linear font scaling still applies. Feature modules (M7a to M7f) follow the same layout.
**Screenshot tests (D-010).** `roborazzi.test.verify=true` in `android/gradle.properties` makes every unit-test run, and so `check`, compare renders with the committed goldens. An intended change is re-recorded with `tools/gradle-serial.sh recordRoborazziDebug`, and every changed PNG is opened before committing. AGP's Compose Preview Screenshot Testing plugin was still alpha when M2b chose. Golden layout (D-066, M6): one golden per component sheet (or screen state) × light/dark × sv/en, named `<sheet>_<lang>_<theme>.png`, plus a `_200` variant at 200 % font scale for the long states; the content is composed once and language, theme and font scale are switched through a configuration context, so Compose's non-linear font scaling still applies. Feature modules (M7a to M7f) follow the same layout: each feature module has Roborazzi screenshot tests of its screen states with goldens in `src/test/screenshots/`, rendered full screen at 411 × 891 dp, xhdpi (as landed by M7a to M7f).
**Hilt wiring before M8.** `:app` includes `DataBindingsModule` (D-041) in the release graph since `e6ad858` (pre-M8, with `AppBuildInfo` from `BuildConfig` in both variants). Until M8, debug builds bind every `:core:model` interface to `FakeRepositories(FakeScenarios.empty)` in `android/app/src/debug/.../DebugFakesModule.kt` (`72f21b2`, temporary); M8 deletes that file, so both variants bind `:data`. Fakes stay in test and preview source sets only.
Features never call `:domain` directly: repositories return computed state (balances, previews, proposals), implemented in `:data` with `:domain`. The architecture check (Gradle task in `check`, owned by milestone M2b) fails the build on any other dependency edge and on `java.time`, `java.io` or `android.*` imports in `:core:model` and `:domain`.
@@ -741,7 +744,7 @@ Strings are stored as validated, that is trimmed (§8.6, D-034).
On the phone, a rule of this table that has no domain error token of §14.5.3 (the length limits, the date limits, `day_of_month`, the periods) is reported as `FieldError.error` = `validation_failed` (§14.2.4), the same token as the REST code; a missing required value is `required`.
The phone input field for amounts accepts digits, one decimal separator (`,` or `.`) and at most two decimals, without sign, and converts to öre exactly (string parsing, never floating point).
The phone input field for amounts accepts digits, one decimal separator (`,` or `.`) and at most two decimals, without sign, and converts to öre exactly (string parsing, never floating point). A stored amount opens for editing with both decimals ("1 247,60", "12 400,00"); ratio percent fields keep the trimmed form ("60", "60,5") (D-078; `AmountInput.of` and `AmountInput.percent` in `:core:design`).
### 9.7 Icon and colour tokens
@@ -778,13 +781,14 @@ The server validates icon and colour against these lists (`validation_failed`).
* Dates in lists: Swedish `3 okt.`, English `3 Oct`; with year when not the current year (`3 okt. 2025`, `3 Oct 2025`). Detail views: Swedish `lördag 3 oktober 2026`, English `Saturday, 3 October 2026`. Timeline times: 24-hour `14:02` in both languages.
* Relative "last synced": "just now" (< 1 minute), "N min ago" (< 60 minutes), "N h ago" (< 24 hours), otherwise the list date and time. Swedish: "nyss", "för N min sedan", "för N h sedan".
* Chart axis labels (months): Swedish three-letter forms without a dot (`maj`, `jun`, … `okt`), English `May`, `Jun`, … `Oct`; list dates keep the Swedish abbreviation dot (`3 okt.`, `25 sep.`; `mars`, `maj`, `juni`, `juli` are written out) (D-065).
* Day ranges in captions and summaries (S-11 total caption, S-12 summary) are written out with the year: Swedish `24 augusti–17 oktober 2026`, `1–30 november 2026`, `28 december 2025–3 januari 2026` (the year once when both ends share it, the month once when both ends share it); the period chip and the S-12 title keep the short form `24 aug.–17 okt.` (D-077, extends D-065; `DateFormatter.dayRangeLong`).
* File sizes (the update banner, S-17): SI megabytes (10^6 bytes) with one decimal rounded half up and U+00A0 before `MB`: Swedish `8,4 MB`, English `8.4 MB` (D-065).
* Month and weekday names in Swedish and English are tables inside `DateFormatter`, not platform locale data, so JVM tests and the phone give identical output (D-065).
* Formatting lives in `:core:design` (`MoneyFormatter`, `DateFormatter`), never in `:domain`.
### 9.9 Online-only actions
When the phone has no validated network (Android `NET_CAPABILITY_VALIDATED`) or the last request failed with a network error less than 30 seconds ago, these actions are disabled with "Needs connection": create, edit, archive, unarchive, delete of accounts, categories and recurring templates; profile name and picture; invites; device revoke; in-app update. A REST call that fails with a network error shows "Could not reach the server" and changes nothing locally.
When the phone has no validated network (Android `NET_CAPABILITY_VALIDATED`) or the last request failed with a network error less than 30 seconds ago, these actions are disabled with "Needs connection" ("Kräver anslutning", `common_needs_connection`): create, edit, archive, unarchive, delete of accounts, categories and recurring templates; profile name and picture; invites; device revoke; in-app update; "Sync now" ("Synka nu") on S-8 and S-10 (their missing states) and on S-16 (D-080). A REST call that fails with a network error shows "Could not reach the server" and changes nothing locally.
### 9.10 Other edge cases
@@ -1132,9 +1136,13 @@ Category contract v2 (D-054), after SPEC v0.8:
M12a contracts + :domain suggestCategory (landed 6b60ef7) ─┬─► M12b server (migration 0002, importer table; landed 2e2ebe7)
└─► M12c android (:core:model, :domain, :data, fakes; landed e499107) ─► M6, M7c
M12d UI contract additions (D-059: frequentCategoryIds, deletable; landed bdba9d3) ─► M7a..M7f (ran in parallel with M6, landed 3080804)
After the M7 wave (v0.11):
M7a..M7f (landed) ─► M6b core:design fixes (landed 3a1bd14) ─┬─► M8 app integration (started 2026-10-18)
└─► M7g feature cleanups (started 2026-10-18)
```
After M1, M2a and M2b land, these run in parallel without touching each other's files (§14.7): M3, M4 (lands after M3, D-015), M5, M2c-dev, M6 (once U2 is approved and the §9.7 colour change from U2's proposal has landed in this spec), M7a to M7f (once M6 lands), M9 (once M4 lands). M2c-pi runs after M8 and before M10 (D-001). Group M12 "Category contract v2" (D-049, D-054): M12a first; then M12b and M12c in parallel; M6 and M7c wait for M12c (all three landed: `6b60ef7`, `2e2ebe7`, `e499107`). M12d (D-059) ran in parallel with M6 (landed `bdba9d3` and `3080804`); M7a to M7f started 2026-10-18, in parallel.
After M1, M2a and M2b land, these run in parallel without touching each other's files (§14.7): M3, M4 (lands after M3, D-015), M5, M2c-dev, M6 (once U2 is approved and the §9.7 colour change from U2's proposal has landed in this spec), M7a to M7f (once M6 lands), M9 (once M4 lands). M2c-pi runs after M8 and before M10 (D-001). Group M12 "Category contract v2" (D-049, D-054): M12a first; then M12b and M12c in parallel; M6 and M7c wait for M12c (all three landed: `6b60ef7`, `2e2ebe7`, `e499107`). M12d (D-059) ran in parallel with M6 (landed `bdba9d3` and `3080804`); M7a to M7f ran in parallel and landed on 2026-10-18 (`b9b061a`, `31760f9`, `1e42f76`, `082b8dc`, `6ac711d`, `a54eb7c`), followed by M6b, the `:core:design` fix pass (`3a1bd14`). M8 and M7g (feature cleanups) started 2026-10-18 in parallel (disjoint paths: `android/app/**` and `android/adapter/fcm/**` against `android/feature/**`).
### 11.2 Milestone table
@@ -1156,13 +1164,15 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
| M12c | Category contract v2: Android | Landed `e499107` (D-055 to D-057). `:core:model`: `DefaultCategoryKey` (14), `ColorToken` (`SLATE`, `AMBER`), `CategoryIcons.ALL` (33), `ExpenseRepository.suggestCategory`; `:data`: the `UnicodeText` binding (`java.text.Normalizer`, `Character`), `suggestCategory` from Room, DataStore and the keyword table copied from `contracts/category-keywords.csv` at build time (D-055; a test asserts byte identity and the §14.8 rules), parsed by `:domain`'s `parseCategoryKeywords` (D-056); `:core:fakes`: fourteen defaults in the empty scenario and a fake `suggestCategory`; default-name string resources for the four new categories in English and Swedish (AC-7.2) where default names already live | Android gate | none | M12a, M2b, M5 |
| M12d | UI contract additions (D-059) | Landed `bdba9d3` (D-060 to D-062). `:core:model`: `ExpenseEditorOptions.frequentCategoryIds` and `CategoryRepository.deletable(id)` (§14.2.4 "Changes in v0.9"); `:data`: both computed from Room, with repository tests; `:core:fakes`: both computed from each scenario's own data; `ContractTest` updated | Android gate | none | M12c, M5 |
| M6 | Design system | Landed `3080804` (D-063 to D-066). Built from `docs/ux/design-spec.md` §2 and §3 and `docs/ux/strings.csv`. `:core:design` theme, tokens, typography (Material 3 default typeface, D-020), shared components including the ledger rows and expense editor fields (D-019), charts (Compose `Canvas`, D-063), formatters (§9.8), shared strings (including the notification copy) per `docs/ux/design-spec.md` and `docs/ux/strings.csv`, screenshot tests of every component in light and dark, Swedish and English | Android gate | Look at the component screenshots the agent opens and posts (yes/no) | U2, M2b, M12c, the §9.7 colour change |
| M7a | Feature: onboarding | Started 2026-10-18. `:feature:onboarding` screens S-1, S-2, S-3, S-18 | Android gate with screenshot tests of every state, sv and en, light and dark | none | M6, M12d |
| M7b | Feature: home and account | Started 2026-10-18. `:feature:home`, `:feature:account` screens S-4, S-5, S-6, S-16, S-17 | as M7a | none | M6, M12d |
| M7c | Feature: expense | Started 2026-10-18. `:feature:expense` screens S-7, S-8, with the category suggestion of §9.11 (CS-2, CS-3, CS-7) | as M7a | none | M6, M12c, M12d |
| M7d | Feature: settle | Started 2026-10-18. `:feature:settle` screens S-9, S-10 | as M7a | none | M6, M12d |
| M7e | Feature: analytics | Started 2026-10-18. `:feature:analytics` screens S-11, S-12 | as M7a | none | M6, M12d |
| M7f | Feature: settings | Started 2026-10-18. `:feature:settings` screens S-13, S-14, S-15, S-19, S-20, S-21, S-22 | as M7a | none | M6, M12d |
| M8 | App integration | `:app` navigation and deep links, Hilt bindings from interfaces to `:data` and `:domain`, `:adapter:fcm`, notification building (AC-13.5), update banner wiring, revoke wipe, network security config (§8.5), manifest hardening (§8.8), release signing and version properties (§10.6), `google-services.json` copied with `infra/dev/copy-google-services.sh` | Android gate; on the owner's phone against the development server on the Mac (D-001): pair, add an expense offline, reconnect, see it synced | Tap through the pairing and the offline test as relayed by the orchestrator | M3, M4, M5, M7a-M7f, M2c-dev (Firebase, development server) |
| M6b | Design-system fixes after M7 | Landed `3a1bd14` (D-075 to D-079). `:core:design` fixes the M7 agents asked for: `viewerPercentOf` reads basis points, `editor_split_full_on_you`, `AmountInput.of` with both decimals and `AmountInput.percent` (D-078), `quickRowCategories` (D-075), two-line category labels with an ellipsis (D-076), `DateFormatter.monthName` and `dayRangeLong` (D-077), resource-backed snackbars in `KvittScaffold`, optional `ConfirmDialog` body, `KvittButton` loading form, `InviteCodeField` wrapping, `EmptyState` icon tile, `AnalyticsFilterBar` period row, screen tokens in `KvittDimens`, `common_ok` and `common_undo`, the module keys of M7b to M7f recorded in `docs/ux/strings.csv` (D-079) | Android gate | none | M7a to M7f |
| M7a | Feature: onboarding | Landed `b9b061a` 2026-10-18 (D-071). `:feature:onboarding` screens S-1, S-2, S-3, S-18 | Android gate with screenshot tests of every state, sv and en, light and dark | none | M6, M12d |
| M7b | Feature: home and account | Landed `31760f9` 2026-10-18 (D-071). `:feature:home`, `:feature:account` screens S-4, S-5, S-6, S-16, S-17 | as M7a | none | M6, M12d |
| M7c | Feature: expense | Landed `1e42f76` 2026-10-18 (D-074, D-080). `:feature:expense` screens S-7, S-8, with the category suggestion of §9.11 (CS-2, CS-3, CS-7) | as M7a | none | M6, M12c, M12d |
| M7d | Feature: settle | Landed `082b8dc` 2026-10-18 (D-074). `:feature:settle` screens S-9, S-10 | as M7a | none | M6, M12d |
| M7e | Feature: analytics | Landed `6ac711d` 2026-10-18 (D-069 to D-071). `:feature:analytics` screens S-11, S-12 | as M7a | none | M6, M12d |
| M7f | Feature: settings | Landed `a54eb7c` 2026-10-18 (D-071 to D-073). `:feature:settings` screens S-13, S-14, S-15, S-19, S-20, S-21, S-22 | as M7a | none | M6, M12d |
| M7g | Feature cleanups | Started 2026-10-18. Cleanups in `android/feature/**` after M6b; scope in its brief | as M7a | none | M7a to M7f, M6b |
| M8 | App integration | Started 2026-10-18 (in flight; decisions D-085 to D-089 reserved, renumbered to D-081 onwards on landing). `:app` navigation and deep links (§13.2, D-071), deletion of the debug fakes module (§6.2), Hilt bindings from interfaces to `:data` and `:domain`, `:adapter:fcm`, notification building (AC-13.5), update banner wiring, revoke wipe, network security config (§8.5), manifest hardening (§8.8), release signing and version properties (§10.6), `google-services.json` copied with `infra/dev/copy-google-services.sh` | Android gate; on the owner's phone against the development server on the Mac (D-001): pair, add an expense offline, reconnect, see it synced | Tap through the pairing and the offline test as relayed by the orchestrator | M3, M4, M5, M7a-M7f, M2c-dev (Firebase, development server) |
| M9 | Splitwise importer | Landed `ab298ab` (D-044 to D-046). `kvitt-import` per §10.8 with tests over synthetic CSV fixtures (`crates/kvitt-import/tests/import.rs`), reconciliation against the totals rows, the committed category table | Server gate | Provide the CSV files (kept outside git) when OQ-2 is answered | M2a, M4 |
| M10 | First release | Release build signed, published, both phones paired, push working both ways, nightly backup on Drive, restore drill report, memory check | `docs/validation/first-release.md` and `docs/validation/restore-drill.md` complete; server RSS under 100 MB | Create the release keystore with the `keytool` command of §10.6, outside the repository; install the APK on both phones from the landing page; allow "Install unknown apps"; partner pairs with the invite; confirm notifications arrive | M2c-pi, M3, M4, M8 |
| M11 | Import history | Rehearsed on 2026-10-18: the real run reconciled on both files in a throwaway database (Vardag 1746 rows, Hemma 37 rows; 21 rows unmapped before D-044). Dry run of 2026-10-18 with the M12b mapping (D-053): Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled. Real import run on the Pi per §10.8 "Production run" (D-025), `docs/validation/splitwise-import.md` with per-account totals compared to Splitwise's balances (aggregates only) | Imported balances equal Splitwise's "Total balance" rows per group | Provide the CSVs; confirm the balances on the phone match Splitwise | M9, M10 |
@@ -1173,10 +1183,22 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
* **M2a** owns the router and registers every route up front so that M3 and M4 only fill in handler bodies in their own files. The same for the utoipa `ApiDoc`. M2a creates the shell files of the later server milestones (D-004, §14.7) and the seams of §6.3 (D-015). `server/rust-toolchain.toml` pins the exact current stable Rust release and the Dockerfile uses the same version (§10.3).
* **M2b** writes the `*State` classes and repository interfaces of §14.2.4 so that M5 and M7 work against the same interfaces in parallel. It creates a module shell for every module of §6.2, with one empty entry composable per feature module (D-004), puts the feature convention of §6.2 (D-017) into the feature convention plugin, and fixes how `resolveRange` signals an invalid `CUSTOM` range (§14.5.4, D-018: `IncompleteCustomRangeException`, D-011). Its screenshot-test choice is D-010 (Roborazzi). It also created the Room 3 entities and `KvittDatabase` v1 in `:data` (D-012), so M5 builds on them.
* **M5** owns `:data` and builds on the entities, `KvittDatabase` v1 and the exported schema `android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json` that M2b created (D-012); it adds DAOs, the API client, sync and the repositories. Room 3 migrations take an `SQLiteConnection` (§14.2.3).
* **M7x** features use `:core:fakes` only; they never depend on `:data`. Each feature agent adds its fake scenarios in `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/<feature>/` and nowhere else in that module. Each fills in its own module shell from M2b, follows the feature convention of §6.2 and takes shared UI from `:core:design` (D-019).
* **M7x** features use `:core:fakes` only; they never depend on `:data`. Each feature agent adds its fake scenarios in `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/<feature>/` and nowhere else in that module. Each fills in its own module shell from M2b, follows the feature convention of §6.2 and takes shared UI from `:core:design` (D-019). All six landed on 2026-10-18 (`b9b061a` M7a, `6ac711d` M7e, `082b8dc` M7d, `31760f9` M7b, `1e42f76` M7c, `a54eb7c` M7f); their readings are D-069 to D-074 and D-080, the shared fixes they asked for went into M6b (`3a1bd14`, D-075 to D-079). M7g (started 2026-10-18) does the feature-side cleanups after M6b.
* **M6b** (landed `3a1bd14`) is the `:core:design` fix pass after the M7 wave, in M6's paths (§14.7); see the §11.2 row.
* **Contract requests for a later contract pass (M12e)**, collected from the M5, M6 and M7 reports; none is a contract change in v0.11, each needs the §14.8 rule when taken:
* the `pair()` result lacks the invite kind, so S-2 shows for any first `member` pairing, device invites included (AC-2.4, AC-2.10);
* the `kvitt://invite` link carries no admin name, so `onboarding_code_help` ("You get the code from %1$s …") cannot be filled and S-1 shows the generic help line (AC-2.6);
* `SyncSummary` has no "last attempt failed" flag; `SyncStatusPill` is called with `failed = false` (§13.3 S-4);
* `Rejection` has no actor, so the stale-change copy on S-16 names the partner;
* `Device` has only `appVersionCode`, so S-20 rows show "Kvitt 11" where the design shows the version name "1.1";
* `RecurringRepository.create` needs a `TemplateId` for a new template; the S-19 editor sends `TemplateId("")`;
* `UpdateRepository.check()` has no result, so S-22 cannot tell "up to date" from a network failure (AC-16.6, D-073);
* `SettleState` lacks the list of accounts the settlement leaves untouched; S-9 derives it from Home balances, which include the settlement being edited (D-074);
* the D-027 re-stamp (re-stamp `updated_at` of pending rows after a `clock_skew` rejection) is still not in `:data`;
* the 128 px picture size: `:data` always fetches E-11 at 512; a 128 variant needs a size parameter in `:core:model` and `:data` (§14.2.4 "Pictures"); the owner answers whether it is needed.
* **M2c-dev / M2c-pi** (D-001): M2c-dev builds only what development needs on the Mac and runs now; M2c-pi builds everything that concerns the Pi (registry trust, TLS, router, cert-sync, Pi deploy, APK publishing to the Pi, Drive upload, Gitea Actions) and runs after M8, before M10.
* **M4** (landed `bd6f10a`) added `into_make_service_with_connect_info::<SocketAddr>()` to `serve` so the per-IP rate limits of §8.6 see the peer address (D-032). Its tests are three binaries, `tests/sync_api/`, `tests/push_fcm/` and `tests/scheduler_daily/` (§14.7); the scheduler lives in `src/tasks/scheduler.rs`.
* **M8** is the only plan that edits `:app` after M2b. It also carries M6's contract requests (v0.10, see M6 above), the D-027 re-stamp, and the 128 px picture size question (§14.2.4 "Pictures"). M5's contract requests (v0.5): `:app` provides `AppBuildInfo(versionCode, versionName, debug)` and includes `DataBindingsModule` in the release graph (D-041); WorkManager is configured with `KvittWorkerFactory` and the default initializer disabled; `:app` calls `SyncTriggers.onAppStart`, `onForeground` and `syncForPush`, and `UpdateRepository.check` on cold start and every resume; the `ImageLoader` that `:data` provides is set as Coil's singleton. It owns the `:app` items network security config (§8.5), manifest hardening (§8.8), release signing and version properties (§10.6), and copies `google-services.json` from `infra/secrets/` with `infra/dev/copy-google-services.sh` (§10.7).
* **M8** is the only plan that edits `:app` after M2b. In v0.10 it was given M6's contract requests (see M6 above), the D-027 re-stamp and the 128 px picture size question (§14.2.4 "Pictures"); it keeps the `:app` ones (`LocalAvatarPictures`, `ProvideHousehold`, `LocalKvittClock`, the One UI font check). Since v0.11 the items that need `:core:model` or `:data` (the `SyncSummary` "last attempt failed" field, the D-027 re-stamp, the picture size) are on the M12e list below; M8 wires the navigation of §13.2 (D-071) and deletes the debug fakes module (§6.2). M5's contract requests (v0.5): `:app` provides `AppBuildInfo(versionCode, versionName, debug)` and includes `DataBindingsModule` in the release graph (D-041); WorkManager is configured with `KvittWorkerFactory` and the default initializer disabled; `:app` calls `SyncTriggers.onAppStart`, `onForeground` and `syncForPush`, and `UpdateRepository.check` on cold start and every resume; the `ImageLoader` that `:data` provides is set as Coil's singleton. It owns the `:app` items network security config (§8.5), manifest hardening (§8.8), release signing and version properties (§10.6), and copies `google-services.json` from `infra/secrets/` with `infra/dev/copy-google-services.sh` (§10.7).
* **M10**: the orchestrator relays every HUMAN step in chat. The keystore is created with the command of §10.6 (D-024).
* **M2c-pi**: the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive are requested from the owner at the start of that sprint (D-023).
* **M9** (landed `ab298ab`) reuses the server's own push validation (`kvitt_server::api::validate`, `rows`) and the `SqlAuditWriter`, so imported rows pass the same checks as synced ones (§14.4.4). Its tests are one binary, `crates/kvitt-import/tests/import.rs`, over synthetic fixtures only; one test checks the §10.8 category table against `data/splitwise_categories.csv`.
@@ -1222,6 +1244,8 @@ A brief for one UX agent (Opus) who has read this spec and nothing else. It runs
* A floating action button "Add expense" (Lägg till utgift) on Home and on Account detail.
* Account detail, expense editor, expense detail, settle up, settlement detail and all settings sub-screens are pushed screens with a top app bar and back.
* The expense editor is a full-screen screen, not a bottom sheet (the numeric keyboard needs the room).
* Wiring (D-071, as built by M7a to M7f; M8 connects it): the top-level screens S-3, S-4, S-11 and S-13 render their own `KvittNavigationBar` and take `onNavigate: (KvittDestination) -> Unit`; the `:app` scaffold draws no second bar, and the `NavHost` switches top-level destinations from those lambdas. Navigation-argument keys are constants in each feature (`ARG_EXPENSE_ID` and `ARG_ACCOUNT_ID`; `SettleArgs.SETTLEMENT_ID` and `SettleArgs.ACCOUNT_ID`; `DrillDownArgs.KEYS`; settings `"categoryId"` and `"templateId"`), read through `SavedStateHandle`.
* Result snackbars (D-071): the result of S-7 ("Utgiften sparades"), of S-9 and of archiving an account is shown by the screen returned to (Home shows them through its `snackbarHostState`); the account-archived snackbar has an undo action (`common_undo`, "Ångra").
### 13.3 Screens and what each must show
@@ -1229,28 +1253,28 @@ Each screen must be designed in these states where they apply: **loaded**, **emp
| Id | Screen | Must show | Actions |
|---|---|---|---|
| S-1 | Pairing | App name and one-line purpose; server URL field (prefilled); invite code field (12 characters, shown in groups of 4); display name field ("Ditt namn", 1 to 40 code points, §9.6), required on a first pairing and hidden in the "Para ihop telefonen igen" variant (D-050); explanation of where the code comes from; progress while pairing; errors `invite_invalid`, network error, `rate_limited` | Pair; paste code |
| S-2 | Partner profile step | Display name field (prefilled), avatar with "Add picture"; the owner's name and avatar ("You are joining Jacob") | Continue |
| S-3 | Waiting for partner | Explanation that the partner must join first; the pending invite's expiry if any | Invite partner (goes to S-21); Settings |
| S-4 | Home | Global balance header: both avatars, wording per §9.3, amount; "Settle up" button when non-zero; sync line: "Last synced …" and pending count when > 0, tap opens S-16 (`SyncStatusPill` takes a `failed` flag for "Sync failed" until `SyncSummary` gets a "last attempt failed" field, an M8 contract request; no contract change in v0.10); update banner (S-17) when available; list of active accounts, each with name, balance wording and amount, number of expenses this month; collapsed "Archived (n)" section; empty state when no accounts ("Create your first account") | Add expense (FAB); Settle up; open account; New account |
| S-5 | Account detail | Account name, default split ("50 / 50" with names), balance header; list of expenses and settlements interleaved by date, grouped by month with month totals (household spend), each expense row per AC-8.10, each settlement row per AC-11.7 (its title may wrap to 2 lines, 3 at 200 % font scale, and its second line is the type label `ledger_settlement_label` "Betalning" from `:core:design`, D-068); archived badge if archived (an archived account hides the add-expense FAB, AC-6.4, and keeps "Settle this account", D-052); pending-sync marker per row | Add expense (not on an archived account); Settle this account; Edit account; Archive / Unarchive; Delete (only if allowed) |
| S-6 | Account editor | Name field; default split control (one slider or stepper plus two percent fields that always sum to 100, two decimals allowed); preview "Of 100,00 kr: Jacob 60,00 kr, Anna 40,00 kr"; "Needs connection" state | Save; Cancel |
| S-7 | Expense editor | Amount entry (large, numeric keyboard, `kr`); description; account picker (active accounts); category picker (grid of icon + colour + name, active categories; for a new expense the category is selected from the description per §9.11, with no marker); date (today default, date picker); payer toggle (two avatars: "Paid by"); split section: mode chooser (`default` "Account default 60/40", `ratio` "Custom ratio", `exact` "Exact amounts", `full` "All on one person") and its controls; live preview of each person's share; notes (collapsed by default); validation errors inline; delete in edit mode; after saving, the snackbar "Utgiften sparades" without "Ångra" (an undo would be an audited create and delete that also notifies the partner, D-052) | Save; Cancel; Delete (edit mode) |
| S-8 | Expense detail | Amount, description, category, account, date, payer, each user's share and the split mode, notes, "Recurring: <template>" when generated; timeline (AC-12.4, AC-12.5); "deleted" state when opened from a notification of a deletion | Edit; Delete |
| S-9 | Settle up | Direction ("Anna pays Jacob" with a swap control); amount; allocation list: account name, account debt, editable allocated amount, over-settle warning per row (AC-11.5); remaining-to-allocate indicator; "Add account"; date; note; per-account variant with a single fixed row, which keeps the direction swap (§9.4: per-account settle-ups go either way) and an editable amount (AC-11.4 "defaults to") (D-052) | Confirm; Cancel |
| S-10 | Settlement detail | Payer, recipient, amount, date, note, allocations per account; timeline | Edit; Delete |
| S-11 | Analytics | Range selector (presets of AC-14.2 plus custom); granularity toggle month/week; measure toggle household / my share; group-by toggle category/account; total for range; chart 1 stacked bars over time with legend; chart 2 category breakdown; chart 3 account breakdown; empty state ("No expenses in this period"); a chart whose range has no expenses shows only the 0 baseline, with no gridline values (D-068) | Change filters; tap a bar or a row to open S-12 |
| S-12 | Analytics drill-down | Title naming the filter ("Mat & hushåll, oktober 2026"); total; expense rows as in S-5 | Open expense |
| S-1 | Pairing | App name and one-line purpose; server URL field (prefilled); invite code field (12 characters, shown in groups of 4); display name field ("Ditt namn", 1 to 40 code points, §9.6), required on a first pairing and hidden in the "Para ihop telefonen igen" variant (D-050); explanation of where the code comes from (always the generic help line in v1: the invite link carries no admin name for `onboarding_code_help`, §11.3); progress while pairing; errors `invite_invalid`, network error, `rate_limited` | Pair; paste code |
| S-2 | Partner profile step | Display name field (prefilled; a blank name shows `onboarding_error_name_required` "Skriv ditt namn"), avatar with "Add picture"; the owner's name and avatar ("You are joining Jacob"). Shown after any first `member` pairing until `pair()` reports the invite kind (§11.3) | Continue |
| S-3 | Waiting for partner | Explanation that the partner must join first; the pending invite's expiry if any; error state when fetching the invite failed (snackbar with "Försök igen") | Invite partner (goes to S-21); Settings |
| S-4 | Home | Global balance header: both avatars, wording per §9.3, amount; "Settle up" button when non-zero; sync line: "Last synced …" and pending count when > 0, tap opens S-16 (`SyncStatusPill` takes a `failed` flag for "Sync failed" until `SyncSummary` gets a "last attempt failed" field; Home passes `failed = false`, a contract request for M12e, §11.3); update banner (S-17) when available ("Later" hides it for the ViewModel's lifetime only, so it returns on the next start); list of active accounts, each with name, balance wording and amount, number of expenses this month; collapsed "Archived (n)" section; empty state when no accounts ("Create your first account") | Add expense (FAB); Settle up; open account; New account |
| S-5 | Account detail | Account name, default split with names (ratios written "60/40", `common_ratio`), balance header; list of expenses and settlements interleaved by date, grouped by month with month totals (household spend; month headers are not sticky), each expense row per AC-8.10, each settlement row per AC-11.7 (its title may wrap to 2 lines, 3 at 200 % font scale, and its second line is the type label `ledger_settlement_label` "Betalning" from `:core:design`, D-068); archived badge if archived (an archived account hides the add-expense FAB, AC-6.4, and keeps "Settle this account", D-052); pending-sync marker per row | Add expense (not on an archived account); Settle this account; Edit account; Archive / Unarchive; Delete (only if allowed) |
| S-6 | Account editor | Name field; default split control (one slider or stepper plus two percent fields that always sum to 100, two decimals allowed); preview "Of 100,00 kr: Jacob 60,00 kr, Anna 40,00 kr"; "Needs connection" state; the discard dialog has a title and no body text (D-080) | Save; Cancel |
| S-7 | Expense editor | Amount entry (large, numeric keyboard, `kr`); description; account picker (active accounts); category picker (grid of icon + colour + name, active categories; for a new expense the category is selected from the description per §9.11, with no marker); date (today default, date picker); payer toggle (two avatars: "Paid by"); split section: mode chooser (`default` "Account default 60/40", `ratio` "Custom ratio", `exact` "Exact amounts", `full` "All on one person") and its controls; live preview of each person's share; notes (collapsed by default); validation errors inline; delete in edit mode; after saving, the snackbar "Utgiften sparades" without "Ångra" (an undo would be an audited create and delete that also notifies the partner, D-052), shown by the screen returned to (D-071); the quick row is filled by `quickRowCategories` (D-075) and category labels take up to two centred lines, a longer custom name ending the second line in "…" (D-076); the discard dialog has no body text (D-080) | Save; Cancel; Delete (edit mode) |
| S-8 | Expense detail | Amount, description, category, account, date, payer, each user's share and the split mode, notes, "Recurring: <template>" when generated; timeline (AC-12.4, AC-12.5); "deleted" state when opened from a notification of a deletion; "missing" state (not yet on this phone) with "Synka nu" (`expense_detail_sync_now`), online only (§9.9, D-080) | Edit; Delete |
| S-9 | Settle up | Direction ("Anna pays Jacob" with a swap control); amount; allocation list: account name, account debt, editable allocated amount, over-settle warning per row (AC-11.5); remaining-to-allocate indicator; "Add account"; date; note; per-account variant with a single fixed row, which keeps the direction swap (§9.4: per-account settle-ups go either way) and an editable amount (AC-11.4 "defaults to") (D-052); when editing, the saved rows count as edited and "Föreslå igen" re-proposes; the info line on untouched accounts is derived from Home balances and may be slightly off while editing (D-074); the discard dialog has no body text (D-080) | Confirm; Cancel |
| S-10 | Settlement detail | Payer, recipient, amount, date, note, allocations per account; timeline; "missing" state with "Synka nu" (`settle_detail_sync_now`), online only (§9.9, D-080) | Edit; Delete |
| S-11 | Analytics | Range selector (presets of AC-14.2 plus custom); granularity toggle month/week; measure toggle household / my share; group-by toggle category/account; total for range; chart 1 stacked bars over time with legend; chart 2 category breakdown; chart 3 account breakdown; empty state ("No expenses in this period"); a chart whose range has no expenses shows only the 0 baseline, with no gridline values (D-068). Total card (D-069): caption "<range> · Hushållet" for the household measure and "<range> · N veckor" in week granularity; for my share the label is "Totalt · Min del" so the caption does not repeat it; month presets show whole-month ranges ("maj–okt 2026"), custom ranges and week granularity show days (§9.8, D-077). Legend and stacking order: categories by `sortOrder`, accounts in `AccountRepository.all()` order (`AnalyticsState.groups` order is unspecified, §14.2.4). Loading means `SyncSummary.lastSuccessAt == null` and shows skeleton cards; offline looks like loaded; no error state (analytics are local, AC-14.1) | Change filters; tap a bar or a row to open S-12 |
| S-12 | Analytics drill-down | Title naming the filter ("Mat & hushåll, oktober 2026"; a week as in `docs/ux/strings.csv`, "Vardag, vecka 41"; a category or account over the whole range "<name>, <range caption>") (D-070); total; expense rows as in S-5. No loading and no error state (D-070) | Open expense |
| S-13 | Settings hub | Own avatar and name; partner's avatar and name; rows: Profile, Categories, Recurring expenses, Devices, Invite partner or Pair a device (admin), Sync status, About | Navigate |
| S-14 | Profile | Avatar with change/remove; display name; "Needs connection" state | Save |
| S-15 | Categories | List: icon, colour, name, archived section; editor: name (with "Reset name" for defaults), icon grid (33 icons of §9.7), colour swatches (14 tokens); delete or archive with the reason when delete is not allowed | Add; Edit; Reorder (drag); Archive; Delete |
| S-16 | Sync status | Last successful sync time; pending changes count with a list; "Changes not synced" list from `sync_rejections` with what was lost and why (copy per §14.4.6 reasons); clock-skew warning; "Sync now" | Sync now; open a rejected change (read-only); dismiss it |
| S-16 | Sync status | Last successful sync time; pending changes count with a list (the shared ledger rows of `:core:design`, D-019, not a separate 36 dp row); "Changes not synced" list from `sync_rejections` with what was lost and why (copy per §14.4.6 reasons); clock-skew warning; "Sync now", online only (§9.9, D-080) | Sync now; open a rejected change (read-only); dismiss it |
| S-17 | Update available | Banner on Home with version name; dialog explaining "Install unknown apps" the first time; download progress; failure | Update; Later |
| S-18 | Device removed | Explanation that this phone's access was removed and local data deleted | Pair again (goes to S-1) |
| S-19 | Recurring expenses | List: description, amount, account, "Every month on day 25", next date, paused badge; editor with the expense fields of S-7 (no date) plus day of month, first month, optional last month, pause switch | Add; Edit; Pause/Resume; Delete |
| S-19 | Recurring expenses | List: description, amount, account, "Every month on day 25", next date, paused badge; editor with the expense fields of S-7 (no date) plus day of month (a new template defaults to today's day, AC-9.8, D-072), first month, optional last month, pause switch; the discard dialog has no body text (D-080) | Add; Edit; Pause/Resume; Delete |
| S-20 | Devices | Rows per AC-3.1 | Revoke (with confirmation) |
| S-21 | Invites | For the admin: "Invite partner" (only before the partner joins) and "Pair another device" for either user; a created invite shows the code in groups of 4, the link, expiry, and a share action (Android share sheet); list of pending invites with cancel | Create; Share; Cancel |
| S-22 | About | App version name and code, server URL, server version (from `/health`), open-source licences | Check for update |
| S-22 | About | App version name and code, server URL, server version (from `/health`), open-source licences; after "Check for update" with no update, "Du har den senaste versionen" (AC-16.6, D-073) | Check for update |
System notifications (copy needed in both languages, built per AC-13.5):
@@ -1294,10 +1318,10 @@ Suggested starting points the UX agent may use, replace or merge (it must delive
2. **"Skärgård"**: Material 3 Expressive with a cool sea-and-granite palette, large rounded shapes, bold balance header, playful but quiet.
3. **"Kontor"**: dense, utilitarian, data-forward. Neutral greys, strong typographic hierarchy, charts as the hero on Analytics, minimal decoration.
**Phase 2 (U2): design spec for the chosen direction.** After the owner picks (and comments), in `docs/ux/`. The owner picked direction 3 "Kalas" with the muted palette of `docs/ux/directions/3-kalas/`, typeface per D-020 (D-047). U2 landed `16c4d01`, approved by the owner. `docs/ux/design-spec.md` is the binding design document for `:core:design` (§2 tokens, §3 components) and for every screen (§4, §5); this spec wins over it on rules. `docs/ux/strings.csv` (558 rows at U2, header included; 559 since v0.10 with `ledger_settlement_label`, D-068, and English "Dining" for `category_default_restaurants`, D-067; keys with the D-017 prefixes; plurals as `key:one` / `key:other`) is the source of every string resource:
**Phase 2 (U2): design spec for the chosen direction.** After the owner picks (and comments), in `docs/ux/`. The owner picked direction 3 "Kalas" with the muted palette of `docs/ux/directions/3-kalas/`, typeface per D-020 (D-047). U2 landed `16c4d01`, approved by the owner. `docs/ux/design-spec.md` is the binding design document for `:core:design` (§2 tokens, §3 components) and for every screen (§4, §5); this spec wins over it on rules. `docs/ux/strings.csv` (558 rows at U2, header included; 559 since v0.10 with `ledger_settlement_label`, D-068, and English "Dining" for `category_default_restaurants`, D-067; 578 since v0.11 with the shared keys `common_ok`, `common_undo` and `editor_split_full_on_you` and the module keys of M7b to M7f recorded as built, D-079; keys with the D-017 prefixes; plurals as `key:one` / `key:other`) is the source of every string resource:
* `design-spec.md`: final tokens (colour roles light and dark, category colour hex values, typography scale, shapes, spacing scale in dp, elevation, motion durations and easing), component inventory (each with anatomy, states, sizes), and for every screen S-1 to S-22: layout description, all states of §13.3, interaction details, TalkBack labels.
* `strings.csv`: every user-visible string of the app with columns `key,en,sv,notes`, keys in `snake_case` prefixed by the feature module that holds them (`home_balance_owes_you`, `expense_save`; shared strings for `:core:design` never start with a feature name; the app name is the only `:app` string; §6.2, D-017), placeholders as Android format arguments (`%1$s`), plurals marked in `notes` with their quantities. This becomes `values/strings.xml` and `values-sv/strings.xml` in M6 and M7; feature agents add no strings that are not in this file without a decisions entry.
* `strings.csv`: every user-visible string of the app with columns `key,en,sv,notes`, keys in `snake_case` prefixed by the feature module that holds them (`home_balance_owes_you`, `expense_save`; shared strings for `:core:design` never start with a feature name; the app name is the only `:app` string; §6.2, D-017), placeholders as Android format arguments (`%1$s`), plurals marked in `notes` with their quantities. This becomes `values/strings.xml` and `values-sv/strings.xml` in M6 and M7; feature agents add no strings that are not in this file without a decisions entry. Keys recorded by D-079 (v0.11): shared `common_ok`, `common_undo`, `editor_split_full_on_you` ("All on you" / "Allt på dig"; `ledger_settlement_label` was D-068); module keys as built: `analytics_granularity`, `analytics_measure`, `analytics_group_by`, `analytics_range_apply`; `settle_row_amount`, `settle_detail_delete`, `settle_detail_sync_now`; `expense_detail_sync_now`; `home_archived_expanded`, `home_archived_collapsed`; `settings_category_archived_snackbar`, `settings_category_unarchived_snackbar`, `settings_recurring_summary_none`, `settings_expanded`, `settings_collapsed`, `settings_partner_missing`.
* `screens/<S-id>-<slug>.png` for every screen and state, light and dark, Swedish.
* Updated colour hex values for the 14 tokens of §9.7 (the muted Kalas values, `slate` and `amber` included), proposed as a spec change (decisions entry, spec version bump) by the orchestrator (D-051).
@@ -1903,6 +1927,13 @@ Readings fixed in v0.10 (D-060 to D-062, as landed by M12d `bdba9d3`; no type ch
* **`frequentCategoryIds` keys and ties (D-061).** The map has a key for every active account (an empty list when nothing was used there) and no key for an archived account. Ties: use count descending, then `sort_order` ascending, then category id ascending, so Room and the fakes give the identical order.
* **`deletable` edge cases (D-062).** `CategoryRepository.deletable(id)` emits false for an unknown or already deleted id. Archived categories follow the same AC-7.6 rule as active ones. In `:core:fakes`, `delete` and `deletable` share one rule function, so a fake never offers a delete it then refuses.
Readings fixed in v0.11 (D-069, D-073 to D-075, as landed by M7a to M7f and M6b; no type changes):
* **`AnalyticsState.groups` order (D-069).** Unspecified; the UI does not rely on it. S-11 sorts legends and stacks itself: categories by `sortOrder`, accounts in `AccountRepository.all()` order.
* **`FieldError.field` values (D-074).** The model property names: `amountOre`, `description`, `notes`, `date`, `split`, `accountId`, `categoryId`, `allocations`, as `:core:fakes` and `:data` emit them.
* **`UpdateRepository.check()` (D-073).** Returns nothing; S-22 reads `UpdateState` afterwards and treats a still-`None` state as "up to date". A result type is a contract request (§11.3).
* **Quick row fill (D-075).** The D-060 fill lives in `:core:design`'s `quickRowCategories(categories, frequent, selected)`; editors pass the raw `frequentCategoryIds[accountId]`. An empty or short list is filled from `sort_order`; the selected category takes the fifth place when absent. This replaces `docs/ux/design-spec.md` §3.7's interim "selected first, then `sort_order`" and the v0.9 fallback above.
Readings fixed in v0.5 (no type changes):
* **`OnlineResult.Failed("network", null)`.** A REST call that fails on the network (no answer from the server) returns code `network`, the one token outside §14.3.2, so the UI can show "Could not reach the server" (§9.9) rather than the `Offline` state, which means the call was not attempted (D-029).
@@ -2591,15 +2622,15 @@ Ownership (an agent edits only paths its milestone owns; anything else needs the
| `android/settings.gradle.kts`, `android/build.gradle.kts`, `android/gradle.properties`, `android/gradle/**`, `android/build-logic/**`, the module shells (build file, manifest, one empty entry composable per feature) of `:core:design`, `:data`, `:adapter:fcm` and every `:feature:*` (D-004) | M2b | later plans may add catalog entries and module includes (rebase resolves) |
| `android/core/model/**`, `android/domain/**` | M2b | contract-change plans only |
| `android/core/fakes/**` | M2b | M7x add files only under `fakes/<feature>/` |
| `android/core/design/**` | M6 | M7x may not edit; requests go through the orchestrator |
| `android/core/design/**` | M6 (fix pass M6b, landed `3a1bd14`) | M7x may not edit; requests go through the orchestrator; later design plans |
| `android/data/**` | M5 (M2b created the Room 3 entities, `KvittDatabase` v1 and the exported schema, D-012) | later data plans; the exported schema is a contract file (§14.8) |
| `android/adapter/fcm/**`, `android/app/**` (after the M2b shell) | M8 | later integration plans |
| `android/feature/onboarding/**` | M7a | |
| `android/feature/home/**`, `android/feature/account/**` | M7b | |
| `android/feature/expense/**` | M7c | |
| `android/feature/settle/**` | M7d | |
| `android/feature/analytics/**` | M7e | |
| `android/feature/settings/**` | M7f | |
| `android/feature/onboarding/**` | M7a | M7g (feature cleanups), later feature plans |
| `android/feature/home/**`, `android/feature/account/**` | M7b | M7g (feature cleanups), later feature plans |
| `android/feature/expense/**` | M7c | M7g (feature cleanups), later feature plans |
| `android/feature/settle/**` | M7d | M7g (feature cleanups), later feature plans |
| `android/feature/analytics/**` | M7e | M7g (feature cleanups), later feature plans |
| `android/feature/settings/**` | M7f | M7g (feature cleanups), later feature plans |
| `infra/**` | M2c-dev, then M2c-pi (D-001) | M10 updates runbooks |
| `.gitea/workflows/**` | M2c-pi (D-023) | later infra plans |
| git-ignored `infra/secrets/**` in the main checkout | M2c-dev (D-022), then M2c-pi | M8 copies `google-services.json` from it (§10.7) |
@@ -2653,3 +2684,4 @@ Rule: a change to any other contract file, or a semantic change to `contracts/op
| 0.8 | 2026-10-18 | Decisions from the owner's UX session (`docs/ux/proposed-decisions.md` UX-1 to UX-8 on branch `UX-sprint`). D-047: direction 3 "Kalas" with the muted palette, typeface per D-020 (§9.7, §13.4). D-048: navigation stays at three destinations, an "Aktivitet" tab not taken for v1; §13.2 binding. D-049: category suggestion from the description, new §9.11 (CS-1 to CS-7: new expenses only, manual choice wins, history most often then first token, keyword table, AC-8.6 default, no marker), AC-8.6, `ExpenseRepository.suggestCategory` (§14.2.4), Kotlin-only `suggestCategory` in `:domain` with the `UnicodeText` interface bound by `:data` and `suggest_category.json` as landed by M12a (`6b60ef7`) (§14.5.5, §14.5.6), contract file `contracts/category-keywords.csv` (§14.8). D-050: S-1 display-name field on a first pairing (AC-2.4, §13.3). D-051: §9.7 hex values become the muted Kalas values from U2, token names fixed, `slate` and `amber` `TODO(U2)`. D-052: "Utgiften sparades" without undo (S-7), archived account hides the FAB (S-5), per-account settle-up keeps the swap and an editable amount (S-9), avatar tokens `avatar-1` and `avatar-2` (AC-4.5), Swedish effect copy (AC-8.10). D-053: finer Splitwise mapping (§10.8). D-054: fourteen default categories (`housing`, `insurance`, `children`, `fees` added; AC-7.2), icons `key`, `shield`, `account_balance` (33), colours `slate`, `amber` (14), server schema 2 through migration 0002 rebuilding `categories` (§14.1.1, locked as `contracts/schema/migrations/0002_categories_v2.sql`, copied from M12a's file; no `server-v2.sql`, §14.8), importer schema check 2 (§10.8), enums in §14.2.4 and §14.3.3, E-1 `schema_version` 2; Room schema unchanged. New milestone group M12 "Category contract v2" (M12a contracts, then M12b server and M12c Android in parallel; M6 and M7c wait for M12c) (§11). |
| 0.9 | 2026-10-18 | Outcomes of M12b (`2e2ebe7`) and M12c (`e499107`), U2 (landed `16c4d01`) and UX-9. D-055: the keyword table is copied from `contracts/category-keywords.csv` at build time by `:data:copyCategoryKeywords` and `:core:fakes:copyCategoryKeywords` (AGP generated resource directory), a missing file fails the build, packaged as `se/hagstedt/kvitt/data/category-keywords.csv` (and the fakes path in debug), no committed copy, a `:data` test asserts byte identity and the §14.8 rules; supersedes the `android/data/src/main/resources` copy (§11.2 M12c, §14.2.4, §14.7, §14.8). D-056: pure `parseCategoryKeywords(csv)` in `:domain`, shared by `:data` and `:core:fakes` (§14.5.6). D-057: `DefaultCategoryKey` declared in AC-7.2 order; the Rust wire enum, the OpenAPI enum and the 0002 `CHECK` list the ten v1 keys then the four new ones; wire values unaffected (§14.2.4). D-058: test harnesses seed users after the fourteen category seeds (`server_version` 15 and 16); `E-7-me/response.json` shows 15 and 16 and its test compares them again (§14.3.5). D-051 fulfilled: §9.7 hex values from `docs/ux/design-spec.md` §2.3; `design-spec.md` is the binding design document and `strings.csv` (558 rows, D-017 prefixes, plurals as `key:one`/`key:other`) the source of string resources (§13.4). D-059: UX-9 accepted as a contract change: `ExpenseEditorOptions.frequentCategoryIds` and `CategoryRepository.deletable(id)` with their readings and fallbacks (§14.2.4), new milestone M12d (§11, §14.7); M7a to M7f depend on M6 and M12d. Clarifications without a decision id: `suggestCategory` history is every non-deleted expense (pending included, all accounts, both users), active categories neither archived nor deleted, fallback last-used in the account if active else `other`, null account gives `other`; when `other` is archived and no active last-used exists the result is null and the editor shows no category until the user picks one (AC-8.6, §9.11 CS-5, §14.2.4); stored `expense_date` values are valid ISO dates (§9.6), so parsing cannot fail. §11.2: M12a, M12b, M12c, U1, U2 landed, M6 started 2026-10-18; dry run of 2026-10-18 (Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled) (§11.2, §11.3). §10: development stack migrated to schema 2 on 2026-10-18 after a backup. PLAN-M12b: bundled SQLite is 3.51.x (3.51.3), not 3.50. |
| 0.10 | 2026-10-18 | Outcomes of M12d (`bdba9d3`) and M6 (`3080804`) and two owner-approved string decisions. D-060: `frequentCategoryIds` holds only categories used in the account (possibly fewer than five or empty); filling to five and placing the selected category fifth stays in the editor; not a contract change (§14.2.4). D-061: a key for every active account (empty list when unused), none for archived accounts; ties by count, `sort_order`, then category id (§14.2.4). D-062: `deletable` is false for an unknown or deleted id; archived categories follow AC-7.6; fake `delete` and `deletable` share one rule (§14.2.4). D-063: charts on Compose `Canvas` in `:core:design`, not Vico (3.3.1 cannot meet design-spec §3.10); Vico stays in the catalog unused, removable later (§6.2). D-064: Material 3 Expressive against material3 1.4.0: `ShortNavigationBar` used; `MaterialShapes`, `ButtonGroup`, `LoadingIndicator`, `LinearWavyProgressIndicator`, medium extended FAB, button sizes, segmented list items, expressive slider, `MotionScheme.expressive()` and the increased shapes take fallbacks; fulfils design-spec §6 open point 5 (§6.2). D-065: sv and en month and weekday tables in `DateFormatter`; chart labels without dots; file sizes in SI megabytes with one decimal (§9.8). D-066: golden layout per component sheet × light/dark × sv/en, plus 200 % for long states, switched through a configuration context; M7a to M7f follow it (§6.2). D-067: English default name of `restaurants` is "Dining" (AC-7.2, `strings.csv`, `:core:design`). D-068: shared `ledger_settlement_label` ("Payment" / "Betalning") for the settlement row's type label; settlement row titles may wrap to 2 lines (3 at 200 %); an empty chart range shows only the 0 baseline (§13.3, §13.4). Without a decision id: `:core:design` described as built (§6.2); `SyncStatusPill` takes a `failed` flag until `SyncSummary` gets a "last attempt failed" field (§13.3, M8 request); M6's M8 contract requests `LocalAvatarPictures`/`AvatarPictures` (authenticated E-11 request from user id and `pictureVersion`), `ProvideHousehold(me, partner)`, `LocalKvittClock`; design-spec §2.7 One UI font check at the M8 install (§11.3). §11.2: M12d and M6 landed, M7a to M7f started 2026-10-18. |
| 0.11 | 2026-10-18 | Outcomes of the M7 wave (M7a `b9b061a`, M7e `6ac711d`, M7d `082b8dc`, M7b `31760f9`, M7c `1e42f76`, M7f `a54eb7c`) and the design-system fix pass M6b (`3a1bd14`); owner approved in advance. D-069: S-11 total caption "<range> · Hushållet" / "<range> · N veckor", label "Totalt · Min del" for my share, whole-month ranges for month presets and days otherwise, legend and stacking by `sortOrder` and `AccountRepository.all()` order, `AnalyticsState.groups` order unspecified, loading = `lastSuccessAt == null` with skeleton cards, offline looks like loaded (§13.3, §14.2.4). D-070: S-12 week title per `strings.csv` ("Vardag, vecka 41"), whole-range title "<name>, <range caption>", no error state on S-11/S-12, no loading state on S-12 (§13.3). D-071: top-level screens S-3, S-4, S-11, S-13 render their own `KvittNavigationBar` with `onNavigate`, no second bar in `:app`; navigation-argument keys are feature constants read through `SavedStateHandle`; result snackbars shown by the screen returned to, account-archived snackbar with undo (§13.2). D-072: a new template's day of month defaults to today (AC-9.8, S-19). D-073: S-22 "Sök efter uppdatering" reports "Du har den senaste versionen" when `UpdateState` stays `None`; network failure indistinguishable, accepted for v1 (new AC-16.6, §14.2.4, §11.3). D-074: `FieldError.field` values are the model property names; an edited settlement keeps its saved rows, "Föreslå igen" re-proposes; S-9's untouched-accounts line derives from Home balances (AC-11.6, §13.3, §14.2.4). D-075: the D-060 fill is `:core:design`'s `quickRowCategories`, replacing design-spec §3.7's interim rule (§14.2.4). D-076: category labels up to two centred lines, long custom names end in "…" (§13.3 S-7). D-077: day ranges in captions and summaries written out with the year, chip and S-12 title keep the short form (§9.8). D-078: stored amounts open for editing with both decimals, ratio percents trimmed (§9.6). D-079: shared keys `common_ok`, `common_undo`, `editor_split_full_on_you` and the M7b–M7f module keys recorded in `strings.csv` (578 rows, header included) (§13.4). D-080: "Synka nu" on S-8, S-10 and S-16 is online only; discard dialogs of S-6, S-7, S-9, S-19 have no body text (§9.9, §13.3). Without a decision id: §6.2 `:app` Hilt wiring before M8 (release binds `:data` since `e6ad858`, the debug fakes module `72f21b2` is deleted by M8) and feature screenshot tests full screen at 411 × 891 dp xhdpi; §13.3 S-1 generic help line, S-2 blank-name error, S-3 error state, S-5 non-sticky month headers and "60/40" ratios (`common_ratio`), S-16 pending rows use the shared ledger rows, "Senare" hides the update banner for the ViewModel's lifetime; §11.3 contract requests for a later contract pass (M12e). §11: M7a–M7f and M6b landed, M8 and M7g started 2026-10-18 (M8 holds D-085 to D-089, renumbered to D-081 onwards on landing); §14.7 owners after landing. |
+5 -5
View File
@@ -11,12 +11,12 @@ compile, unit tests (including every vector of `contracts/test-vectors/`), lint
|---|---|---|
| `:core:model` | `kvitt.jvm.library` | UI contract (§14.2.4), domain types `model.money` (§14.5.1), analytics types `model.analytics` (§14.5.4) |
| `:domain` | `kvitt.jvm.library` | §14.5 functions; `VectorTest` runs every vector file, copied by `:domain:copyTestVectors` |
| `:core:design` | `kvitt.android.library` + `kvitt.android.compose` | `KvittTheme`, tokens, shared components, `Canvas` charts, formatters and shared strings (M6, `3080804`; SPEC §6.2); M6 owns the module |
| `:core:fakes` | `kvitt.android.library` | `FakeRepositories(scenario)`, `FakeScenarios.empty`; scenarios of feature X go in `fakes/<x>/` |
| `:core:design` | `kvitt.android.library` + `kvitt.android.compose` | `KvittTheme`, tokens, shared components, `Canvas` charts, formatters and shared strings (M6, `3080804`; fixes M6b, `3a1bd14`; SPEC §6.2); M6 owns the module |
| `:core:fakes` | `kvitt.android.library` | `FakeRepositories(scenario)`, `FakeScenarios.empty`; scenarios of feature X go in `fakes/<x>/` (added by M7a to M7f) |
| `:data` | `kvitt.android.library` + KSP + Room 3 | Room entities and `KvittDatabase` v1, schema in `data/schemas/`; M5 owns the module |
| `:adapter:fcm` | `kvitt.android.library` | shell; M8 owns it |
| `:feature:*` | `kvitt.android.feature` | shells with one entry composable; M7a to M7f own them |
| `:app` | `kvitt.android.application` + compose + hilt + Roborazzi | Application, MainActivity, goldens in `app/src/test/screenshots/` |
| `:feature:*` | `kvitt.android.feature` | screens built by M7a to M7f (landed 2026-10-18), goldens in `feature/<x>/src/test/screenshots/` (full screen 411 × 891 dp, xhdpi); top-level screens draw their own `KvittNavigationBar` (SPEC §13.2, D-071); M7g does the cleanups after M6b |
| `:app` | `kvitt.android.application` + compose + hilt + Roborazzi | Application, MainActivity, goldens in `app/src/test/screenshots/`; release binds `:data` (`DataBindingsModule`, `e6ad858`), debug binds the fakes in `src/debug/.../DebugFakesModule.kt` (`72f21b2`) until M8 deletes it; M8 owns the module |
## Architecture check
@@ -45,7 +45,7 @@ debug configuration, and, in `:core:model` and `:domain`, on an Android plugin,
A stateless overload that takes the `*State` and event lambdas is what previews and screenshot tests render.
* ViewModels are `@HiltViewModel` classes that receive repositories **through the interfaces of `:core:model`** only.
`:app` binds those interfaces to `:data` (release) or `:core:fakes` (debug and tests) with Hilt (M8).
`:app` binds those interfaces to `:data` with Hilt (release since `e6ad858`, debug too once M8 deletes the temporary debug fakes module); tests and previews use `:core:fakes`.
* Features never reference another feature, the `NavHost` or `:data`/`:domain`. `:app` owns the `NavHost` and wires
the lambdas.
* Strings live in the feature's own `src/main/res/values/strings.xml` and `values-sv/strings.xml`, keys prefixed with
+25 -19
View File
@@ -3,40 +3,46 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- `main` at `3080804` (M6 design system, on top of `bdba9d3` M12d) plus the SPEC v0.10 docs commit from `agent-spec10` once it lands. History: `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `fad6fc9` M2b, `fdf2093` M2a, `040bf41` SPEC v0.4, `503cee5` M3, `c28c52b` M5, `982451d` SPEC v0.5, `bd6f10a` M4, `23a2f1e` SPEC v0.6, `ab298ab` M9, `e8ee518` SPEC v0.7, `6b60ef7` M12a, `7fb9f6f` SPEC v0.8, `e499107` M12c, `2e2ebe7` M12b, `d452fa6` UX Kalas categories and tokens, `16c4d01` UX sprint U2, `45eecd1` SPEC v0.9, `bdba9d3` M12d, `3080804` M6.
- Landed: M0 spec, M1 contracts and vectors (`c6df2ab`), M2a server skeleton and `kvitt-domain` (`fdf2093`), M2b Android skeleton and `:domain` (`fad6fc9`), M2c-dev infra for development (`c885aba`), M3 server core API (`503cee5`), M4 server sync, audit, push, scheduler (`bd6f10a`), M5 Android data layer (`c28c52b`), M9 Splitwise importer (`ab298ab`), M12a (`6b60ef7`), M12b (`2e2ebe7`), M12c (`e499107`), U1 and U2 (`16c4d01`, owner approved), SPEC v0.9 (`45eecd1`, D-055 to D-059), M12d UI contract additions (`bdba9d3`, D-060 to D-062), M6 design system (`3080804`, D-063 to D-066).
- With `agent-spec10`: SPEC v0.10. D-060 `frequentCategoryIds` holds only used categories, the editor fills to five; D-061 a key per active account, none for archived, ties by count, `sort_order`, category id; D-062 `deletable` false for unknown or deleted ids, archived follow AC-7.6, fake `delete` and `deletable` share one rule; D-063 charts on Compose `Canvas`, Vico unused in the catalog (removable later); D-064 Expressive fallbacks against material3 1.4.0 (only `ShortNavigationBar` used); D-065 month and weekday tables in `DateFormatter`, chart labels without dots, SI megabytes with one decimal (§9.8); D-066 golden layout (sheet × light/dark × sv/en, +200 %), M7a–M7f follow it; D-067 English "Dining" for `category_default_restaurants` (`strings.csv`, `:core:design`, AC-7.2); D-068 shared `ledger_settlement_label` ("Payment" / "Betalning"), settlement titles wrap to 2 lines (3 at 200 %), an empty chart shows only the 0 baseline (§13.3).
- `main` at `3a1bd14` (M6b design-system fixes after the M7 wave) plus the SPEC v0.11 docs commit from `agent-spec11` once it lands. History: `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `fad6fc9` M2b, `fdf2093` M2a, `040bf41` SPEC v0.4, `503cee5` M3, `c28c52b` M5, `982451d` SPEC v0.5, `bd6f10a` M4, `23a2f1e` SPEC v0.6, `ab298ab` M9, `e8ee518` SPEC v0.7, `6b60ef7` M12a, `7fb9f6f` SPEC v0.8, `e499107` M12c, `2e2ebe7` M12b, `d452fa6` UX Kalas categories and tokens, `16c4d01` UX sprint U2, `45eecd1` SPEC v0.9, `bdba9d3` M12d, `3080804` M6, `7bd0aa1` SPEC v0.10, `72f21b2` app debug fakes bindings (temporary), `e6ad858` app release wiring of `:data` (pre-M8), `b9b061a` M7a, `6ac711d` M7e, `082b8dc` M7d, `31760f9` M7b, `1e42f76` M7c, `a54eb7c` M7f, `3a1bd14` M6b.
- Landed: M0 spec, M1 contracts and vectors (`c6df2ab`), M2a (`fdf2093`), M2b (`fad6fc9`), M2c-dev (`c885aba`), M3 (`503cee5`), M4 (`bd6f10a`), M5 (`c28c52b`), M9 (`ab298ab`), M12a (`6b60ef7`), M12b (`2e2ebe7`), M12c (`e499107`), U1 and U2 (`16c4d01`, owner approved), SPEC v0.9 (`45eecd1`), M12d (`bdba9d3`), M6 (`3080804`), SPEC v0.10 (`7bd0aa1`, D-060 to D-068), M7a onboarding (`b9b061a`), M7e analytics (`6ac711d`), M7d settle (`082b8dc`), M7b home and account (`31760f9`), M7c expense (`1e42f76`), M7f settings (`a54eb7c`), M6b `:core:design` fixes (`3a1bd14`).
- With `agent-spec11`: SPEC v0.11, D-069 to D-080 (owner approved in advance). D-069 S-11 caption, legend order, loading = no successful sync yet; D-070 S-12 titles, no error or loading state; D-071 top-level screens draw their own `KvittNavigationBar`, feature argument-key constants, result snackbars on the screen returned to (archive with undo); D-072 new template day = today; D-073 "Du har den senaste versionen" when `UpdateState` stays `None`; D-074 `FieldError.field` names, edited settlement keeps its rows, "Föreslå igen"; D-075 `quickRowCategories` in `:core:design`; D-076 two-line category labels with "…" for long custom names; D-077 long day ranges with the year; D-078 amounts open with two decimals; D-079 new string keys, `strings.csv` 578 rows; D-080 "Synka nu" online only, discard dialogs without body. §11.3 has the list of contract requests for a later contract pass (M12e).
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test` (from `server/`). Both are green on `main` as far as reported.
- Worktrees in flight under `.claude/worktrees/`: `agent-m7a` to `agent-m7f` (M7a to M7f, started 2026-10-18, in parallel, disjoint paths `android/feature/<x>/**` and `android/core/fakes/**/fakes/<x>/`), `agent-spec10` (this docs change: SPEC v0.10).
- Worktrees in flight under `.claude/worktrees/`: `agent-m8` (M8 app integration, started 2026-10-18; the phone agent; stops at the pairing HUMAN step), `agent-m7g` (M7g feature cleanups in `android/feature/**`, started 2026-10-18), `agent-spec11` (this docs change: SPEC v0.11).
- The UX sprint (owner's session in `~/orca/workspaces/kvitt/UX-sprint`, branch `UX-sprint`) has landed; nothing is in flight there.
- Importer: the dry run of 2026-10-18 with M12b's mapping reconciled on both real files (Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped). The CSV files stay in `export/` (git-ignored).
- Development stack (M2c-dev): running on the Mac, reachable at `http://192.168.1.210:8080`; its database was migrated to schema 2 on 2026-10-18 after a backup. Firebase project `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm`; `infra/secrets/fcm-service-account.json` and `infra/secrets/google-services.json` in the main checkout (git-ignored).
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug` built from `c28c52b` (older than `main`), as of 2026-10-18.
- Development stack (M2c-dev): running on the Mac, reachable at `http://192.168.1.210:8080`; database at schema 2 since 2026-10-18 (migrated after a backup). Firebase project `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm`; `infra/secrets/fcm-service-account.json` and `infra/secrets/google-services.json` in the main checkout (git-ignored).
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug` built from `c28c52b`, as of the last report; M8 installs its build.
## 2. Open work (one line each, with who owns it: agent or owner)
- Land `agent-spec10` (orchestrator): SPEC v0.10, D-060 to D-068, the "Dining" label and `ledger_settlement_label` in `docs/ux/strings.csv` and `:core:design`, HANDOFF, `prompts/README.md`, `android/README.md`.
- M7a to M7f (`agent-m7a` to `agent-m7f`, in flight, parallel). Decision ranges: M7a D-070–D-074, M7b D-075–D-079, M7c D-080–D-084, M7d D-085–D-089, M7e D-090–D-094, M7f D-095–D-099; spec version v0.11 for all; the second and later to land renumber to be contiguous.
- M8 App integration after M7 (orchestrator brief), carrying: M5's contract requests (§11.3); M6's (`LocalAvatarPictures`/`AvatarPictures` with the authenticated E-11 Coil request from user id and `pictureVersion`, `ProvideHousehold(me, partner)` around screens, `LocalKvittClock`); the `SyncSummary` "last attempt failed" field so `SyncStatusPill` drops its `failed` flag; the D-027 re-stamp (on the first sync attempt after a `clock_skew` rejection the phone re-stamps `updated_at` of each such pending row before pushing; not in `:data` yet); the picture size question (`:data` always fetches E-11 at 512; a 128 variant needs a size parameter on `:core:model` and `:data`); the design-spec §2.7 check on the S26 that `FontFamily.Default` keeps tabular figures and distinct weights 500/600/700 under One UI fonts.
- Land `agent-spec11` (orchestrator): SPEC v0.11, D-069 to D-080, HANDOFF, `prompts/README.md`, `android/README.md`, PLAN-M8 §3 facts.
- M8 App integration (`agent-m8`, in flight, phone agent). Decision range D-085 to D-089 reserved; renumbered to D-081 onwards on landing (after `agent-spec11`). Spec version v0.12. It wires the §13.2 navigation (D-071), deletes `android/app/src/debug/.../DebugFakesModule.kt` (`72f21b2`), keeps the `:app` requests of M6 (`LocalAvatarPictures`, `ProvideHousehold`, `LocalKvittClock`) and the design-spec §2.7 One UI font check on the S26.
- M7g Feature cleanups (`agent-m7g`, in flight): feature modules after M6b; no phone.
- Later contract pass M12e (not started; SPEC §11.3 list): invite kind in the `pair()` result, admin name in the invite link, `SyncSummary` "last attempt failed", actor on `Rejection`, version name on `Device`, `RecurringRepository.create` without a caller-made `TemplateId`, a result for `UpdateRepository.check()`, untouched accounts in `SettleState`, the D-027 re-stamp in `:data`, the 128 px picture size (owner question).
- Image build for the Pi (M2c-pi or M10): since M4 the arm64 server image needs a C toolchain in the build stage (aws-lc-rs) and CA roots in the distroless image (§10.3); confirm both and one FCM send from the shipped image.
- Later cleanup (any Android plan that owns `android/gradle/**`): remove the unused Vico entries from `libs.versions.toml` (D-063).
- Plans (SPEC §11.2 v0.10), in dependency order; prompt `prompts/<id>.md`:
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M9, M12a, M12b, M12c, M12d, U1, U2: landed.
- Now: M7a to M7f in parallel.
- Next: M8 App integration after M7; M2c-pi Infra on the Pi after M8 (owner: answer OQ-3, grant the Pi, Home Assistant Pi, Gitea and Drive allowances at the start, D-023); M10 First release after M2c-pi (owner: keystore per §10.6, D-024); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045).
- Plans (SPEC §11.2 v0.11), in dependency order; prompt `prompts/<id>.md`:
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7f, M9, M12a, M12b, M12c, M12d, U1, U2: landed.
- Now: M8 and M7g in parallel.
- Next: M2c-pi Infra on the Pi after M8 (owner: answer OQ-3, grant the Pi, Home Assistant Pi, Gitea and Drive allowances at the start, D-023); M10 First release after M2c-pi (owner: keystore per §10.6, D-024); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045); M12e contract pass when scheduled.
## 3. HUMAN steps pending (relay in chat)
- Later, at the owner's convenience: look through M6's component screenshots (`android/core/design/src/test/screenshots/`); approved in advance ("go with your recommendations"), so nothing blocks on it.
## 3. HUMAN steps pending (relay in chat; the owner does them tonight, 2026-10-18)
1. M8 pairing and offline test on the phone; relay wording comes from the M8 report.
2. Look through the M6 component screenshots (`android/core/design/src/test/screenshots/`); approved in advance, nothing blocks on it.
3. Answer OQ-3 for M2c-pi (§4).
4. Release keystore per SPEC §10.6 for M10, or allow an agent to generate one.
5. Yes/no: the keyword "boende" maps to travel. Yes/no: is the 128 px picture size needed?
## 4. Owner questions (asked, unanswered)
- OQ-3 LAN addresses and SSH users of the Kvitt Pi and the Home Assistant Pi; SSH port of the HA Pi; certificate directory there; which SSH add-on (SPEC §12). Needed by M2c-pi only. The only open question (OQ-1, OQ-2, OQ-4 answered: D-001, D-003, §10.9).
- OQ-3 LAN addresses and SSH users of the Kvitt Pi and the Home Assistant Pi; SSH port of the HA Pi; certificate directory there; which SSH add-on (SPEC §12). Needed by M2c-pi only.
- "boende" keyword → travel (yes/no); 128 px picture size needed (yes/no) (§3 item 5).
## 5. Paused (waiting on something outside the repo)
- Nothing.
## 6. Numbering
- Decisions: D-001 to D-068 used (D-008 and D-009 unused). Next free: **D-069**; D-070 to D-099 are handed to M7a to M7f (§2).
- Spec version: v0.10 written; next **v0.11** (shared by the M7 landings).
- Server schema version **2** (migration 0002, landed with M12b); next **3**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`, unchanged by M12, M12d and M6); next **2**.
- Decisions: D-001 to D-080 used (D-008 and D-009 unused). D-085 to D-089 reserved by M8 (renumbered to D-081 onwards on landing). Next free: **D-081**.
- Spec version: v0.11 written; next **v0.12**.
- Server schema version **2** (migration 0002, landed with M12b); next **3**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**.
- Sync protocol **1**, push payload **1**.
## 7. How the work is run
+12
View File
@@ -73,3 +73,15 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-066 | 2026-10-18 | Golden layout: one Roborazzi golden per component sheet × light/dark × sv/en (`<sheet>_<lang>_<theme>.png`), plus a 200 % font-scale variant for long states; the content is composed once and language, theme and font scale are switched through a configuration context, so Compose's non-linear font scaling still applies. Feature modules (M7a to M7f) follow the same layout. | M6 report: one layout keeps the goldens reviewable and the feature agents' screenshot tests consistent. | SPEC §6.2 (Screenshot tests) |
| D-067 | 2026-10-18 | The English default name of the category `restaurants` is "Dining" (was "Restaurants"); Swedish "Restaurang & café" stays. Changed in AC-7.2, `docs/ux/strings.csv` and `android/core/design/src/main/res/values/strings.xml` (`category_default_restaurants`) together. | Owner approved: "Restaurants" clips at 11 sp in the six-column `CategoryQuickRow`, and design-spec §3.7 allows no ellipsis there. | SPEC AC-7.2, §13.4; `docs/ux/strings.csv` |
| D-068 | 2026-10-18 | New shared string `ledger_settlement_label` ("Payment" / "Betalning", `module=core:design`) as the type label on the second line of a settlement row (design-spec §3.5), added to `docs/ux/strings.csv` beside the other `ledger_settlement_*` rows and to `:core:design`'s `values/strings.xml` and `values-sv/strings.xml`. Also: `SettlementRow` titles may wrap to 2 lines (3 at 200 % font scale), and a chart whose range has no expenses shows only the 0 baseline with no gridline values (design-spec §3.10). | M6 report: `settle_detail_title` belongs to `:feature:settle` and `:feature:account` cannot use it (D-017); the wrap and the empty-chart rendering were not stated in the design spec. | SPEC §13.3 (S-5, S-11), §13.4; `docs/ux/strings.csv` |
| D-069 | 2026-10-18 | S-11 readings. Total-card caption "<range> · Hushållet" for the household measure and "<range> · N veckor" in week granularity; for my share the label is "Totalt · Min del", so the caption does not repeat it. Month presets show whole-month ranges ("maj–okt 2026"), custom ranges and week granularity show days. Legend and stacking order: categories by `sortOrder`, accounts in `AccountRepository.all()` order; the order of `AnalyticsState.groups` is unspecified and the UI sorts. "Loading" means `SyncSummary.lastSuccessAt == null` and shows skeleton cards; the design spec has no offline or loading frame for S-11/S-12, so offline looks like loaded. | M7e report (`6ac711d`): the design spec left caption wording, order and the loading condition open. | SPEC §13.3 (S-11), §14.2.4 ("Readings fixed in v0.11") |
| D-070 | 2026-10-18 | S-12 readings. A week title follows `docs/ux/strings.csv` ("Vardag, vecka 41"), not "v. 41"; a category or account over the whole range is titled "<name>, <range caption>". No error state on S-11 or S-12 (analytics are local, AC-14.1); S-12 has no loading state. | M7e report (`6ac711d`): design spec and strings disagreed on the week title; the error and loading states of §13.3 do not apply to local analytics. | SPEC §13.3 (S-11, S-12) |
| D-071 | 2026-10-18 | Navigation wiring. The top-level screens S-3, S-4, S-11 and S-13 render their own `KvittNavigationBar` and take `onNavigate: (KvittDestination) -> Unit`; the `:app` scaffold draws no second bar; the `NavHost` switches top-level destinations from those lambdas. Navigation-argument keys are constants in each feature (`ARG_EXPENSE_ID`/`ARG_ACCOUNT_ID`, `SettleArgs.SETTLEMENT_ID`/`ACCOUNT_ID`, `DrillDownArgs.KEYS`, settings `"categoryId"`/`"templateId"`), read through `SavedStateHandle`. Result snackbars of S-7, S-9 and account archiving are shown by the screen returned to (Home through its `snackbarHostState`); the account-archived snackbar has an undo action (`common_undo`). | M7a, M7b, M7e, M7f reports and M8's brief: the features built the bar and the argument keys themselves; M8 needs one rule to wire them. | SPEC §13.2, §11.2 (M8), §11.3 (M8) |
| D-072 | 2026-10-18 | A new recurring template's "Dag i månaden" defaults to today's day of the month; the start month is the current month if that day is today or later, else the next month (so by default the current month). | M7f report (`a54eb7c`): AC-9.8 gave the start-month rule but no default day. | SPEC AC-9.8, §13.3 (S-19) |
| D-073 | 2026-10-18 | "Sök efter uppdatering" (S-22) reports "Du har den senaste versionen" when `UpdateState` is still `None` after `check()`; a network failure cannot be told apart because `check()` returns nothing. Accepted for v1; "give `UpdateRepository.check()` a result" is a contract request for a later contract pass. | M7f report (`a54eb7c`): the UI contract has no result for a manual check. | SPEC AC-16.6 (new), §13.3 (S-22), §14.2.4, §11.3 |
| D-074 | 2026-10-18 | `FieldError.field` values are the model property names `amountOre`, `description`, `notes`, `date`, `split`, `accountId`, `categoryId`, `allocations`, as `:core:fakes` and `:data` emit them. Editing a settlement opens with its saved rows counted as user-edited, so changing the amount keeps them; "Föreslå igen" re-proposes. The S-9 info line on untouched accounts derives them from Home balances and may be slightly off while editing (balances include the settlement being edited). | M7c (`1e42f76`) and M7d (`082b8dc`) reports: field names were not fixed; AC-11.6 did not say whether an edit re-proposes; `SettleState` has no untouched-accounts list. | SPEC §14.2.4, AC-11.6, §13.3 (S-9), §11.3 |
| D-075 | 2026-10-18 | The quick-row fill of D-060 lives in `:core:design`'s `quickRowCategories(categories, frequent, selected)`; editors pass the raw `frequentCategoryIds[accountId]`; an empty or short list is filled from `sort_order`, and the selected category takes fifth place when absent. Replaces `docs/ux/design-spec.md` §3.7's interim "selected first, then sort_order". | M6b (`3a1bd14`): S-7 and S-19 both need the fill; one helper keeps them identical. | SPEC §14.2.4, §13.3 (S-7), §11.2 (M6b) |
| D-076 | 2026-10-18 | Category item labels (quick row and grid) take up to two centred lines; longer custom names end the second line in "…"; single long words still hyphenate ("Entertain-ment"); TalkBack reads the full name. "No ellipsis" (design-spec §3.7) still holds for the default names. | M6b (`3a1bd14`): custom category names (up to 40 code points, §9.6) can be longer than two lines; the default names still fit without an ellipsis. | SPEC §13.3 (S-7); `docs/ux/design-spec.md` §3.7 |
| D-077 | 2026-10-18 | Day ranges in captions and summaries are written out with the year ("24 augusti–17 oktober 2026", "1–30 november 2026", "28 december 2025–3 januari 2026"); the period chip and the S-12 title keep the short form "24 aug.–17 okt.". Extends D-065. | M6b (`3a1bd14`, `DateFormatter.dayRangeLong`): M7e needed a long form for the total caption. | SPEC §9.8, §13.3 (S-11) |
| D-078 | 2026-10-18 | A stored amount opens for editing with both decimals ("1 247,60", "12 400,00"); ratio percent fields keep the trimmed form ("60", "60,5"). Note: `viewerPercentOf` now reads basis points (bug fix, no rule change). | M6b (`3a1bd14`, `AmountInput.of` and `AmountInput.percent`, replacing M7c's own `moneyInput`): money is always shown with two decimals (§9.8), percents are not money. | SPEC §9.6 |
| D-079 | 2026-10-18 | New shared keys `common_ok`, `common_undo`, `editor_split_full_on_you` ("All on you" / "Allt på dig"; `ledger_settlement_label` was D-068). The module keys proposed by M7b to M7f are recorded in `docs/ux/strings.csv` as built: `analytics_granularity`, `analytics_measure`, `analytics_group_by`, `analytics_range_apply`; `settle_row_amount`, `settle_detail_delete`, `settle_detail_sync_now`; `expense_detail_sync_now`; `home_archived_expanded`, `home_archived_collapsed`; `settings_category_archived_snackbar`, `settings_category_unarchived_snackbar`, `settings_recurring_summary_none`, `settings_expanded`, `settings_collapsed`, `settings_partner_missing`. `strings.csv` has 578 rows, header included. | M7b to M7f reports and M6b (`3a1bd14`): §13.4 requires a decisions entry for strings not in the file. | SPEC §13.4; `docs/ux/strings.csv` |
| D-080 | 2026-10-18 | "Synka nu" (the missing states of S-8 and S-10, and S-16) is an online-only action, disabled with "Kräver anslutning" offline. Discard dialogs (S-6, S-7, S-9, S-19) have a title and no body text. | M7c report (`1e42f76`): §9.9 did not list "Sync now", which needs the server; the discard dialogs' body text was not fixed. | SPEC §9.9, §13.3 (S-6, S-7, S-8, S-9, S-10, S-16, S-19) |
+14 -11
View File
@@ -2,7 +2,7 @@
One ready-to-paste start prompt per plan (`PLAN-<id>.md` at the repository root), plus `ORCHESTRATOR-prompt.md`. Before pasting, the orchestrator fills `{{NEXT_DECISION}}`, `{{SPEC_VERSION}}`, `{{SCHEMA_VERSION}}` (disjoint ranges for agents that run in parallel, CLAUDE.md › Numbering) and `{{ORCHESTRATOR_NOTES}}` (answers to the plan's open points, phone assignment, overrides). `U2.md` also takes `{{OWNER_PICK_AND_COMMENTS}}`. U1 and U2 run in the owner's own Claude session in `~/orca/workspaces/kvitt/UX-sprint` on branch `UX-sprint`. The numbers are for proposed entries in agents' reports: implementation agents never edit `SPEC.md` or `docs/decisions.md`; a docs agent records what the orchestrator accepts (CLAUDE.md › Orchestration).
Dependencies follow SPEC §11.1 and §11.2 (v0.10). Status as of 2026-10-18: M0 done, M1 landed (`c6df2ab`), M2c-dev landed (`c885aba`), M2a landed (`fdf2093`), M2b landed (`fad6fc9`), M3 landed (`503cee5`), M5 landed (`c28c52b`), M4 landed (`bd6f10a`), M9 landed (`ab298ab`); SPEC v0.7 landed (`e8ee518`); M12a landed (`6b60ef7`); SPEC v0.8 landed (`7fb9f6f`); M12c landed (`e499107`); M12b landed (`2e2ebe7`); U1 and U2 landed (`16c4d01`, owner approved); SPEC v0.9 landed (`45eecd1`); M12d landed (`bdba9d3`); M6 landed (`3080804`); M7a to M7f started 2026-10-18 in parallel; SPEC v0.10 records the M12d and M6 outcomes (D-060 to D-066) and two string decisions (D-067, D-068). The plans' open points are resolved in SPEC v0.3 and recorded in each plan's §4; v0.4 records the M2a and M2b decisions (D-005 to D-014); v0.5 the M3 and M5 decisions (D-026 to D-034, D-040 to D-043); v0.6 the M4 decisions (D-035 to D-039); v0.7 the M9 decisions (D-044 to D-046); v0.8 the owner's UX-session decisions (D-047 to D-054) and the milestone group M12; v0.9 D-055 to D-059 and M12d; v0.10 D-060 to D-068.
Dependencies follow SPEC §11.1 and §11.2 (v0.11). Status as of 2026-10-18: M0 done, M1 landed (`c6df2ab`), M2c-dev landed (`c885aba`), M2a landed (`fdf2093`), M2b landed (`fad6fc9`), M3 landed (`503cee5`), M5 landed (`c28c52b`), M4 landed (`bd6f10a`), M9 landed (`ab298ab`); SPEC v0.7 landed (`e8ee518`); M12a landed (`6b60ef7`); SPEC v0.8 landed (`7fb9f6f`); M12c landed (`e499107`); M12b landed (`2e2ebe7`); U1 and U2 landed (`16c4d01`, owner approved); SPEC v0.9 landed (`45eecd1`); M12d landed (`bdba9d3`); M6 landed (`3080804`); SPEC v0.10 landed (`7bd0aa1`); M7a landed (`b9b061a`), M7e (`6ac711d`), M7d (`082b8dc`), M7b (`31760f9`), M7c (`1e42f76`), M7f (`a54eb7c`); M6b `:core:design` fixes landed (`3a1bd14`); M8 and M7g (feature cleanups, no prompt file; brief by the orchestrator) started 2026-10-18 in parallel; SPEC v0.11 records the M7 and M6b outcomes (D-069 to D-080). M8 holds D-085 to D-089, renumbered to D-081 onwards on landing. The plans' open points are resolved in SPEC v0.3 and recorded in each plan's §4; v0.4 records the M2a and M2b decisions (D-005 to D-014); v0.5 the M3 and M5 decisions (D-026 to D-034, D-040 to D-043); v0.6 the M4 decisions (D-035 to D-039); v0.7 the M9 decisions (D-044 to D-046); v0.8 the owner's UX-session decisions (D-047 to D-054) and the milestone group M12; v0.9 D-055 to D-059 and M12d; v0.10 D-060 to D-068; v0.11 D-069 to D-080.
## Dependency order and waves
@@ -24,14 +24,16 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7
| 2 | M12c Category contract v2: Android | `M12c.md` | M12a (M2b, M5 landed) | done (`e499107`) |
| 2 | M12d UI contract additions (UX-9) | `M12d.md` | M12c, SPEC v0.9 | done (`bdba9d3`) |
| 2 | M6 Design system | `M6.md` | U2 approved, M2b, M12c, the §9.7 colour spec change (v0.9) | done (`3080804`) |
| 3 | M7a Feature onboarding | `M7a.md` | M6, M12d | started 2026-10-18; M7b to M7f, M9 |
| 3 | M7b Feature home and account | `M7b.md` | M6, M12d | started 2026-10-18; M7a, M7c to M7f, M9 |
| 3 | M7c Feature expense | `M7c.md` | M6, M12c, M12d | started 2026-10-18; M7a, M7b, M7d to M7f, M9 |
| 3 | M7d Feature settle | `M7d.md` | M6, M12d | started 2026-10-18; M7a to M7c, M7e, M7f, M9 |
| 3 | M7e Feature analytics | `M7e.md` | M6, M12d | started 2026-10-18; M7a to M7d, M7f, M9 |
| 3 | M7f Feature settings | `M7f.md` | M6, M12d | started 2026-10-18; M7a to M7e, M9 |
| 3 | M9 Splitwise importer | `M9.md` | M2a, M4 | M5, M6, M7a to M7f |
| 4 | M8 App integration | `M8.md` | M3, M4, M5, M7a to M7f, M2c-dev | M9 if still running |
| 3 | M7a Feature onboarding | `M7a.md` | M6, M12d | done (`b9b061a`) |
| 3 | M7b Feature home and account | `M7b.md` | M6, M12d | done (`31760f9`) |
| 3 | M7c Feature expense | `M7c.md` | M6, M12c, M12d | done (`1e42f76`) |
| 3 | M7d Feature settle | `M7d.md` | M6, M12d | done (`082b8dc`) |
| 3 | M7e Feature analytics | `M7e.md` | M6, M12d | done (`6ac711d`) |
| 3 | M7f Feature settings | `M7f.md` | M6, M12d | done (`a54eb7c`) |
| 3 | M9 Splitwise importer | `M9.md` | M2a, M4 | done (`ab298ab`) |
| 3 | M6b Design-system fixes after M7 | none (orchestrator brief) | M7a to M7f | done (`3a1bd14`) |
| 4 | M7g Feature cleanups | none (orchestrator brief) | M7a to M7f, M6b | started 2026-10-18; M8 |
| 4 | M8 App integration | `M8.md` | M3, M4, M5, M7a to M7f, M2c-dev | started 2026-10-18 (phone agent); M7g |
| 5 | M2c-pi Infra on the Pi | `M2c-pi.md` | M2c-dev, M8; OQ-3 answered | none needed |
| 6 | M10 First release | `M10.md` | M2c-pi, M3, M4, M8 | none |
| 7 | M11 Import history | `M11.md` | M9, M10 | none |
@@ -40,8 +42,9 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7
- **M3 and M4** (D-015): M2a provides `next_occurrence_date`, the `AuditWriter`/`PushQueue` traits with no-op implementations and `tasks::start`; M3 writes the recurring generation function and calls the traits; M4 replaces the implementations, its scheduler calls M3's function, and M4 lands after M3.
- **M2a and M2b** create shell files and modules in later milestones' paths (D-004); ownership passes on when they land.
- **M7a to M7f** follow the feature convention of SPEC §6.2 (D-017) and take the ledger rows and expense editor fields from M6's `:core:design` (D-019).
- **M7a to M7f** (landed) follow the feature convention of SPEC §6.2 (D-017) and take the ledger rows and expense editor fields from M6's `:core:design` (D-019); their readings are D-069 to D-074 and D-080, the shared fixes went into M6b (`3a1bd14`, D-075 to D-079). Their contract requests are on the M12e list in SPEC §11.3.
- **M8** wires the navigation of SPEC §13.2 (D-071: top-level screens draw their own bar) and deletes the debug fakes module (`72f21b2`); release has bound `:data` since `e6ad858`.
- **M6** landed `3080804` (D-063 to D-066): charts on `Canvas`, Expressive fallbacks against material3 1.4.0, formatting tables in code, one golden per sheet × theme × language (+200 %), which M7a to M7f follow. Its M8 contract requests are in SPEC §11.3.
- **M12** (D-054): done; the importer test `category_table_matches_the_spec` is green again since M12b (`2e2ebe7`). M12d (D-059) landed `bdba9d3` (D-060 to D-062); M7a to M7f run on top of both.
- **Phone:** M2b (one `installDebug`), M5 (only if instrumented tests), M8 and M10 use the phone; only one at a time, named in the notes.
- **Phone:** M2b (one `installDebug`), M5 (only if instrumented tests), M8 and M10 use the phone; only one at a time, named in the notes. M8 is the phone agent now; M7g does not use the phone.
- **Outside the repository:** M2c-dev (D-022, done), M2c-pi (the Kvitt Pi, the Home Assistant Pi, Gitea, Drive: allowances requested at the start of that sprint, D-023), M8 (dev server on the Mac), M10 and M11 (production) need the owner's allowance in the notes.