Version **0.9** (2026-10-18). Schema version **2**. Sync protocol version **1**. Push payload version **1**.
Version **0.10** (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.
@@ -168,7 +168,7 @@ The server starts with an empty database. The household is created from the comm
| `default_key` | English name | Swedish name | `icon` | `color` | `sort_order` |
| UI | Jetpack Compose, Material 3; Material 3 Expressive components where they are stable. The Material 3 artifact is pinned explicitly in the catalog (1.4.0 at M2b) alongside the Compose BOM | Native, modern, screenshot-testable |
| UI | Jetpack Compose, Material 3; Material 3 Expressive components where they are stable. The Material 3 artifact is pinned explicitly in the catalog (1.4.0 at M2b) alongside the Compose BOM. Against material3 1.4.0 only `ShortNavigationBar` of the Expressive set is public and used; every other Expressive API takes the fallback listed in D-064 (M6) | Native, modern, screenshot-testable |
| Min / target SDK | `minSdk` 36 (Android 16); `compileSdk` and `targetSdk` 37 (the latest stable API when M2b pinned them, in `libs.versions.toml`) | Both phones run Android 16; no legacy paths |
| Local DB | Room 3 (`androidx.room3`, 3.0.3 at M2b), KSP only, an `SQLiteDriver` with `androidx.sqlite:sqlite-bundled`, exported schema (D-012) | Full offline copy; migrations with tests |
| DI | Hilt. `:data`'s `DataModule` provides implementations only; the bindings of the `:core:model` interfaces to them live in `DataBindingsModule` (`@DisableInstallInCheck`), which `:app` includes in the release graph, so the debug fakes do not duplicate bindings (D-041) | Standard, works with WorkManager and ViewModels |
| Background | WorkManager (periodic 15 min sync, expedited sync) | Survives process death, respects Doze |
| Dates | kotlinx-datetime | Allowed in pure modules (no `java.time`) |
| Images | Coil (Compose) with an auth header interceptor | Disk cache for offline avatars |
| Charts | Vico (Compose). If Vico's current release cannot draw a stacked bar chart with Material 3 theming, hand-roll the three charts with Compose `Canvas` in `:core:design` | Mature Compose chart library; fallback is small |
| Charts | Compose`Canvas` in `:core:design`, no chart library (D-063). Vico 3.3.1 cannot give the per-bar TalkBack nodes and tap targets, 2 dp segment gaps, 10/4 corner radii on the whole bar and an axis gutter growing at 200 % that `docs/ux/design-spec.md` §3.10 requires; Vico stays in the version catalog unused, removable in a later cleanup | The three charts are small; the design spec's accessibility rules need per-bar control |
| Push | Firebase Cloud Messaging, only inside `:adapter:fcm` | Free tier; vendor isolated per `CLAUDE.md` |
| Secrets | Android Keystore: an AES-256-GCM key (non-exportable) encrypts the device token; ciphertext in a private DataStore file | `EncryptedSharedPreferences` is deprecated |
| Build | Gradle with version catalog `android/gradle/libs.versions.toml`, Gradle daemon on JDK 25 (pinned), bytecode target Java 17. With AGP 9, `check` runs only the debug unit-test variant. `versionCode` and `versionName` come from the Gradle properties `kvitt.versionCode` and `kvitt.versionName`, defaulting to `1` and `dev` (§10.6) | Existing pin |
| `:core:design` | Android library | Theme, tokens, typography, shared components (money text, avatar, category chip, balance header, charts; the ledger rows for expenses and settlements used by S-5, S-12 and S-16; the expense editor fields used by S-7 and S-19, D-019), `MoneyFormatter`, `DateFormatter`, shared strings | `:core:model` |
| `:core:design` | Android library | Built by M6 (`3080804`) from `docs/ux/design-spec.md` §2 and §3: `KvittTheme`, tokens (`KvittColors`, `KvittShapes`, `KvittMotion`), typography, shared components (money text, avatar, category tile and pickers, balance header, sync pill, banners, charts drawn with `Canvas`, D-063; the ledger rows for expenses and settlements used by S-5, S-12 and S-16; the expense editor fields used by S-7 and S-19, D-019), `MoneyFormatter`, `DateFormatter`, shared strings | `:core:model` |
| `:core:fakes` | Android library, used only by tests, screenshot tests and debug previews | Scenario-driven fakes of every repository interface (they call `:domain` for computed values so fakes never disagree with the real rules) | `:core:model`, `:domain` |
| `: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.
**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.
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`.
@@ -777,6 +777,9 @@ The server validates icon and colour against these lists (`validation_failed`).
* Money is never shown with a minus sign; direction is in words (§9.2, §9.3).
* 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).
* 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`.
M12d UI contract additions (D-059: frequentCategoryIds, deletable) ─► M7a..M7f (runs in parallel with M6)
M12d UI contract additions (D-059: frequentCategoryIds, deletable; landed bdba9d3) ─► M7a..M7f (ran in parallel with M6, landed 3080804)
```
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) runs in parallel with M6; M7a to M7f wait for both.
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.
### 11.2 Milestone table
@@ -1151,14 +1154,14 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
| M12a | Category contract v2: contracts | Landed `6b60ef7`. `contracts/category-keywords.csv` (148 entries), `contracts/schema/migrations/0002_categories_v2.sql` (§14.1.1), `contracts/test-vectors/suggest_category.json` (65 cases), E-1 example `schema_version` 2, `tools/contracts-check.py` (keyword-table rules, 0002 loaded on a fresh and an in-use v1 database, Python reference of §9.11, new icon and colour tokens), and `:domain`'s `suggestCategory` with `UnicodeText` passing the vectors (§14.5.6). The OpenAPI `DefaultCategoryKey` extension and the new category examples were held back for M12b | `python3 tools/contracts-check.py` passes | none | M1, SPEC v0.8 |
| M12b | Category contract v2: server | Landed `2e2ebe7` (D-057, D-058). Migration `server/crates/kvitt-server/migrations/0002_categories_v2.sql` byte-identical to the contract file with its identity test; `SCHEMA_VERSION` 2; seed test of the fourteen defaults (ids, keys, icons, colours, sort orders, `server_version` from the sequence) on a fresh database and on a v1 database holding expenses (rows and references kept); icon and colour lists of §9.7 in `validate.rs`; `DefaultCategoryKey` with the fourteen keys in `contracts/openapi.yaml` (held back by M12a) and in the utoipa wire types, so the OpenAPI match test passes; the category examples M12a held back; importer table `splitwise_categories.csv` per §10.8 with its spec-parity test; importer schema check 2 (D-054); E-31 and other examples only where they enumerate the defaults | Server gate; `python3 tools/contracts-check.py` passes | none | M12a, M3, M4, M9 |
| 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) | `: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 | Started 2026-10-18 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, 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 | `: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 | `:feature:home`, `:feature:account` screens S-4, S-5, S-6, S-16, S-17 | as M7a | none | M6, M12d |
| M7c | Feature: expense | `: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 |
| 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 |
| 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) |
| 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 |
@@ -1173,12 +1176,13 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
* **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).
* **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. 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. 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).
* **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`.
* **M12a, M12b, M12c** (D-049 to D-054) carry the owner's UX-session decisions into the contracts and the landed code: M3, M5 and M9 were built with ten defaults, so this is a contract change against `main`, not only the spec. M12a (landed `6b60ef7`) wrote `contracts/`, the checker and `:domain`'s `suggestCategory`; M12b (landed `2e2ebe7`) and M12c (landed `e499107`) ran in parallel (disjoint paths, §14.7); their outcomes are D-055 to D-058. The Room schema stays at version 1: category rows reach the phone through sync. The colour hex values of `slate` and `amber` are not needed by M12 (`:core:model` holds token names only); they arrive with U2 and M6.
* **M12d** (D-059) adds the two UI-contract members UX-9 found (`ExpenseEditorOptions.frequentCategoryIds`, `CategoryRepository.deletable`) to `:core:model`, `:data` and `:core:fakes`, in parallel with M6 (disjoint paths). M7c and M7f consume them; until M12d lands, the design spec's fallbacks apply (`docs/ux/design-spec.md` §3.7 and §6).
* **M12d** (D-059, landed `bdba9d3`) added the two UI-contract members UX-9 found (`ExpenseEditorOptions.frequentCategoryIds`, `CategoryRepository.deletable`) to `:core:model`, `:data` and `:core:fakes`, in parallel with M6 (disjoint paths); its readings are D-060 to D-062 (§14.2.4 "Readings fixed in v0.10"). M7c and M7f consume them; the design spec's fallbacks (`docs/ux/design-spec.md` §3.7 and §6) are no longer needed.
* **M6** (landed `3080804`) built `:core:design` with charts on Compose `Canvas` (D-063), the Material 3 Expressive fallbacks of D-064, the formatting tables of D-065 and the golden layout of D-066. Its contract requests for **M8** (no change now): `:app` provides `LocalAvatarPictures` with an `AvatarPictures` that builds the authenticated E-11 Coil request from the user id and `pictureVersion` (§14.3.4); `:app` wraps every screen in `ProvideHousehold(me, partner)`; `:app` provides `LocalKvittClock` (tests and previews pass a fixed clock); `SyncSummary` gains a "last attempt failed" field so `SyncStatusPill` can drop its `failed` parameter (§13.3 S-4). The design-spec §2.7 open point (that `FontFamily.Default` keeps tabular figures and distinct weights 500/600/700 under every One UI font style on the S26) is checked at the M8 install on the owner's phone.
* **M11** runs the import per §10.8 "Production run" (D-025). The rehearsal on 2026-10-18 ran both real files against a throwaway database and reconciled on both (Vardag 1746 rows, Hemma 37 rows); 21 rows were unmapped before D-044. The dry run of 2026-10-18 with M12b's mapping confirmed it: Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled.
---
@@ -1228,14 +1232,14 @@ Each screen must be designed in these states where they apply: **loaded**, **emp
| 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; 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; 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-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-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") | Change filters; tap a bar or a row to open S-12 |
| 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-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 |
@@ -1290,7 +1294,7 @@ 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, header included; 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; 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.
@@ -1893,6 +1897,12 @@ Changes in v0.9 (D-055 to D-057, D-059):
* **`ExpenseEditorOptions.frequentCategoryIds` (D-059).** Per active account: the active (non-archived, non-deleted) categories ordered by the number of non-deleted expenses in that account that use them, descending; ties by `sort_order` ascending; at most 5 entries; categories with no use in the account are left out (the list may be shorter or empty). Counted over every expense in the phone's Room copy (both users, imported and recurring included), so both phones show the same order after sync; it is not a per-phone history like AC-8.6's "last used on this phone". The flow re-emits when expenses or categories change; the editor reads it once when it opens and on an account change, so tiles do not move while the user looks at them. UI rule (design, not contract): the row shows the account's list, filled up to five from the remaining active categories in `sort_order`; the selected category takes the fifth place when it is not among them. Fallback until M12d lands: the selected category first, then the first active categories in `sort_order` up to five.
* **`CategoryRepository.deletable(id)` (D-059).** Computed from Room: false for a default category (`defaultKey != null`) or one used by a non-deleted expense or non-deleted recurring template (AC-7.6). The server stays the authority: `delete` can still return `Failed("category_is_default")` or `Failed("category_in_use")` (for example when the partner's new expense has not synced yet), and the UI shows the same reason inline. The editor picks the text from `deletable` and `defaultKey`: default → `settings_category_delete_blocked_default_short`; in use and archived → `settings_category_delete_blocked_in_use_archived`; in use and active → `settings_category_delete_blocked_in_use`. Fallback until M12d lands: for a custom category "Ta bort kategori" is enabled and `Failed("category_in_use")` shows the in-use reason inline after the tap. Both members are implemented in `:data` and `:core:fakes` (every fake scenario computes them from its own data) by M12d.
Readings fixed in v0.10 (D-060 to D-062, as landed by M12d `bdba9d3`; no type changes):
* **`frequentCategoryIds` holds only used categories (D-060).** Each list holds only categories actually used in that account, so it may have fewer than five entries or be empty. Filling up to five from `sort_order` and placing the selected category fifth is the editor's job (UX-9, `docs/ux/design-spec.md` §3.7), not the repository's.
* **`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.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).
@@ -2597,7 +2607,7 @@ Ownership (an agent edits only paths its milestone owns; anything else needs the
| M12 (D-054): `contracts/category-keywords.csv`, `contracts/schema/migrations/**`, `contracts/test-vectors/suggest_category.json` and the other `contracts/**` edits of §11.2 row M12a, `tools/contracts-check.py`, `android/domain/**` (`suggestCategory`, `UnicodeText`) | M12a (contract-change plan, landed `6b60ef7`) | contract-change plans only |
| M12 (D-054): `contracts/openapi.yaml` (`DefaultCategoryKey`) and the category examples M12a held back; `kvitt-server``migrations/0002_categories_v2.sql`, `src/db/mod.rs` (`SCHEMA_VERSION`), `src/api/validate.rs` (token lists), `src/wire/category.rs` and `src/wire/mod.rs` (key enum), their tests; `kvitt-import``data/splitwise_categories.csv`, `src/categories.rs` and its tests | M12b (contract-change plan, landed `2e2ebe7`) | the usual owners |
| M12 (D-054): `android/core/model/**` (enums, icons, `ExpenseRepository`), `android/data/**` (`UnicodeText` binding, `suggestCategory`, the build-time copy of the keyword table, D-055), `android/core/fakes/**` (defaults, fake `suggestCategory`) | M12c (contract-change plan, landed `e499107`) | the usual owners |
| D-059: `android/core/model/**` (`ExpenseEditorOptions`, `CategoryRepository`), `android/data/**` (`frequentCategoryIds`, `deletable`), `android/core/fakes/**` (both, outside the `fakes/<feature>/` folders) | M12d (contract-change plan) | the usual owners; M7x keep their `fakes/<feature>/` folders |
| D-059: `android/core/model/**` (`ExpenseEditorOptions`, `CategoryRepository`), `android/data/**` (`frequentCategoryIds`, `deletable`), `android/core/fakes/**` (both, outside the `fakes/<feature>/` folders) | M12d (contract-change plan, landed `bdba9d3`) | the usual owners; M7x keep their `fakes/<feature>/` folders |
| `docs/validation/<file>` | the milestone that names the file | |
Module shells (D-004): a shell file or module created by M2a or M2b in a path owned by a later milestone holds only what makes the build and the 501 stubs work; the later milestone owns it from the moment the shell lands and writes the body.
@@ -2642,3 +2652,4 @@ Rule: a change to any other contract file, or a semantic change to `contracts/op
| 0.7 | 2026-10-18 | Decisions from the M9 (`ab298ab`) implementation report and the importer rehearsal on the real exports. D-044: nine Swedish Splitwise category names added to the §10.8 table and to `splitwise_categories.csv` with the key of their English equivalent (Städning and Verktyg - Övrigt `utilities`, Tjänster `home`, Cykel `transport`, Flyg `travel`, Spel `entertainment`, Kläder `shopping`, Livet - Övrigt and Utbildning `other`). D-045: reconciliation: the run commits only if each user's balance of the whole account equals the sum of the file's totals rows (one per currency, converted with `--fx`); a mismatch or a missing totals row rolls back and exits 4, a dry run reports the same, an account already holding other rows rolls back, a mismatch after any `--fx` conversion is reported but accepted (§10.8, AC-17.5). D-046: CLI semantics: `--user` takes a user id or an exact unique display name; no database is created or migrated, `meta.schema_version` must be 1; a dry run runs the full transaction and rolls back; exit codes 0 ok, 1 database or file error, 2 usage, 3 cannot import, 4 reconciliation mismatch; the import key uses an existing account's stored name; an empty description or one over 1000 code points makes the row invalid; descriptions over 100 code points are cut with the full text in `notes`; settlement notes cut to 200; both nets zero is mode `full` on the owner; idempotency through the existing `import_key` columns (§10.8, AC-17.5). M9 marked landed, M11 rehearsal recorded (§11.2, §11.3). |
| 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. |
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
-`main` at `16c4d01` (UX sprint U2, on top of `2e2ebe7` M12b) plus the SPEC v0.9 docs commit from `agent-spec9` 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, `16c4d01` UX sprint U2.
- 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 category contract v2 contracts (`6b60ef7`), SPEC v0.8 (`7fb9f6f`), M12c category contract v2 Android (`e499107`), M12b category contract v2 server (`2e2ebe7`; the importer test `category_table_matches_the_spec` is green again), U1 and U2 (`16c4d01`, `docs/ux/**` only; the owner approved U2).
- With `agent-spec9`: SPEC v0.9. D-055 build-time copy of the keyword table (`:data:copyCategoryKeywords`, `:core:fakes:copyCategoryKeywords`), D-056 `parseCategoryKeywords` in `:domain`, D-057 enum declaration orders, D-058 E-7 example shows `server_version` 15 and 16 (example and `tests/core_api/me.rs` changed in that commit), D-059 UX-9 accepted (`ExpenseEditorOptions.frequentCategoryIds`,`CategoryRepository.deletable`) with the new milestone M12d (PLAN-M12d, `prompts/M12d.md`); D-051 fulfilled: §9.7 hex values from `docs/ux/design-spec.md` §2.3; `design-spec.md` binding for `:core:design` and the screens, `strings.csv` (558 rows) the source of string resources (§13.4); the `suggestCategory` readings (null when `other` is archived and no active last-used category, AC-8.6); PLAN-M6, PLAN-M7c, PLAN-M7f carry design-spec §6 notes.
-`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).
- 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-spec9` (this docs change: SPEC v0.9),`agent-m6` (M6 design system, started 2026-10-18 from design-spec §2 and §3 and `strings.csv`), `agent-m12d` (M12d UI contract additions).
- 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).
- 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.
## 2. Open work (one line each, with who owns it: agent or owner)
- Land `agent-spec9` (orchestrator): SPEC v0.9, D-055 to D-059, PLAN-M12d and its prompt, plan touch-ups (M6, M7c, M7f, M12b, M12c), the E-7 example and its test.
- M6 Design system (`agent-m6`, in flight) and M12d UI contract additions (`agent-m12d`, in flight), in parallel; disjoint paths.
- 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.
- 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.
-D-027 re-stamp (orchestrator assigns: M8 as a contract request on `:data`, or a small `:data` agent): on the first sync attempt after a `clock_skew` rejection the phone re-stamps `updated_at` of each such pending row to its current clock before pushing. Not in `:data` yet.
- Picture size (M6, if needed): `:data` always fetches E-11 at 512; a size parameter for the 128 variant is a contract request on `:core:model` and `:data`.
- Plans (SPEC §11.2 v0.9), in dependency order; prompt `prompts/<id>.md`:
- Next waves: M7a to M7f in parallel after M6 and M12d (M7c and M7f consume M12d); 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).
-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`:
- 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).
## 3. HUMAN steps pending (relay in chat)
-When M6 reports: look at M6's component screenshots and answer yes/no (SPEC §11.2 row M6).
-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.
## 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).
@@ -34,9 +34,9 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- Nothing.
## 6. Numbering
- Decisions: D-001 to D-059 used (D-008 and D-009 unused). Next free: **D-060**.
- Spec version: v0.9 written; next **v0.10**.
- 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 and M12d); next **2**.
- 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**.
@@ -64,3 +64,12 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-057 | 2026-10-18 | `DefaultCategoryKey` in `:core:model` is declared in AC-7.2 sort order. The Rust wire enum, the OpenAPI `DefaultCategoryKey` enum and the 0002 `CHECK` list the ten v1 keys, then `housing`, `insurance`, `children`, `fees`. Wire values are unaffected. | M12b (`2e2ebe7`) and M12c (`e499107`) reports: the declaration orders differ by side; enums compare as sets on the wire. | SPEC §14.2.4 |
| D-058 | 2026-10-18 | The server's test harnesses seed the two users after the fourteen category seeds, so they have `server_version` 15 and 16. `contracts/examples/E-7-me/response.json` shows 15 and 16 (was 11 and 12, from schema 1's ten seeds), and the E-7 test compares `server_version` with the example again instead of treating it as volatile. | M12b report (`2e2ebe7`): migration 0002 moved the users' versions; the example should match what the server produces. | SPEC §14.3.5; `contracts/examples/E-7-me/response.json`, `server/crates/kvitt-server/tests/core_api/me.rs` |
| D-059 | 2026-10-18 | UX-9 accepted as a contract change: `ExpenseEditorOptions.frequentCategoryIds: Map<AccountId, List<CategoryId>>` (per active account up to 5 active categories by use count in that account over the whole Room copy, ties by `sort_order`, unused ones left out; read once on open and on an account change) and `CategoryRepository.deletable(id): Flow<Boolean>` (false for a default or a category used by a non-deleted expense or template, AC-7.6; the server stays the authority), with the readings, UI text choice and fallbacks of §14.2.4 "Changes in v0.9". Implemented in `:core:model`, `:data` and `:core:fakes` by the new milestone M12d, run in parallel with M6; M7c and M7f consume it, and M7a to M7f depend on M6 and M12d. | U2 (`docs/ux/proposed-decisions.md` UX-9): S-7's `CategoryQuickRow` and S-15's category editor have binding states the UI contract did not carry. | SPEC §11.1, §11.2, §11.3, §14.2.4, §14.7; PLAN-M12d, PLAN-M7c, PLAN-M7f |
| D-060 | 2026-10-18 | `ExpenseEditorOptions.frequentCategoryIds` holds only categories actually used in each active account, so a list may have fewer than five entries or be empty. Filling up to five from `sort_order` and placing the selected category fifth stays in the editor (UX-9, `docs/ux/design-spec.md` §3.7). Not a contract change. As landed by M12d (`bdba9d3`). | M12d report: D-059's "at most 5, unused ones left out" left open whether the repository or the editor fills the row; keeping the fill in the editor keeps the repository a plain usage count. | SPEC §14.2.4 ("Readings fixed in v0.10"), §11.2, §11.3 |
| D-061 | 2026-10-18 | `frequentCategoryIds` has a key for every active account (an empty list when nothing was used there) and no key for an archived account. After use count (descending) and `sort_order` (ascending), ties fall back to the category id (ascending), so the order is identical in Room and in the fakes. | M12d report: without a last tie-break two categories with equal count and `sort_order` (custom categories can share one) could order differently in `:data` and `:core:fakes`. | SPEC §14.2.4 |
| D-062 | 2026-10-18 | `CategoryRepository.deletable(id)` returns 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. | M12d report: D-059 did not cover unknown, deleted or archived ids; one rule function keeps the fake from offering a delete it then refuses. | SPEC §14.2.4, AC-7.6 |
| D-063 | 2026-10-18 | Charts (S-11) are drawn with Compose `Canvas` in `:core:design`, not Vico. No chart dependency is added; Vico stays in `android/gradle/libs.versions.toml` unused and can be removed in a later cleanup. Takes the fallback §6.2 already allowed. As landed by M6 (`3080804`). | M6 report: Vico 3.3.1 cannot give design-spec §3.10's one focusable TalkBack node per bar with its own sentence and tap target, 2 dp gaps between segments, the 10/4 corner radii on the whole bar (not per segment) and a y-axis gutter that grows at 200 % font scale. | SPEC §6.2 (Charts, module table), §11.2, §11.3 |
| D-064 | 2026-10-18 | Material 3 Expressive against material3 1.4.0: `ShortNavigationBar` is public and used. Not usable, with the fallback taken: `MaterialShapes` (Cookie4, Clover4Leaf, Sunny, Cookie9) → rounded 30 %, rounded 16 dp, circle, circle; `ButtonGroup` → `SingleChoiceSegmentedButtonRow` styled per the design spec; `LoadingIndicator` → `CircularProgressIndicator`; `LinearWavyProgressIndicator` → `LinearProgressIndicator`; medium extended FAB → `ExtendedFloatingActionButton` 56 dp high, radius 20; Expressive button sizes → `Button` with `heightIn`; segmented list items → Kvitt's own `SegmentedGroup`; Expressive slider → Kvitt's own split bar with a knob; `MotionScheme.expressive()` and the `MaterialTheme(motionScheme)` overload are internal → Material components keep the default motion and Kvitt components use the design-spec §2.11 springs in `KvittMotion`; `Shapes.largeIncreased` and the other increased shapes are internal → held in `KvittShapes`. Fulfils design-spec §6 open point 5. | M6 report: the design spec names Expressive APIs that material3 1.4.0 (pinned since M2b) does not expose publicly; the spec's rule is a fallback without a design change. | SPEC §6.2 (UI); `docs/ux/design-spec.md` §3.1, §6 |
| D-065 | 2026-10-18 | Formatting tables are code: Swedish and English month and weekday names are tables inside `DateFormatter`, not platform locale data, so JVM tests and phone output are identical. Chart axis labels have no dots ("maj, jun, … okt"); list dates keep the Swedish dot ("3 okt.", "25 sep."). File sizes are SI megabytes (10^6 bytes) with one decimal, rounded half up, U+00A0 before "MB" ("8,4 MB", "8.4 MB"). | M6 report: platform locale data differs between the JVM used by tests and Android, and §9.8 gave no file-size format for the update banner. | SPEC §9.8 |
| 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` |
@@ -1205,3 +1205,5 @@ Same editor with "Ändra utgift", fields loaded from `load(id)`, nothing focused
5. **Expressive API status.** Every Expressive API of §3.1 is checked against the current Material 3 release when M6 pins it; any that is not usable takes its listed fallback without a design change.
Resolved in the U2 alignment pass: the catalogue frames follow this file (payer chip `swap_horiz`, ratio presets 40/60 · 50/50 · 60/40, allocation remove buttons, S-13 "14 aktiva" without an "Enheter" subtitle and without a device name, S-16 rows "Väntar på synk", S-12 count caption), and every label quoted here has a `strings.csv` key (including the split-section collapse label, the keypad's "Rensa" action, the S-10 missing state and the category delete confirmation).
Recorded after U2 in SPEC v0.10: open points 1 and 2 are resolved by M12d (D-059 to D-062) and open point 5 by M6 (D-064); English "Dining" for `restaurants` (D-067, so §2.13 reads "Dining") and `ledger_settlement_label`, the settlement-title wrap and the empty-chart baseline (see D-068) live in SPEC.md and `strings.csv`, which win where this file differs.
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.9). 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); M6 and M12d started 2026-10-18; SPEC v0.9 records the M12b and M12c outcomes (D-055 to D-058), the §9.7 values and UX-9 (D-059). 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.
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.
## Dependency order and waves
@@ -22,14 +22,14 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7
| 4 | M8 App integration | `M8.md` | M3, M4, M5, M7a to M7f, M2c-dev | M9 if still running |
| 5 | M2c-pi Infra on the Pi | `M2c-pi.md` | M2c-dev, M8; OQ-3 answered | none needed |
@@ -41,7 +41,7 @@ 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).
- **M6**started 2026-10-18 after the §9.7 values landed (SPEC v0.9, D-051) and M12c landed; it builds from `docs/ux/design-spec.md` §2 and §3 and `strings.csv`.
- **M12** (D-054): done; the importer test `category_table_matches_the_spec` is green again since M12b (`2e2ebe7`). M12d (D-059) runs beside M6 in disjoint paths; M7a to M7f wait for both.
- **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.
- **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.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.