SPEC v0.16: D-098 goldens at 1x in three variants, D-099 no licences row, D-102 S-7 editor rows; R8 release build; HANDOFF, prompts

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 21:47:02 +02:00
parent bacbd1896c
commit bfc3acbbe0
5 changed files with 48 additions and 35 deletions
+14 -8
View File
@@ -1,6 +1,6 @@
# Kvitt: specification
Version **0.15** (2026-10-18). Schema version **3**. Sync protocol version **1**. Push payload version **1**.
Version **0.16** (2026-10-18). Schema version **3**. 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.
@@ -433,7 +433,7 @@ 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: 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).
**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-098, M8c, revises D-066): three goldens per component sheet or screen state, `<name>_sv_light.png`, `<name>_sv_dark.png` and `<name>_en_light.png` (English covers string length, dark covers the theme; there is no `en_dark`), plus `<name>_sv_light_200.png` at 200 % font scale for the long states; `:app`'s `empty_screen_<light|dark>.png` are the only other names. 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. Every capture renders at 1x density through the Robolectric qualifier `w411dp-h<height>dp-mdpi`, so a full screen of 411 × 891 dp is 411 × 891 px. Every committed PNG (goldens and the `docs/ux/` mock-ups) is compressed losslessly with `oxipng -o 4 --strip safe` before committing (`7c453d8`); Roborazzi verifies against the compressed files. Feature modules follow the same layout: each feature module has Roborazzi screenshot tests of its screen states with goldens in `src/test/screenshots/`, rendered full screen. The reduction took the goldens from 1143 PNGs and 131 MB to 865 and 26 MB, the mock-ups from 36.8 to 26.0 MB, for a small repository on the home Gitea server.
**Hilt wiring (D-081, M8).** Both variants bind every `:core:model` interface to `:data`: `android/app/src/main/.../di/AppDataModule.kt` includes `DataBindingsModule` (D-041), and `AppBuildInfo` comes from `BuildConfig` in both variants. The debug fakes module (`72f21b2`) and the release-only data module were deleted by M8; no Hilt module lives in `src/debug` or `src/release`. Fakes live only in test source sets and feature previews and never reach an installed build; `:app`'s unit tests run on a plain `Application`, not the Hilt application.
@@ -990,7 +990,8 @@ Used only if the certificate sync cannot be made to work. Documented in `infra/R
keytool -genkeypair -v -keystore kvitt-release.jks -alias kvitt -keyalg RSA -keysize 4096 -validity 10000
```
The file is git-ignored by the `*.jks` pattern and never committed (D-101, revises D-024's "outside the repository"). `android/local.properties` holds its absolute path and secrets in the keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias` (`kvitt`), `kvitt.release.keyPassword`. Losing it means the phones must uninstall and reinstall (data comes back by sync). The owner keeps a copy of the file and its password in their password manager. Without these keys in `local.properties`, `:app:assembleRelease` builds an unsigned APK (M8).
The file is git-ignored by the `*.jks` pattern and never committed (D-101, revises D-024's "outside the repository"). `android/local.properties` holds its absolute path and secrets in the keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias` (`kvitt`), `kvitt.release.keyPassword`. Losing it means the phones must uninstall and reinstall (data comes back by sync). The owner keeps a copy of the file and its password in their password manager. Without these keys in `local.properties`, `:app:assembleRelease` builds an unsigned APK (M8). Release signing was verified on 2026-10-18: alias `kvitt`, RSA 4096, APK signature scheme v2.
* Release builds are R8-shrunk, optimised and obfuscated (`isMinifyEnabled`, `isShrinkResources`, `proguard-android-optimize.txt` plus `android/app/proguard-rules.pro`, no project keep rules needed as of 2026-10-18; landed `57f984f`); the signed APK is about 11 MB (was 43 MB); `infra/publish-apk.sh` saves `mapping.txt` next to the APK as `apk/kvitt-<version_code>.mapping.txt` for retrace (the script is still to be written; under D-100 M2c-pi decides where it lives).
* `infra/publish-apk.sh` on the Mac: builds `:app:assembleRelease` through `tools/gradle-serial.sh`, computes SHA-256 and size, copies the APK to the Pi's `data` volume as `apk/kvitt-<version_code>.apk`, writes `apk/latest.json` atomically (write temp, rename):
```json
@@ -1150,6 +1151,10 @@ M8, M7g (landed) ─► M12e contract pass (landed 1470a2f and 90505a8; D-086 to
After the first phone test (v0.15):
M8 phone test (2026-10-18) ─► M8b UI fixes (landed f0575d3, D-097) ─► M2c-pi bundle (D-100), last, after the app is proven ─► M10
After v0.15 (v0.16):
M8b ─► M8c goldens and About (landed b481fab, 64f5706, 7c453d8; D-098, D-099) ─► M8d editor rows (landed 3c68594, D-102) ─► M8e (D-107)
R8 release build (landed 57f984f)
```
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), as the last step once the app is proven on the phone against the development server (D-100). 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) ran in parallel (disjoint paths: `android/app/**` and `android/adapter/fcm/**` against `android/feature/**`) and landed on 2026-10-18 (`34a2590`; `369e516` and `00a14a3`). The contract pass M12e (D-086 to D-095) ran after them and landed on 2026-10-18 (`1470a2f` contract pass, `90505a8` launcher icon).
@@ -1198,7 +1203,7 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
* **M6b** (landed `3a1bd14`) is the `:core:design` fix pass after the M7 wave, in M6's paths (§14.7); see the §11.2 row.
* **M12e** (landed `1470a2f` and `90505a8` on 2026-10-18, D-086 to D-095) took the contract requests that M5, M6, M7 and M8 had collected here, as one contract change on both sides (§14.8): the invite kind from `pair()` (D-086), the admin's name in invite links (D-087), `SyncSummary.lastAttemptFailed` (D-088), `Rejection.actor` (D-089), the device version name with server schema 3 (D-090), `NewRecurringTemplate` without an id (D-091), `UpdateCheck` (D-092), `SettleState.untouched` (D-093), the D-027 re-stamp in `:data` (D-094), and `PushEvent` in `:core:model`, the six screen-hosted snackbar hosts, snackbar resource ids from S-7 and S-8 and the launcher icon (D-095). The 128 px picture size was left unchanged by assumption: `:data` keeps fetching E-11 at 512 and no size parameter was added, since v1 does not need one (the owner confirmed on 2026-10-18 that it is not needed: single 512 size, §14.2.4). Room stays at schema 1 (the phone stores no devices). A phone running the M12e APK needs a server at schema 3 (§14.8 "Compatibility").
* **Remaining contract requests** (none is a contract change in v0.14; it needs the §14.8 rule when taken):
* None open. S-22's "Licenser för öppen källkod" did nothing (no licences screen and no strings for one); the owner answered OQ-5 on 2026-10-18: no licences screen in v1, the button is hidden (implemented by M8c).
* None open. S-22's "Licenser för öppen källkod" did nothing (no licences screen and no strings for one); the owner answered OQ-5 on 2026-10-18: no licences screen in v1, the row is removed (implemented by M8c, `64f5706`, D-099).
The `fid` request was taken on 2026-10-18 (`c694f78`, D-096): the server sends the installation id in FCM HTTP v1 `message.fid` (§14.6).
* **M2c-dev / M2c-pi** (D-001, D-100): M2c-dev builds only what development needs on the Mac (landed); 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) as the `infra/pi/` bundle the owner runs, and runs last, after M8 and the phone test, before M10.
@@ -1224,7 +1229,7 @@ Only things that cannot be decided without the owner. Everything else in this sp
| OQ-2 | Answered (D-003): two exports, English and Swedish header variants, member columns in either order, ISO dates, point decimal, double-quoted fields, trailing empty line and `Total balance` / `Totalsumma` row, payment category `Payment` / `Betalning`, all rows SEK. Group "hemma" becomes account "Hemma", the general group becomes account "Vardag". Details §10.8 and PLAN-M9. | M9, M11 | answered |
| OQ-3 | Answered by placeholders (D-100): the LAN address of the Kvitt Pi; LAN address, SSH port and user of the Home Assistant Pi; the directory holding `fullchain.pem` and `privkey.pem` there (expected `/ssl` on Home Assistant OS); which SSH add-on is used. The owner fills them in the M2c-pi bundle's `.env` on the Pi; no agent needs them. | M2c-pi | answered |
| OQ-4 | Answered: the Google Drive backup folder id is `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV` (§10.9). | M2c-pi | answered |
| OQ-5 | Answered 2026-10-18: no licences screen in v1; S-22's "Licenser för öppen källkod" is hidden (implemented by M8c). | M10 | answered |
| OQ-5 | Answered 2026-10-18 and implemented (D-099, `64f5706`): no licences screen in v1; S-22 has no "Licenser för öppen källkod" row. | M10 | answered |
---
@@ -1266,7 +1271,7 @@ Each screen must be designed in these states where they apply: **loaded**, **emp
| 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 ("Sync failed" when `SyncSummary.lastAttemptFailed`; `SyncStatusPill` takes no `failed` parameter, D-088); 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), `onSaved` handing `:app` the string resource id (D-095); 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-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"); account, date, payer and split shown as four fixed full-width rows in that order, which do not move when a value changes (D-102); 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), `onSaved` handing `:app` the string resource id (D-095); 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); after a delete `:app` shows `snackbar_expense_deleted` on the screen returned to (D-095) | 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 reads `SettleState.untouched` (nets without the settlement being edited, empty for a per-account settle-up, D-093, revising D-074's reading of Home balances); 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 |
@@ -1281,7 +1286,7 @@ Each screen must be designed in these states where they apply: **loaded**, **emp
| 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; a new template is sent as `NewRecurringTemplate` (`RecurringForm.newTemplate()`), an edit as `RecurringTemplate` (`RecurringForm.template(original, …)`), D-091; the discard dialog has no body text (D-080) | Add; Edit; Pause/Resume; Delete |
| S-20 | Devices | Rows per AC-3.1; the app version shows `Device.appVersionName` ("Kvitt 1.1"), else the version code (D-090) | 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 (not built; OQ-5); after "Check for update": `UpdateCheck.UpToDate` shows "Du har den senaste versionen" (`settings_about_up_to_date`), `UpdateCheck.Failed` shows "Kunde inte nå servern" (`common_could_not_reach_server`) (AC-16.6, D-092) | Check for update |
| S-22 | About | App version name and code, server URL, server version (from `/health`); no open-source licences row (OQ-5, D-099); after "Check for update": `UpdateCheck.UpToDate` shows "Du har den senaste versionen" (`settings_about_up_to_date`), `UpdateCheck.Failed` shows "Kunde inte nå servern" (`common_could_not_reach_server`) (AC-16.6, D-092) | Check for update |
System notifications (copy needed in both languages, built per AC-13.5):
@@ -1325,7 +1330,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. Amended after U2: chart bars have top corners 4 dp and bottom corners 2 dp (D-097, design-spec §2.9 and §3.10). `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:
**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. Amended after U2: chart bars have top corners 4 dp and bottom corners 2 dp (D-097, design-spec §2.9 and §3.10); S-7's account, date, payer and split options are four fixed rows, `EditorOptionRows`, and connected buttons have 8 dp side padding (D-102, design-spec §3.7 and §3.10); S-22 has no licences row (D-099, design-spec S-22); the mock-up PNGs are compressed losslessly (D-098). `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; 577 since v0.16 without `settings_about_licences`, D-099; 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. 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`.
@@ -2744,3 +2749,4 @@ Rule: a change to any other contract file, or a semantic change to `contracts/op
| 0.13 | 2026-10-18 | Outcome of the M12e contract pass (`1470a2f` contract pass, `90505a8` launcher icon; gates green: Android BUILD SUCCESSFUL, 710 actionable tasks, 906 tests; server 253 tests with the OpenAPI equality, vector and migration byte-identity tests). D-086: E-3 returns `invite_kind`; `SessionRepository.pair` returns `OnlineResult<Pairing(user, inviteKind)>`, an unknown kind reads as `device`; S-2 follows only a `partner` pairing; `FakeScenario.pairingInviteKind` (AC-2.4, AC-2.10, §13.2, §13.3 S-2, §14.2.4, E-3). D-087: E-12 link `<public_url>/i/<code>?from=<admin name>`; E-4 takes optional `from` and passes it only into `kvitt://invite?server&code&from`, never shown, an invalid value dropped; `InviteLink`, `AppLink.Invite`, `PairingScreen(initialAdminName)`, `PairingViewModel.prefill` carry it; S-1 shows `onboarding_code_help` with the name; links without a name still parse (AC-2.5, AC-2.6, §13.3 S-1, E-4, E-12). D-088: `SyncSummary.lastAttemptFailed`; `SyncStatusPill`, `syncPillVariant`, `syncLineText` drop `failed`; S-16 failed = online and `lastAttemptFailed` (§13.3 S-4, S-16, §14.2.4, §11.3). D-089: `Rejection.actor` from the stored `current.updated_by_user_id`, partner as fallback; no protocol change (§14.2.4, §14.4.6). D-090: optional request header `X-Kvitt-App-Version-Name` on every operation, stored at E-3 and the hourly last-seen update, blank or over 32 characters ignored; migration 0003 adds `devices.app_version_name`, server schema 3 (new §14.1.2, contract copy with byte-identity test, v2→v3 migration test); `app_version_name` on the Device wire object; `Device.appVersionName`; S-20 shows "Kvitt 1.1"; importer expects schema 3; Room unchanged at 1 (§8.2, §10, §10.8, §14.1, §14.3.1, §14.3.3, E-1, E-3, AC-3.1, §13.3 S-20). D-091: `RecurringRepository.create(NewRecurringTemplate)`, the server mints the id (§13.3 S-19, §14.2.4, E-28). D-092: `UpdateRepository.check(): UpdateCheck` (`UpToDate`, `Available`, `Failed`), 404 is up to date, debug reports `UpToDate`; S-22 up-to-date or could-not-reach-server; revises D-073 (AC-16.6, §13.3 S-22, §14.2.4). D-093: `SettleState.untouched` with nets excluding the edited settlement, empty per account; revises D-074's S-9 reading (§13.3 S-9, §14.2.4). D-094: the D-027 re-stamp as implemented: at the start of a cycle with `clock_skew` set, pending rows stamped later than now are re-stamped to now (§14.4.5). D-095: `PushEvent` in `:core:model`; six screens host the shared snackbar host, no `:app` overlay host; snackbar resource ids from S-7 and S-8; adaptive vector launcher icon (`#9C5238`, `receipt_long` at 48 dp, monochrome layer); revises D-071 (§6.2, §13.2, §13.3 S-7, S-8, §14.2.4, §14.6). Without a decision id: the 128 px picture size left unchanged by assumption (owner question open); the M12e list in §11.3 replaced by its outcome and a "Remaining contract requests" list (OQ-5 licences, `fid`); goldens re-recorded (S-20 devices; new `s1_pairing_link_admin`, `s22_about_check_failed`); compatibility note: the M12e APK needs a schema-3 server, so the server is redeployed first (§14.8); dev stack at schema 3, `up.sh --down` then `up.sh` on Podman (§10). §11: M12e landed (§11.1, §11.2). |
| 0.14 | 2026-10-18 | Follow-ups after M12e (`2b4e21c`, `c694f78`, `1e404cc`). D-096: the server sends the stored `fcm_token` (the Firebase Installation ID of D-083) in FCM HTTP v1 `message.fid`, no longer in the deprecated `message.token`; no database or wire change, the column and the E-17 field keep the name `fcm_token`; `contracts/examples/push/*.json` use `fid`; revises D-083's "no server change" (§14.6, AC-13.7, §11.3). Without a decision id: the Pi image's C stage and CA roots and an OAuth plus FCM call from the arm64 image confirmed on the Mac (`docs/spikes/pi-image-arm64.md`; 63.8 MB, cold build 48 s), running on the Pi stays M2c-pi's (§10.3); Vico removed from the version catalog (`2b4e21c`), D-063 pointer (§6.2); §11.3 remaining contract requests: only OQ-5. |
| 0.15 | 2026-10-18 | The first phone test, M8b (`f0575d3`) and owner answers. D-097: chart bars top corners 4 dp, bottom 2 dp, replacing 10/4 (§6.2, §13.4, design-spec §2.9, §3.10). D-100: no SSH from the Mac to the Pis; M2c-pi delivers the `infra/pi/` bundle the owner copies to the Kvitt Pi and runs, OQ-3 values as `.env` placeholders, M2c-pi last after the app is proven, no agent allowances on the Pis, Home Assistant, Gitea or Drive; revises D-023, keeps D-001 (§10 intro, §10.1, §10.10, §11.1, §11.2, §11.3, §12). D-101: the release keystore `kvitt-release.jks` at the repository root, git-ignored by `*.jks`, absolute path in `android/local.properties`, copy and password in the owner's password manager; revises D-024 (§10.6, §11.2 M10, §11.3). Without a decision id: the M8 end-to-end test passed on the owner's S26 against the development server (pairing, scripted partner via a second admin device, offline pending and sync, `fcm_token` and `app_version_name` registered, push by `fid` delivered and tapped); M8b fixes (invite-code `VisualTransformation`, quick-row badge above the ripple clip, `KvittTopAppBar` status-bar inset, 221 goldens) and `FcmMessageBody.fid` (`09f474f`) (§11.2 M8, §11.3); OQ-5 answered: licences button hidden in v1 (M8c); the 128 px picture size not needed (§14.2.4); the "boende" keyword keeps suggesting `travel` and the account page split bar stays as designed (no change). |
| 0.16 | 2026-10-18 | M8c (`b481fab`, `64f5706`, `7c453d8`), the R8 release build (`57f984f`) and M8d (`3c68594`). D-098: goldens in three variants `<name>_sv_light`, `<name>_sv_dark`, `<name>_en_light` plus `<name>_sv_light_200`, rendered at 1x through `w411dp-h<height>dp-mdpi`, every committed PNG compressed with `oxipng -o 4 --strip safe` (1143 PNGs / 131 MB to 865 / 26 MB, mock-ups 36.8 to 26.0 MB); revises D-066 (§6.2, §13.4). D-099: S-22 has no open-source licences row, `onOpenLicences` and `settings_about_licences` removed, `strings.csv` 577 rows; closes OQ-5 (§11.3, §12, §13.3, §13.4, design-spec S-22). D-102: S-7 options as four fixed full-width rows (Konto, Datum, Betalt av, Delning), `EditorOptionRows` in `:core:design`; connected buttons 8 dp side padding (§13.3, §13.4, design-spec §3.7, §3.10). Without a decision id: release builds R8-shrunk, optimised and obfuscated, APK about 11 MB (was 43 MB), `mapping.txt` kept beside the APK; release signing verified (alias `kvitt`, RSA 4096, v2) (§10.6); §11.1 graph. |
+22 -20
View File
@@ -3,49 +3,51 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- `main` at `09f474f` (M12e follow-up: `FcmMessageBody` uses `fid`) plus the SPEC v0.15 docs commit from `agent-docs-v015` 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, deleted by M8), `e6ad858` app release wiring of `:data` (pre-M8), `b9b061a` M7a, `6ac711d` M7e, `082b8dc` M7d, `31760f9` M7b, `1e42f76` M7c, `a54eb7c` M7f, `3a1bd14` M6b, `d504004` SPEC v0.11, `34a2590` M7g, `369e516` and `00a14a3` M8, `cf8ef45` SPEC v0.12, `1470a2f` M12e contract pass, `90505a8` M12e launcher icon, `03b7184` SPEC v0.13, `2b4e21c` Vico catalog entries removed (D-063), `c694f78` server FCM target in `fid` (D-096), `1e404cc` Pi image arm64 check (`docs/spikes/pi-image-arm64.md`), `fd60170` SPEC v0.14, `f0575d3` M8b UI fixes from the first phone test, `09f474f` `FcmMessageBody.fid` in `:data` (D-096 follow-up).
- 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), M12d (`bdba9d3`), M6 (`3080804`), M7a to M7f, M6b (`3a1bd14`), SPEC v0.11 (`d504004`, D-069 to D-080), M7g feature cleanups (`34a2590`), M8 app integration (`369e516` + `00a14a3`, landed 2026-10-18, gate green: BUILD SUCCESSFUL, 710 actionable tasks), SPEC v0.12 (`cf8ef45`, D-081 to D-085), M12e contract pass (`1470a2f` + `90505a8`, landed 2026-10-18; gates green: Android BUILD SUCCESSFUL, 710 actionable tasks, 906 tests; cargo 253 tests, OpenAPI equality, vectors and migration byte-identity green), SPEC v0.13 (`03b7184`, D-086 to D-095), Vico removal (`2b4e21c`), FCM `fid` target (`c694f78`, D-096), Pi image arm64 check (`1e404cc`: C stage, CA roots and an OAuth plus FCM call from the arm64 image confirmed on the Mac; running on the Pi stays M2c-pi's), SPEC v0.14 (`fd60170`, D-096), M8 on-phone test (2026-10-18, passed), M8b (`f0575d3`, D-097), `FcmMessageBody.fid` (`09f474f`).
- `main` at `3c68594` (M8d) plus the SPEC v0.16 docs commit from `agent-docs-v016` 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, deleted by M8), `e6ad858` app release wiring of `:data` (pre-M8), `b9b061a` M7a, `6ac711d` M7e, `082b8dc` M7d, `31760f9` M7b, `1e42f76` M7c, `a54eb7c` M7f, `3a1bd14` M6b, `d504004` SPEC v0.11, `34a2590` M7g, `369e516` and `00a14a3` M8, `cf8ef45` SPEC v0.12, `1470a2f` M12e contract pass, `90505a8` M12e launcher icon, `03b7184` SPEC v0.13, `2b4e21c` Vico catalog entries removed (D-063), `c694f78` server FCM target in `fid` (D-096), `1e404cc` Pi image arm64 check (`docs/spikes/pi-image-arm64.md`), `fd60170` SPEC v0.14, `f0575d3` M8b UI fixes from the first phone test, `09f474f` `FcmMessageBody.fid` in `:data` (D-096 follow-up), `e7d963a` SPEC v0.15, `b481fab` M8c goldens at 1x in three variants (D-098), `64f5706` M8c S-22 licences row removed (D-099), `7c453d8` every PNG through oxipng, `57f984f` R8 release build, `3c68594` M8d editor rows (D-102).
- 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), M12d (`bdba9d3`), M6 (`3080804`), M7a to M7f, M6b (`3a1bd14`), SPEC v0.11 (`d504004`, D-069 to D-080), M7g feature cleanups (`34a2590`), M8 app integration (`369e516` + `00a14a3`, landed 2026-10-18, gate green: BUILD SUCCESSFUL, 710 actionable tasks), SPEC v0.12 (`cf8ef45`, D-081 to D-085), M12e contract pass (`1470a2f` + `90505a8`, landed 2026-10-18; gates green: Android BUILD SUCCESSFUL, 710 actionable tasks, 906 tests; cargo 253 tests, OpenAPI equality, vectors and migration byte-identity green), SPEC v0.13 (`03b7184`, D-086 to D-095), Vico removal (`2b4e21c`), FCM `fid` target (`c694f78`, D-096), Pi image arm64 check (`1e404cc`: C stage, CA roots and an OAuth plus FCM call from the arm64 image confirmed on the Mac; running on the Pi stays M2c-pi's), SPEC v0.14 (`fd60170`, D-096), M8 on-phone test (2026-10-18, passed), M8b (`f0575d3`, D-097), `FcmMessageBody.fid` (`09f474f`), SPEC v0.15 (`e7d963a`, D-097, D-100, D-101); landed today after v0.15: M8c (`b481fab`, `64f5706`, `7c453d8`; D-098, D-099), the R8 release build (`57f984f`), M8d (`3c68594`, D-102).
- M8 outcome (recorded in SPEC v0.12 as D-081 to D-085): both variants bind `:data` through `di/AppDataModule.kt`, debug fakes module gone (D-081, revises D-041); debug network security config allows cleartext everywhere, the AC-2.8 limit is enforced in `SessionRepositoryImpl.normaliseServerUrl`, pinned-root config behind `kvitt.tls.pinnedRoot` (D-082); FCM registration by Firebase Installation ID, firebase-messaging 26.0.0, no server change (D-083); invite links ignored while paired (D-084); generic-notification, deletion and `sync_batch` taps open Home, push in an expedited WorkManager job (D-085).
- M8 end-to-end test (2026-10-18, dev server, owner's S26): pairing with the bootstrap code OK; a scripted partner joined through a second admin device paired with `kvitt-server pair-device` (the CLI has no partner-invite subcommand: small ops request for a later server plan); Home showed the account; the offline expense showed the pending marker and synced after reconnect (pending 0, the server has the row); the owner's device registered `fcm_token` and `app_version_name`; a push by `fid` was delivered and the notification tap worked. Still unverified on the phone: the status-bar fix (next install) and the design-spec §2.7 typeface check (the owner looked, no complaint).
- M8b outcome (SPEC v0.15): invite-code field holds the raw code, hyphens as a `VisualTransformation` (cursor bug fixed, unit test); quick-row selected badge drawn above the ripple clip; `KvittTopAppBar` pads for the status-bar inset (`windowInsets` parameter), `KvittScaffold` follows; chart bars top 4, bottom 2 dp (D-097); 221 goldens re-recorded. `FcmMessageBody.fid` in `:data` followed (`09f474f`).
- M8c, R8 and M8d outcome (SPEC v0.16): goldens `<name>_sv_light`, `_sv_dark`, `_en_light` plus `_sv_light_200`, at 1x (`w411dp-h<height>dp-mdpi`), oxipng-compressed: 1143 PNGs / 131 MB to 865 / 26 MB, mock-ups 36.8 to 26.0 MB (D-098, revises D-066); S-22 has no licences row, `settings_about_licences` gone (D-099, OQ-5 closed); release APK R8-shrunk, about 11 MB (was 43 MB), `mapping.txt` to be kept beside the APK by the publish script (SPEC §10.6); release signing verified (alias `kvitt`, RSA 4096, v2); S-7 options as four fixed rows `EditorOptionRows`, connected buttons 8 dp side padding (D-102).
- Owner answers 2026-10-18: "boende" keeps suggesting `travel` (no change); the 128 px picture size is not needed; OQ-5 "no, hide the button" (implemented by M8c); the account page split bar stays as designed; M2c-pi becomes an owner-run bundle (D-100); the keystore lives at the repository root (D-101); the Gitea push waits for fewer goldens and a history rewrite, no LFS.
- M12e outcome (recorded in SPEC v0.13 as D-086 to D-095): E-3 `invite_kind` and `Pairing`, S-2 only after a `partner` pairing (D-086); admin name in E-12 links via `?from=`, passed on by E-4, S-1 help line names the admin (D-087); `SyncSummary.lastAttemptFailed` (D-088); `Rejection.actor` (D-089); `X-Kvitt-App-Version-Name`, migration 0003, server schema 3, S-20 "Kvitt 1.1" (D-090); `NewRecurringTemplate` (D-091); `UpdateCheck` (D-092, revises D-073); `SettleState.untouched` (D-093, revises D-074); D-027 re-stamp in `:data` (D-094); `PushEvent` in `:core:model`, six screen-hosted snackbar hosts, snackbar resource ids, adaptive vector launcher icon (D-095, revises D-071). 128 px picture size: the owner confirmed it is not needed (single 512 size, SPEC §14.2.4).
- Compatibility: the M12e APK cannot pair or list devices against a pre-M12e server (DTOs without defaults, by design), so the server is rebuilt and redeployed before the new APK is installed (SPEC §14.8). Old phones on the new server work.
- In flight: M8c (goldens reduction, D-098, D-099: the licences button hidden), M8d (editor rows, D-102), M2c-pi bundle `infra/pi/` (D-103 to D-106), a release-signing check, `agent-docs-v015` (this docs change: SPEC v0.15, D-097, D-100, D-101).
- Remote: `origin` on Gitea exists and the SSH key works; the first push waits for the golden reduction (M8c) and a history rewrite (owner decision 2026-10-18: option "fewer goldens", no LFS). Push needs the owner's explicit ask.
- In flight: M2c-pi bundle `infra/pi/` (D-103 to D-106), M8e (S-19 editor rows, D-107), `agent-docs-v016` (this docs change: SPEC v0.16, D-098, D-099, D-102). Then a one-off history rewrite (drop the superseded PNG blobs; no clone exists) and the first push to `origin` (owner authorised, no confirmation needed), which also runs the Gitea Actions workflow.
- Remote: `origin` on Gitea exists and the SSH key works; the golden reduction (M8c) has landed; the history rewrite and the first push follow this docs pass (owner authorised 2026-10-18, no further confirmation). The owner created the Gitea secret `kvitt_cicd` and the Drive token (`infra/secrets/rclone-drive-token.json`, turned into `rclone.conf`; both git-ignored).
- 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 green on `main` as far as reported.
- The UX sprint branch `UX-sprint` and its worktree `~/orca/workspaces/kvitt/UX-sprint` were removed on 2026-10-18 (fully merged).
- 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`; rebuilt on 2026-10-18 with the M12e server and the `fid` target (a push by `fid` arrived in the M8 test), database at schema 3, data kept. Backups `infra/dev/backups/kvitt-20261018T093143Z.sqlite.gz` (before schema 2) and `infra/dev/backups/kvitt-20261018T130443Z.sqlite.gz` (before schema 3) exist (git-ignored). On this Podman setup `infra/dev/up.sh` alone does not swap in a rebuilt image: run `infra/dev/up.sh --down`, then `infra/dev/up.sh`. The owner's phone paired with the bootstrap code on 2026-10-18; a scripted partner exists, joined through a second admin device paired with `kvitt-server pair-device` (tokens held by the orchestrator outside the repo). 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`, paired against the dev server for the M8 test on 2026-10-18; the next install carries M8b and the M8c and M8d work.
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug`, paired against the dev server for the M8 test on 2026-10-18; the next install carries M8b, M8c and M8d.
## 2. Open work (one line each, with who owns it: agent or owner)
- Land `agent-docs-v015` (orchestrator): SPEC v0.15, D-097, D-100, D-101, HANDOFF, `prompts/README.md`, design-spec §2.9 and §3.10.
- Land M8c, M8d, the M2c-pi bundle and the release-signing check when they report (orchestrator), then a docs agent for v0.16 (D-098, D-099, D-102 to D-106).
- Install the next debug build and have the owner look (§3 item 1).
- First push to Gitea `origin` after M8c and the history rewrite (orchestrator, on the owner's ask).
- Land `agent-docs-v016` (orchestrator): SPEC v0.16, D-098, D-099, D-102, HANDOFF, `prompts/README.md`, design-spec §3.7, §3.10 and S-22.
- Land the M2c-pi bundle (D-103 to D-106) and M8e (D-107) when they report (orchestrator), then a docs agent for v0.17.
- History rewrite (drop superseded PNG blobs; no clone exists), then the first push to Gitea `origin` (orchestrator; owner authorised, no confirmation needed); the push runs the Gitea Actions workflow (§3 item 3).
- Install the next debug build and have the owner look (§3 item 1); build a signed release for the smoke taps (§3 item 2).
- Cleanup (agents of the owning modules, no plan yet): 20 unused strings R8 found (`editor_next`, `home_notification_rationale`, `settle_empty_settled`, `home_sync_status_tag_*`, …) could be deleted by their modules; optional `localeFilters` en+sv.
- Ops request (no plan yet): a `kvitt-server` subcommand for a partner invite, so a scripted partner needs no second admin device.
- Plans (SPEC §11.2 v0.15), in dependency order; prompt `prompts/<id>.md`:
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b, M9, M12a, M12b, M12c, M12d, M12e, U1, U2: landed.
- Now: M8c, M8d, M2c-pi bundle, release-signing check.
- Plans (SPEC §11.2 v0.16), in dependency order; prompt `prompts/<id>.md`:
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b, M8c, M8d, M9, M12a, M12b, M12c, M12d, M12e, U1, U2, R8 release build: landed.
- Now: M2c-pi bundle, M8e.
- Next: the owner runs the M2c-pi bundle on the Kvitt Pi, after the app is proven (D-100); M10 First release after M2c-pi (keystore per §10.6, D-101); 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)
1. Look at the next install on the phone (yes/no each): the title bar sits below the status bar; the quick-row selected badge is whole; the invite-code field cursor stays after the last character; the expense editor rows (M8d).
2. Keystore (D-101): add the four `kvitt.release.*` lines to `android/local.properties`, or let the agent do it (in flight); keep a copy of `kvitt-release.jks` and its password in the password manager.
3. Pi (D-100), after the app is proven: copy the `infra/pi/` bundle to the Kvitt Pi, fill the placeholders in its `.env`, run the README checklist.
4. Look through the M6 component screenshots (`android/core/design/src/test/screenshots/`); approved in advance, nothing blocks on it.
1. Next install on the phone (yes/no each): the title bar sits below the status bar; the quick-row selected badge is whole; the invite-code field cursor stays after the last character; the expense editor shows Konto, Datum, Betalt av, Delning as four rows that do not move when a value changes; "Household" fits its half of the Analytics filter bar.
2. Release build smoke taps: the app opens; S-1 or Home renders; Settings opens and shows the version; add one expense and see it sync.
3. After the first push: tell me whether the Gitea Actions run succeeded.
4. Later, Pi (D-100), after the app is proven: copy the `infra/pi/` bundle to the Kvitt Pi, fill the placeholders in its `.env`, run the README checklist.
## 4. Owner questions (asked, unanswered)
- None. OQ-3 is answered by placeholders (D-100); OQ-5 answered (hide the button).
- None. OQ-3 is answered by placeholders (D-100); OQ-5 answered and implemented (D-099).
## 5. Paused (waiting on something outside the repo)
- Nothing.
## 6. Numbering
- Decisions: D-001 to D-097 used plus D-100 and D-101 (D-008 and D-009 unused; D-098, D-099 reserved by M8c, D-102 by M8d, D-103 to D-106 by M2c-pi). Next free: **D-107**.
- Spec version: v0.15 written; next **v0.16** (taken by the M8c, M8d and M2c-pi outcomes).
- Server schema version **3** (migration 0003, landed with M12e); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**.
- Decisions: D-001 to D-102 used except D-008 and D-009 (D-103 to D-106 reserved by M2c-pi, D-107 by M8e). Next free: **D-108**.
- Spec version: v0.16 written; next **v0.17** (taken by the M2c-pi and M8e outcomes).
- Server schema version **3** (migration 0003, landed with M12e); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Both unchanged by v0.16.
- Sync protocol **1**, push payload **1**.
## 7. How the work is run
+4 -1
View File
@@ -70,7 +70,7 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| 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.~~ (Vico removed from the catalog on 2026-10-18, `2b4e21c`; SPEC v0.14.) 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; 4/2 since D-097) 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-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;~~ (revised by D-098: three variants `sv_light`, `sv_dark`, `en_light` plus `sv_light_200`, at 1x density) 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") |
@@ -102,5 +102,8 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-095 | 2026-10-18 | `PushEvent` lives in `:core:model` (`model/Push.kt`); `:adapter:fcm` keeps `PushListener` and `PushMapping`. Six screens host the shared `snackbarHostState` in their own scaffold (Home, AccountDetail, ExpenseDetail, SettlementDetail, DrillDown, SyncStatus); `:app` has no overlay host. `ExpenseEditorScreen.onSaved(ExpenseId, @StringRes Int?)`, `onDeleted()` without argument on S-7 and S-8; `:app` shows `snackbar_expense_deleted`. Launcher icon: adaptive, vectors only, `mipmap-anydpi/ic_launcher.xml`, background `#9C5238` (light primary), foreground and monochrome `receipt_long` mark at 48 dp, manifest `icon` and `roundIcon`; lint `MissingApplicationIcon` cleared. Revises D-071. | M8 report (§11.3 list): `PushEvent` was in the adapter, the overlay host could cover a FAB or the bottom bar, S-7 and S-8 handed over resolved strings, the app had no launcher icon. | SPEC §6.2, §13.2, §13.3 (S-7, S-8), §14.2.4, §14.6, §11.3 |
| D-096 | 2026-10-18 | The server sends the device's stored `fcm_token` (the Firebase Installation ID of D-083) in the FCM HTTP v1 `message.fid` field, no longer in the deprecated `message.token`. No database or wire change: the `devices.fcm_token` column and the E-17 field keep the name `fcm_token`. `contracts/examples/push/*.json` use `fid` (a contract-file change, hence this entry and SPEC v0.14). Landed `c694f78`. Revises D-083's "no server change". | The FCM HTTP v1 reference (projects.messages, last updated 2026-09-08, read 2026-10-18) deprecates `token` with "Use fid instead" and defines `fid` as the Firebase Installation ID target; `token`, `fid`, `topic` and `condition` are mutually exclusive. Live probe from the arm64 image (`docs/spikes/pi-image-arm64.md`): FCM answered "unregistered" for a made-up id, so the field is accepted. | SPEC §14.6, AC-13.7, §11.3; `server/crates/kvitt-server/src/push/fcm.rs`, `contracts/examples/push/` |
| D-097 | 2026-10-18 | S-11 chart bars have top corners 4 dp and bottom corners 2 dp on the whole bar, replacing top 10, bottom 4. The 2 dp gaps between segments and the clip of the whole bar (not per segment) are unchanged. Assumption recorded: the owner's "4 outer / 2 inner" means top / bottom. Landed by M8b (`f0575d3`) in `KvittShapes` and `Charts.kt`, goldens re-recorded. | First phone test: the owner found the bar tops too round. | SPEC §6.2 (Charts), §13.4; `docs/ux/design-spec.md` §2.9 (shapes table), §3.10 |
| D-098 | 2026-10-18 | Golden layout: three variants per component sheet or screen state, `<name>_sv_light`, `<name>_sv_dark`, `<name>_en_light` (English covers string length, dark covers the theme; no `en_dark`), plus `<name>_sv_light_200` for the long states; rendered at 1x density through the Robolectric qualifier `w411dp-h<height>dp-mdpi`, so a full screen is 411 × 891 px. Every committed PNG, the `docs/ux/` mock-ups included, is compressed losslessly with `oxipng -o 4 --strip safe` (`7c453d8`). Landed by M8c (`b481fab`). Revises D-066 (the compose-once configuration context stays). | The owner wants a small repository for the home Gitea server: the goldens went from 1143 PNGs / 131 MB to 865 / 26 MB, the mock-ups from 36.8 to 26.0 MB. | SPEC §6.2 (Screenshot tests), §13.4; `android/README.md` |
| D-099 | 2026-10-18 | S-22 About has no open-source licences row: `AboutScreen`'s `onOpenLicences` (and `AboutActions.onOpenLicences`, the `:app` call site) and the string `settings_about_licences` are removed, also from `docs/ux/strings.csv` (577 rows). Landed by M8c (`64f5706`). Closes OQ-5. | Owner answer to OQ-5 on 2026-10-18: no licences screen in v1, hide the button. | SPEC §11.3, §12 (OQ-5), §13.3 (S-22), §13.4; `docs/ux/design-spec.md` S-22 |
| D-100 | 2026-10-18 | No SSH from the Mac to the Pis. M2c-pi produces a self-contained bundle `infra/pi/` (compose file, Caddyfile, env template, install, deploy, bootstrap and backup scripts, a README checklist) that the owner copies to the Kvitt Pi and runs there. The OQ-3 values (Kvitt Pi address, Home Assistant Pi address, SSH port and user, certificate directory, SSH add-on) become placeholders the owner fills in the bundle's `.env`; OQ-3 is answered by this. Where the spec has a Mac script act on the Pi (`infra/deploy.sh`, `infra/publish-apk.sh`), the step becomes a bundle script the owner runs; M2c-pi fixes the layout inside `infra/pi/`, the script names and how images and APKs reach the Pi. M2c-pi runs as the last step before M10, after the app is proven on the phone against the development server. Agents never need allowances for the Kvitt Pi, the Home Assistant Pi, Gitea or Google Drive. Revises D-023 and the M2c-pi plan; D-001 stands. | Owner decision 2026-10-18: no agent access to the home servers; the Pi is set up once, by hand, from a checklist. | SPEC §10 intro, §10.1, §10.10, §11.1, §11.2 (M2c-pi, M10), §11.3, §12 (OQ-3); PLAN-M2c-pi |
| D-101 | 2026-10-18 | The release keystore `kvitt-release.jks` (created with the D-024 `keytool` command) lives at the repository root of the main checkout, git-ignored by the `*.jks` pattern of `.gitignore` and never committed; `android/local.properties` references it by absolute path in `kvitt.release.storeFile` with the other three `kvitt.release.*` keys. The owner keeps a copy of the file and its password in their password manager. Revises D-024 ("outside the repository"). | Owner decision 2026-10-18; the `*.jks` ignore rule already keeps the file out of git. | SPEC §10.6, §11.2 (M10), §11.3; PLAN-M10 |
| D-102 | 2026-10-18 | S-7 editor options as fixed rows. Below the category quick row, account, date, payer and split are four full-width rows in a fixed order (Konto, Datum, Betalt av, Delning) instead of a wrapping chip row, so no option moves when a value changes. Rows form a segmented group (outer corners 24, inner 8, gap 3) in surface-container-high (secondary-container while the split section is open), each at least 48 dp and tappable in full. A row has a leading element in a 24 dp box, a label (label-large, muted) in a column as wide as the widest label, the value (body-large, one line, ellipsis) and the trailing affordance at the right edge (expand_more/expand_less, swap_horiz for the payer). At ≥ 1.5× font scale the label sits over the value, which takes up to two lines, and every row keeps the two-line height. TalkBack sentences are unchanged. S-9 and S-19 keep their chips (S-19 follows in M8e, D-107). Also: connected buttons get 8 dp side padding so 'Household' fits half of S-11's filter bar at 1x. Landed by M8d (`3c68594`): `EditorOptionRows` in `:core:design`, `KvittDimens.connectedButtonSide`. | Owner finding on S-7, 2026-10-18: the chips moved when a value changed. | SPEC §13.3 (S-7), §13.4; `docs/ux/design-spec.md` §3.7, §3.10 |
+6 -4
View File
@@ -531,12 +531,14 @@ Every default category has its own colour; `teal` is shared with the archived ex
**`CategoryGridSheet`**: modal bottom sheet "Välj kategori": all active categories in `sort_order`, 4 columns (3 at ≥ 1.5×), items as in the quick row; tapping picks and closes the sheet.
**`EditorChip`** (the chips row under the category row): `FlowRow`, gap 6. Chip: min height 36 (48 target), radius full, `surface-container-high`, padding 4 / 10 / 4 / 8, label 14 / 18 sp weight 500 with an optional key prefix in `on-surface-variant` weight 600 ("Betalt av ", "Delning "), leading 18 dp element, trailing 18 dp icon. Selected (the split section is open): `secondary-container`. Chips:
**`EditorOptionRows`** (S-7, D-102): the account, date, payer and split options as four full-width rows in a fixed order, Konto, Datum, Betalt av, Delning, as a segmented group (outer corners 24, inner 8, gap 3), `surface-container-high`, the Delning row `secondary-container` while the split section is open. Row: min height 48, padding 8 / 16, gap 12: the leading element of the chip table below in a 24 dp box (icons 24), the label (label-large `on-surface-variant`; `editor_account`, `expense_editor_date`, `editor_paid_by`, `editor_split`) in a column as wide as the widest label, the value (body-large, 1 line, ellipsis) and the trailing icon (24) at the right edge; the whole row is one button with the TalkBack sentences below. Rows never move when a value changes. 200 %: the label over the value, the value up to 2 lines, every row keeping the two-line height.
**`EditorChip`** (the chips row of S-9 and S-19): `FlowRow`, gap 6. Chip: min height 36 (48 target), radius full, `surface-container-high`, padding 4 / 10 / 4 / 8, label 14 / 18 sp weight 500 with an optional key prefix in `on-surface-variant` weight 600 ("Betalt av ", "Delning "), leading 18 dp element, trailing 18 dp icon. Selected (the split section is open): `secondary-container`. Chips:
| Chip | Leading | Label | Trailing | Tap |
|---|---|---|---|---|
| Konto | 12 dp dot in the account's chart colour | "Vardag" | `expand_more` | opens `AccountPickerSheet` |
| Datum (S-7 only) | `event` | "Idag" / "Igår" / "14 okt." (§9.8 list format) | `expand_more` | Material `DatePickerDialog`, range per §9.6 |
| Datum (S-9 only) | `event` | "Idag" / "Igår" / "14 okt." (§9.8 list format) | `expand_more` | Material `DatePickerDialog`, range per §9.6 |
| Betalt av | payer avatar 24 | "Betalt av Du" / "Betalt av Anna" | `swap_horiz` | toggles between the two users (AC-8.3), no picker |
| Delning | `call_split` | "Delning Kontots standard 50/50" / "Egen fördelning 70/30" / "Exakta belopp" / "Allt på Anna" | `expand_more` / `expand_less` | opens or closes `SplitSection` in place and scrolls it into view |
@@ -595,7 +597,7 @@ TalkBack: "Konto: Vardag. Dubbeltryck för att byta konto"; "Datum: idag"; "Beta
**`BreakdownList(rows, total, onRow)`** (AC-14.6 charts 2 and 3): rows sorted by amount descending, padding 8 / 0, gap 6: tile 32 (`CategoryTile` or `AccountTile`) + name (title-small, 1 line, ellipsis; "Arkiverat" badge for archived accounts) + right column `MoneyText(Small)` over the percent (body-small muted, one decimal, half-up, "34,8 %", AC-14.10); below, a 10 dp bar track (`surface-container-highest`, radius full) indented 44 dp, filled to amount / largest amount in the group colour. Row is one node "Boende, 62 000 kronor, 34,8 procent"; click → `onRow(id)`.
**`AnalyticsFilterBar(filter, onChange)`**: row 1: period chip (`date_range`, label "Senaste 6 mån" / "Anpassad · 1–30 nov.", `expand_more`; TalkBack "Period: senaste 6 månaderna") + `ButtonGroup` "Månad | Vecka" (min width 170, fills the rest); row 2: `ButtonGroup` "Hushållet | Min del" and `ButtonGroup` "Kategori | Konto", half width each. Connected buttons: min height 40, label-large weight 600, unselected `surface-container-high` / `on-surface-variant`, selected `secondary` / `on-secondary` with full radius. 200 %: one control per row. Semantics: each group a radio group named "Indelning", "Mått", "Gruppera efter".
**`AnalyticsFilterBar(filter, onChange)`**: row 1: period chip (`date_range`, label "Senaste 6 mån" / "Anpassad · 1–30 nov.", `expand_more`; TalkBack "Period: senaste 6 månaderna") + `ButtonGroup` "Månad | Vecka" (min width 170, fills the rest); row 2: `ButtonGroup` "Hushållet | Min del" and `ButtonGroup` "Kategori | Konto", half width each. Connected buttons: min height 40, label-large weight 600, unselected `surface-container-high` / `on-surface-variant`, selected `secondary` / `on-secondary` with full radius; connected buttons: 8 dp side padding (D-102). 200 %: one control per row. Semantics: each group a radio group named "Indelning", "Mått", "Gruppera efter".
**`TotalCard(totalOre, caption)`**: card, padding 16 / 20: "Totalt" (label-large muted; "Totalt · Min del" for my share) over `MoneyText(Large)` over the range caption (body-small muted, "maj–okt 2026 · Hushållet").
@@ -1130,7 +1132,7 @@ Not a screen: the `UpdateBanner` on S-4 (and S-22) and one dialog.
### S-22 About (`:feature:settings`)
**Layout** (app bar: back, "Om Kvitt"; column gap 10): centred app mark 96 (Sunny, `primary`, `receipt_long` 48), "Kvitt 1.1 (11)" (headline-medium), "Delade utgifter för två" (body-medium muted); `UpdateBanner` when an update is available; group of `FactRow`s: Server (`dns`, URL wraps), Serverversion (`memory`, "1.1.0" or "Okänd" when `/health` failed), "Licenser för öppen källkod" (`description`, chevron → the licences screen); "Sök efter uppdatering" (tonal medium, `system_update`, centred; hidden in debug builds, AC-16.5).
**Layout** (app bar: back, "Om Kvitt"; column gap 10): centred app mark 96 (Sunny, `primary`, `receipt_long` 48), "Kvitt 1.1 (11)" (headline-medium), "Delade utgifter för två" (body-medium muted); `UpdateBanner` when an update is available; group of `FactRow`s: Server (`dns`, URL wraps), Serverversion (`memory`, "1.1.0" or "Okänd" when `/health` failed); no open-source licences row (OQ-5, D-099); "Sök efter uppdatering" (tonal medium, `system_update`, centred; hidden in debug builds, AC-16.5).
**States**
+2 -2
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.15). 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`); SPEC v0.11 landed (`d504004`, the M7 and M6b outcomes, D-069 to D-080); M7g feature cleanups landed (`34a2590`, no prompt file; brief by the orchestrator); M8 landed (`369e516`, `00a14a3`), its on-phone test still to run; SPEC v0.12 records the M8 outcomes (D-081 to D-085); the M12e contract pass (no prompt file; brief by the orchestrator) landed (`1470a2f`, `90505a8`); SPEC v0.13 records its outcomes (D-086 to D-095, server schema 3) and landed (`03b7184`); the Vico removal (`2b4e21c`), the FCM `fid` target (`c694f78`) and the Pi image arm64 check (`1e404cc`) landed; SPEC v0.14 records them (D-096); the M8 phone test ran on 2026-10-18 and M8b UI fixes landed (`f0575d3`, D-097) with the `fid` follow-up (`09f474f`); SPEC v0.15 records them and D-100 (M2c-pi as an owner-run bundle) and D-101 (keystore at the repository root). 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; v0.12 D-081 to D-085; v0.13 D-086 to D-095; v0.14 D-096; v0.15 D-097, D-100, D-101.
Dependencies follow SPEC §11.1 and §11.2 (v0.16). 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`); SPEC v0.11 landed (`d504004`, the M7 and M6b outcomes, D-069 to D-080); M7g feature cleanups landed (`34a2590`, no prompt file; brief by the orchestrator); M8 landed (`369e516`, `00a14a3`), its on-phone test still to run; SPEC v0.12 records the M8 outcomes (D-081 to D-085); the M12e contract pass (no prompt file; brief by the orchestrator) landed (`1470a2f`, `90505a8`); SPEC v0.13 records its outcomes (D-086 to D-095, server schema 3) and landed (`03b7184`); the Vico removal (`2b4e21c`), the FCM `fid` target (`c694f78`) and the Pi image arm64 check (`1e404cc`) landed; SPEC v0.14 records them (D-096); the M8 phone test ran on 2026-10-18 and M8b UI fixes landed (`f0575d3`, D-097) with the `fid` follow-up (`09f474f`); SPEC v0.15 records them and D-100 (M2c-pi as an owner-run bundle) and D-101 (keystore at the repository root). 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; v0.12 D-081 to D-085; v0.13 D-086 to D-095; v0.14 D-096; v0.15 D-097, D-100, D-101; after v0.15 M8c (`b481fab`, `64f5706`, `7c453d8`; goldens in three variants at 1x, the S-22 licences row removed), the R8 release build (`57f984f`) and M8d (`3c68594`, S-7 editor rows) landed; SPEC v0.16 records them (D-098, D-099, D-102); the M2c-pi bundle (D-103 to D-106) and M8e (D-107) run now; v0.16 D-098, D-099, D-102.
## Dependency order and waves
@@ -33,7 +33,7 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7
| 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 | done (`34a2590`) |
| 4 | M8 App integration | `M8.md` | M3, M4, M5, M7a to M7f, M2c-dev | done (`369e516`, `00a14a3`); on-phone test run 2026-10-18; M8b fixes `f0575d3` |
| 4 | M8 App integration | `M8.md` | M3, M4, M5, M7a to M7f, M2c-dev | done (`369e516`, `00a14a3`); on-phone test run 2026-10-18; M8b fixes `f0575d3`; M8c `b481fab`, `64f5706`, `7c453d8`; M8d `3c68594`; M8e running (no prompt files; briefs by the orchestrator) |
| 4 | M12e Contract pass | none (orchestrator brief) | M8, SPEC v0.12 (§11.3 list) | done (`1470a2f`, `90505a8`) |
| 5 | M2c-pi Infra on the Pi (bundle `infra/pi/`, D-100) | `M2c-pi.md` | M2c-dev, M8; runs last, after the app is proven | none needed |
| 6 | M10 First release | `M10.md` | M2c-pi, M3, M4, M8 | none |