SPEC v0.19: D-118 Android overhaul (shared test fixtures, :core:design limits and failures); HANDOFF, prompts, hash remap in plans

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 23:21:59 +02:00
parent c58236338c
commit 3ef2c15764
12 changed files with 53 additions and 41 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# PLAN-M12a: Category contract v2, contracts # PLAN-M12a: Category contract v2, contracts
On record only: M12a ran in parallel with the SPEC v0.8 docs change, from the UX-session documents, and landed on `main` as `6b60ef7`. What landed wins where this plan differs: the suggestion records and the `UnicodeText` interface live in `:domain` (M12a also wrote `:domain`'s `suggestCategory`), the vectors include non-NFC strings, and the OpenAPI `DefaultCategoryKey` extension and the new category examples were held back for M12b (SPEC §11.2, §14.5.6). On record only: M12a ran in parallel with the SPEC v0.8 docs change, from the UX-session documents, and landed on `main` as `aa765bb`. What landed wins where this plan differs: the suggestion records and the `UnicodeText` interface live in `:domain` (M12a also wrote `:domain`'s `suggestCategory`), the vectors include non-NFC strings, and the OpenAPI `DefaultCategoryKey` extension and the new category examples were held back for M12b (SPEC §11.2, §14.5.6).
## 1. Goal ## 1. Goal
Write the contract files of the category contract v2 (D-049, D-054) so that M12b (server) and M12c (Android) can work in parallel against them: the keyword table, the locked text of server migration 0002, the `suggest_category` test vectors, the fourteen-key enum in the OpenAPI document, the examples that change, and the checker extended to verify all of it, including a stdlib-only Python reference of §9.11. Write the contract files of the category contract v2 (D-049, D-054) so that M12b (server) and M12c (Android) can work in parallel against them: the keyword table, the locked text of server migration 0002, the `suggest_category` test vectors, the fourteen-key enum in the OpenAPI document, the examples that change, and the checker extended to verify all of it, including a stdlib-only Python reference of §9.11.
+3 -3
View File
@@ -10,15 +10,15 @@ Bring the landed server and importer to the category contract v2 (D-053, D-054):
4. SPEC §8.9 (pragmas), §14.4.2 (pull: the new rows reach phones by `server_version`). 4. SPEC §8.9 (pragmas), §14.4.2 (pull: the new rows reach phones by `server_version`).
5. SPEC §11.2 (row M12b), §11.3 (note M12), §14.7 (row M12b). 5. SPEC §11.2 (row M12b), §11.3 (note M12), §14.7 (row M12b).
6. `docs/decisions.md` D-053, D-054. 6. `docs/decisions.md` D-053, D-054.
7. Files: `git show 6b60ef7` (M12a's commit message lists what it held back), `contracts/schema/migrations/0002_categories_v2.sql` and `tools/contracts-check.py` (from M12a), `server/crates/kvitt-server/migrations/0001_init.sql`, `src/db/mod.rs`, `src/api/validate.rs`, `src/wire/category.rs`, `src/wire/mod.rs`, `src/openapi.rs`, `tests/skeleton/migration.rs`, `tests/skeleton/routes.rs` (E-1 `schema_version`), `tests/openapi_contract.rs`, the tests in `tests/core_api/` and `tests/sync_api/` that count or list categories, `server/crates/kvitt-import/src/categories.rs`, `data/splitwise_categories.csv`, `tests/import.rs` (`category_table_matches_the_spec`), `contracts/examples/E-31-sync-pull/response.json`. 7. Files: `git show aa765bb` (M12a's commit message lists what it held back), `contracts/schema/migrations/0002_categories_v2.sql` and `tools/contracts-check.py` (from M12a), `server/crates/kvitt-server/migrations/0001_init.sql`, `src/db/mod.rs`, `src/api/validate.rs`, `src/wire/category.rs`, `src/wire/mod.rs`, `src/openapi.rs`, `tests/skeleton/migration.rs`, `tests/skeleton/routes.rs` (E-1 `schema_version`), `tests/openapi_contract.rs`, the tests in `tests/core_api/` and `tests/sync_api/` that count or list categories, `server/crates/kvitt-import/src/categories.rs`, `data/splitwise_categories.csv`, `tests/import.rs` (`category_table_matches_the_spec`), `contracts/examples/E-31-sync-pull/response.json`.
8. `CLAUDE.md` (server gate, no `unwrap` outside tests, every schema change gets a migration and a migration test). 8. `CLAUDE.md` (server gate, no `unwrap` outside tests, every schema change gets a migration and a migration test).
## 3. Already established ## 3. Already established
- Migration 0002 is the SQL of §14.1.1 byte for byte; `server-v1.sql` and `0001_init.sql` stay unchanged (§14.1.1). - Migration 0002 is the SQL of §14.1.1 byte for byte; `server-v1.sql` and `0001_init.sql` stay unchanged (§14.1.1).
- It rebuilds `categories` under `PRAGMA defer_foreign_keys = ON` inside sqlx's per-migration transaction (foreign keys stay ON), inserts four rows with `server_version` from `sync_sequence`, sets `meta.schema_version` to `2` (§14.1.1). M12a's checker (`6b60ef7`) loads it on a fresh and on an in-use v1 database with foreign keys on, the way sqlx does; the docs agent's own run of an equivalent text with the SQLite the server bundles (3.51.x, 3.51.3 through `libsqlite3-sys` 0.37.0) also committed with `foreign_key_check` empty. - It rebuilds `categories` under `PRAGMA defer_foreign_keys = ON` inside sqlx's per-migration transaction (foreign keys stay ON), inserts four rows with `server_version` from `sync_sequence`, sets `meta.schema_version` to `2` (§14.1.1). M12a's checker (`aa765bb`) loads it on a fresh and on an in-use v1 database with foreign keys on, the way sqlx does; the docs agent's own run of an equivalent text with the SQLite the server bundles (3.51.x, 3.51.3 through `libsqlite3-sys` 0.37.0) also committed with `foreign_key_check` empty.
- `SCHEMA_VERSION` = 2; E-1 reports it; the importer's check reads that constant, so it follows (§10.8, §14.1.1; D-046 revised by D-054). - `SCHEMA_VERSION` = 2; E-1 reports it; the importer's check reads that constant, so it follows (§10.8, §14.1.1; D-046 revised by D-054).
- Icons: the 30 of v1 plus `key`, `shield`, `account_balance`; colours: the 12 of v1 plus `slate`, `amber` (§9.7). Hex values are irrelevant to the server. - Icons: the 30 of v1 plus `key`, `shield`, `account_balance`; colours: the 12 of v1 plus `slate`, `amber` (§9.7). Hex values are irrelevant to the server.
- `DefaultCategoryKey` has the fourteen keys in both `contracts/openapi.yaml` and the generated document. M12a (`6b60ef7`) held back the contract side, so this plan edits `contracts/openapi.yaml` (that enum only) and adds the category examples M12a held back (see its commit message), then the OpenAPI match test compares the two. - `DefaultCategoryKey` has the fourteen keys in both `contracts/openapi.yaml` and the generated document. M12a (`aa765bb`) held back the contract side, so this plan edits `contracts/openapi.yaml` (that enum only) and adds the category examples M12a held back (see its commit message), then the OpenAPI match test compares the two.
- Importer table per §10.8 (D-053): Rent/Mortgage/Hyra/Avbetalning/Amortering `housing`; Insurance/Försäkringar `insurance`; Childcare/Barnomsorg `children`; Taxes/Skatter `fees`; Swedish El, Värme/gas, Vatten, Sophämtning (`utilities`) and Husdjur (`home`) added. `category_table_matches_the_spec` parses SPEC §10.8, so it fails on `main` from SPEC v0.8 until this plan lands (the orchestrator lands them close together). - Importer table per §10.8 (D-053): Rent/Mortgage/Hyra/Avbetalning/Amortering `housing`; Insurance/Försäkringar `insurance`; Childcare/Barnomsorg `children`; Taxes/Skatter `fees`; Swedish El, Värme/gas, Vatten, Sophämtning (`utilities`) and Husdjur (`home`) added. `category_table_matches_the_spec` parses SPEC §10.8, so it fails on `main` from SPEC v0.8 until this plan lands (the orchestrator lands them close together).
- The E-31 example lists two categories, not the defaults; change it only if it enumerates the defaults. - The E-31 example lists two categories, not the defaults; change it only if it enumerates the defaults.
+5 -5
View File
@@ -1,7 +1,7 @@
# PLAN-M12c: Category contract v2, Android # PLAN-M12c: Category contract v2, Android
## 1. Goal ## 1. Goal
Bring the landed Android contract and data layer to the category contract v2 (D-049, D-054): fourteen default categories, two more colour tokens and three more icons in `:core:model`; `ExpenseRepository.suggestCategory` in `:data`, calling the pure `suggestCategory` that M12a already landed in `:domain` (`6b60ef7`) with a platform `UnicodeText` binding and the keyword table bundled; fakes with the fourteen defaults. After this lands, M6 and M7c build on it. Bring the landed Android contract and data layer to the category contract v2 (D-049, D-054): fourteen default categories, two more colour tokens and three more icons in `:core:model`; `ExpenseRepository.suggestCategory` in `:data`, calling the pure `suggestCategory` that M12a already landed in `:domain` (`aa765bb`) with a platform `UnicodeText` binding and the keyword table bundled; fakes with the fourteen defaults. After this lands, M6 and M7c build on it.
## 2. Read first ## 2. Read first
1. SPEC §9.11 Category suggestion (CS-1 to CS-7), all of it. 1. SPEC §9.11 Category suggestion (CS-1 to CS-7), all of it.
@@ -16,13 +16,13 @@ Bring the landed Android contract and data layer to the category contract v2 (D-
## 3. Already established ## 3. Already established
- `DefaultCategoryKey` in AC-7.2 order (14), `ColorToken` + `SLATE`, `AMBER`, `CategoryIcons.ALL` + `key`, `shield`, `account_balance` at the end (33) (§14.2.4). Wire value = lowercase name. - `DefaultCategoryKey` in AC-7.2 order (14), `ColorToken` + `SLATE`, `AMBER`, `CategoryIcons.ALL` + `key`, `shield`, `account_balance` at the end (33) (§14.2.4). Wire value = lowercase name.
- New default ids `01920000-0000-7000-8000-000000000011` to `…014` (housing, insurance, children, fees), sort orders 25, 45, 85, 95 (AC-7.2, §14.1.1). - New default ids `01920000-0000-7000-8000-000000000011` to `…014` (housing, insurance, children, fees), sort orders 25, 45, 85, 95 (AC-7.2, §14.1.1).
- Landed in `:domain` by M12a (`6b60ef7`), not to be rewritten: `SuggestionHistoryEntry`, `KeywordMatch`, `CategoryKeyword`, `ActiveCategory`, `UnicodeText`, `normalizeDescription`, `suggestCategory(description, history, keywords, activeCategories, fallback, unicode): CategoryId?`, passing `suggest_category.json` (§14.5.6). - Landed in `:domain` by M12a (`aa765bb`), not to be rewritten: `SuggestionHistoryEntry`, `KeywordMatch`, `CategoryKeyword`, `ActiveCategory`, `UnicodeText`, `normalizeDescription`, `suggestCategory(description, history, keywords, activeCategories, fallback, unicode): CategoryId?`, passing `suggest_category.json` (§14.5.6).
- `:domain` may not use `java.*`, so `:data` provides the production `UnicodeText` (`java.text.Normalizer` NFC, `Character.getType` for L and Nd), the same behaviour as the `:domain` test binding (§14.5.6). - `:domain` may not use `java.*`, so `:data` provides the production `UnicodeText` (`java.text.Normalizer` NFC, `Character.getType` for L and Nd), the same behaviour as the `:domain` test binding (§14.5.6).
- `ExpenseRepository.suggestCategory(accountId: AccountId?, description: String): CategoryId?`: history = every non-deleted expense in Room (pending included); active categories = non-archived, non-deleted; fallback = AC-8.6 (last used in that account on this phone if active, else `other`; `accountId` null gives `other`); null only while the phone holds no categories (§14.2.4). - `ExpenseRepository.suggestCategory(accountId: AccountId?, description: String): CategoryId?`: history = every non-deleted expense in Room (pending included); active categories = non-archived, non-deleted; fallback = AC-8.6 (last used in that account on this phone if active, else `other`; `accountId` null gives `other`); null only while the phone holds no categories (§14.2.4).
- The keyword table is bundled as a Java resource, byte-identical to `contracts/category-keywords.csv`, checked by a test (as landed in `e499107`, D-055: copied at build time by `:data:copyCategoryKeywords` and `:core:fakes:copyCategoryKeywords`, no committed copy; parsed by `:domain`'s `parseCategoryKeywords`, D-056); parsed once and cached (§14.2.4, §14.8). - The keyword table is bundled as a Java resource, byte-identical to `contracts/category-keywords.csv`, checked by a test (as landed in `b09f322`, D-055: copied at build time by `:data:copyCategoryKeywords` and `:core:fakes:copyCategoryKeywords`, no committed copy; parsed by `:domain`'s `parseCategoryKeywords`, D-056); parsed once and cached (§14.2.4, §14.8).
- The Room schema stays at version 1; the new default rows arrive through sync (§14.1.1). No Room migration. - The Room schema stays at version 1; the new default rows arrive through sync (§14.1.1). No Room migration.
- Fakes: the empty scenario has exactly the fourteen defaults (§6.2); the fake `suggestCategory` follows §9.11 over the fake's own data by calling the same `:domain` function (`:core:fakes` may depend on `:domain`, §6.2). - Fakes: the empty scenario has exactly the fourteen defaults (§6.2); the fake `suggestCategory` follows §9.11 over the fake's own data by calling the same `:domain` function (`:core:fakes` may depend on `:domain`, §6.2).
- Default category names (AC-7.2) are shown from string resources. At `e8ee518` no module holds default category name strings; `:core:design` will hold them (M6, from `docs/ux/strings.csv`). If, when this plan runs, a module already holds the ten default names as string resources, add the four new names there in English and Swedish (`values/strings.xml`, `values-sv/strings.xml`); otherwise add none and say so in the report (M6 then takes all fourteen from `strings.csv`). - Default category names (AC-7.2) are shown from string resources. At `86e08a1` no module holds default category name strings; `:core:design` will hold them (M6, from `docs/ux/strings.csv`). If, when this plan runs, a module already holds the ten default names as string resources, add the four new names there in English and Swedish (`values/strings.xml`, `values-sv/strings.xml`); otherwise add none and say so in the report (M6 then takes all fourteen from `strings.csv`).
## 4. Owned paths ## 4. Owned paths
From SPEC §14.7 (row M12c, a contract-change plan): `android/core/model/**` (enums, icons, `ExpenseRepository`), `android/data/**` (`UnicodeText` binding, `suggestCategory`, the bundled keyword resource, tests), `android/core/fakes/**` (defaults, fake `suggestCategory`); plus the default-name string files of §3 last item, only if they already exist. From SPEC §14.7 (row M12c, a contract-change plan): `android/core/model/**` (enums, icons, `ExpenseRepository`), `android/data/**` (`UnicodeText` binding, `suggestCategory`, the bundled keyword resource, tests), `android/core/fakes/**` (defaults, fake `suggestCategory`); plus the default-name string files of §3 last item, only if they already exist.
@@ -37,7 +37,7 @@ Edit nothing outside these paths; a needed change elsewhere is a contract reques
## 6. Steps ## 6. Steps
1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m12c -b agent-m12c main`, then in `.claude/worktrees/agent-m12c`: `git reset --hard main`. Copy `android/local.properties` from the main checkout (add `sdk.dir=/opt/homebrew/share/android-commandlinetools` if missing; never overwrite or commit). Run `tools/install-git-hooks.sh`. 1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m12c -b agent-m12c main`, then in `.claude/worktrees/agent-m12c`: `git reset --hard main`. Copy `android/local.properties` from the main checkout (add `sdk.dir=/opt/homebrew/share/android-commandlinetools` if missing; never overwrite or commit). Run `tools/install-git-hooks.sh`.
2. Read §2 in order. Confirm M12a has landed (`6b60ef7`: `contracts/category-keywords.csv` and `:domain`'s `CategorySuggestion.kt` on `main`); if not, stop and report. 2. Read §2 in order. Confirm M12a has landed (`aa765bb`: `contracts/category-keywords.csv` and `:domain`'s `CategorySuggestion.kt` on `main`); if not, stop and report.
3. Run `python3 tools/dependency-report/latest.py`; record the lines. No new library is expected. 3. Run `python3 tools/dependency-report/latest.py`; record the lines. No new library is expected.
4. `:core:model` changes and `ContractTest`; build. 4. `:core:model` changes and `ContractTest`; build.
5. `:domain`: nothing to write; confirm the `suggest_category` vector test passes in the gate. 5. `:domain`: nothing to write; confirm the `suggest_category` vector test passes in the gate.
+1 -1
View File
@@ -1,6 +1,6 @@
# PLAN-M2a: Server skeleton and `kvitt-domain` # PLAN-M2a: Server skeleton and `kvitt-domain`
On record: landed on `main` as `fdf2093`. As built (SPEC v0.4, D-005 to D-007): Rust 1.99.1 pinned in `rust-toolchain.toml` and the Dockerfile; Dockerfile builds arm64 (shipped) and amd64 via `TARGETARCH`; the OpenAPI equality test's eight normalisations and the `CommonRules` modifier (§14.8); explicit serde rename for `bp_sum_not_10000`; per-crate `deny(clippy::unwrap_used, clippy::expect_used)`; seams as documented in `server/README.md` and §6.3. Where this plan and SPEC differ, SPEC wins. On record: landed on `main` as `06f48ef`. As built (SPEC v0.4, D-005 to D-007): Rust 1.99.1 pinned in `rust-toolchain.toml` and the Dockerfile; Dockerfile builds arm64 (shipped) and amd64 via `TARGETARCH`; the OpenAPI equality test's eight normalisations and the `CommonRules` modifier (§14.8); explicit serde rename for `bp_sum_not_10000`; per-crate `deny(clippy::unwrap_used, clippy::expect_used)`; seams as documented in `server/README.md` and §6.3. Where this plan and SPEC differ, SPEC wins.
## 1. Goal ## 1. Goal
Create the Rust Cargo workspace with its three crates, a running `kvitt-server` with config, database pool, schema migration, error type, auth extractor, every route of §7 registered (501 until implemented), `GET /health` and `GET /setup/status`, and a utoipa document equal to `contracts/openapi.yaml`. Implement `kvitt-domain` so that it passes every shared test vector, so M3, M4 and M9 can fill in their own files in parallel. Create the Rust Cargo workspace with its three crates, a running `kvitt-server` with config, database pool, schema migration, error type, auth extractor, every route of §7 registered (501 until implemented), `GET /health` and `GET /setup/status`, and a utoipa document equal to `contracts/openapi.yaml`. Implement `kvitt-domain` so that it passes every shared test vector, so M3, M4 and M9 can fill in their own files in parallel.
+1 -1
View File
@@ -1,6 +1,6 @@
# PLAN-M2b: Android skeleton and `:domain` # PLAN-M2b: Android skeleton and `:domain`
On record: landed on `main` as `fad6fc9`. As built (SPEC v0.4, D-010 to D-014): Roborazzi 1.76.0 on Robolectric 4.17 for screenshots; `IncompleteCustomRangeException`; Room 3 with the entities, `KvittDatabase` v1 and the exported `1.json` in `:data`; JDK 25 test flags and `failOnNoDiscoveredTests` in `build-logic`; `minSdk` 36, `compileSdk`/`targetSdk` 37; the feature convention in `android/README.md`. `installDebug` on the phone is still outstanding (owner). Where this plan and SPEC differ, SPEC wins. On record: landed on `main` as `c277ee3`. As built (SPEC v0.4, D-010 to D-014): Roborazzi 1.76.0 on Robolectric 4.17 for screenshots; `IncompleteCustomRangeException`; Room 3 with the entities, `KvittDatabase` v1 and the exported `1.json` in `:data`; JDK 25 test flags and `failOnNoDiscoveredTests` in `build-logic`; `minSdk` 36, `compileSdk`/`targetSdk` 37; the feature convention in `android/README.md`. `installDebug` on the phone is still outstanding (owner). Where this plan and SPEC differ, SPEC wins.
## 1. Goal ## 1. Goal
Create the Gradle project in `android/` with every module of §6.2, the version catalog, the architecture check, the `:core:model` UI contract and domain types, a `:domain` that passes every shared test vector including analytics, empty fakes, and an `:app` that launches to an empty themed screen. After it lands, M5 (data), M6 (design) and M7a to M7f (features) build against the same interfaces in parallel. Create the Gradle project in `android/` with every module of §6.2, the version catalog, the architecture check, the `:core:model` UI contract and domain types, a `:domain` that passes every shared test vector including analytics, empty fakes, and an `:app` that launches to an empty themed screen. After it lands, M5 (data), M6 (design) and M7a to M7f (features) build against the same interfaces in parallel.
+2 -2
View File
@@ -29,8 +29,8 @@ Assemble the app: `:app` navigation and deep links, Hilt bindings from the `:cor
- Version properties `kvitt.versionCode`, `kvitt.versionName` (debug `1` and `dev`); release signing from `android/local.properties` keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias`, `kvitt.release.keyPassword` (§10.6). - Version properties `kvitt.versionCode`, `kvitt.versionName` (debug `1` and `dev`); release signing from `android/local.properties` keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias`, `kvitt.release.keyPassword` (§10.6).
- During development the debug build pairs against the server on the Mac over the home Wi-Fi (D-001); development data on that server is disposable. - During development the debug build pairs against the server on the Mac over the home Wi-Fi (D-001); development data on that server is disposable.
- Application ids `se.hagstedt.kvitt.debug` (debug), `se.hagstedt.kvitt` (release) (CLAUDE.md). - Application ids `se.hagstedt.kvitt.debug` (debug), `se.hagstedt.kvitt` (release) (CLAUDE.md).
- M5's contract requests, landed with SPEC v0.5 (§11.3 note M8, D-041): `:app` provides `AppBuildInfo(versionCode, versionName, debug)` to `:data` and includes `DataBindingsModule` (`@DisableInstallInCheck`) in the release graph (done pre-M8 in `e6ad858`, with `AppBuildInfo` from `BuildConfig`); debug binds the fakes through the temporary `android/app/src/debug/.../DebugFakesModule.kt` (`72f21b2`), which M8 deletes so both variants bind `:data`; WorkManager is configured with `KvittWorkerFactory` and the default `WorkManagerInitializer` disabled in the manifest; `:app` calls `SyncTriggers.onAppStart`, `SyncTriggers.onForeground` and `SyncTriggers.syncForPush` (push handling, 20 s wait), and `UpdateRepository.check` on cold start and every resume; the `ImageLoader` provided by `:data` is set as Coil's singleton. - M5's contract requests, landed with SPEC v0.5 (§11.3 note M8, D-041): `:app` provides `AppBuildInfo(versionCode, versionName, debug)` to `:data` and includes `DataBindingsModule` (`@DisableInstallInCheck`) in the release graph (done pre-M8 in `6453932`, with `AppBuildInfo` from `BuildConfig`); debug binds the fakes through the temporary `android/app/src/debug/.../DebugFakesModule.kt` (`a41f240`), which M8 deletes so both variants bind `:data`; WorkManager is configured with `KvittWorkerFactory` and the default `WorkManagerInitializer` disabled in the manifest; `:app` calls `SyncTriggers.onAppStart`, `SyncTriggers.onForeground` and `SyncTriggers.syncForPush` (push handling, 20 s wait), and `UpdateRepository.check` on cold start and every resume; the `ImageLoader` provided by `:data` is set as Coil's singleton.
- Open follow-up (D-027): on the first sync attempt after a `clock_skew` rejection the phone re-stamps `updated_at` of each such pending row to the current clock before pushing. Still not in `:data` at `3a1bd14`; since SPEC v0.11 it is on the M12e contract-pass list (§11.3), not M8's. - Open follow-up (D-027): on the first sync attempt after a `clock_skew` rejection the phone re-stamps `updated_at` of each such pending row to the current clock before pushing. Still not in `:data` at `b24c8b5`; since SPEC v0.11 it is on the M12e contract-pass list (§11.3), not M8's.
## 4. Owned paths ## 4. Owned paths
From SPEC §14.7, verbatim: `android/adapter/fcm/**`, `android/app/**` (after the M2b shell) (owner M8; after landing: "later integration plans"). Catalog entries and module includes may be added (§14.7, `android/gradle/**` row). From SPEC §14.7, verbatim: `android/adapter/fcm/**`, `android/app/**` (after the M2b shell) (owner M8; after landing: "later integration plans"). Catalog entries and module includes may be added (§14.7, `android/gradle/**` row).
+8 -3
View File
@@ -1,6 +1,6 @@
# Kvitt: specification # Kvitt: specification
Version **0.18** (2026-10-18). Schema version **3**. Sync protocol version **1**. Push payload version **1**. Version **0.19** (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. 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.
@@ -421,7 +421,7 @@ Module graph (directory `android/`):
|---|---|---|---| |---|---|---|---|
| `:core:model` | pure Kotlin/JVM | Domain types (§14.5.1), analytics types (§14.5.4, package `se.hagstedt.kvitt.model.analytics`), `*State` classes, repository interfaces (§14.2.4) | kotlin, kotlinx | | `:core:model` | pure Kotlin/JVM | Domain types (§14.5.1), analytics types (§14.5.4, package `se.hagstedt.kvitt.model.analytics`), `*State` classes, repository interfaces (§14.2.4) | kotlin, kotlinx |
| `:domain` | pure Kotlin/JVM | Split, balance, allocation, analytics functions (§14.5); test-vector tests | `:core:model` | | `:domain` | pure Kotlin/JVM | Split, balance, allocation, analytics functions (§14.5); test-vector tests | `:core:model` |
| `:core:design` | Android library | Built by M6 (`7730a31`) 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:design` | Android library | Built by M6 (`7730a31`) from `docs/ux/design-spec.md` §2 and §3: `KvittTheme`, tokens (`KvittColors`, `KvittShapes`, `KvittMotion`, which holds only the seesaw spring; navigation has no motion, D-108), 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`, the shared failure texts (`format/Failures.kt`: `FAILURE_NETWORK`, `commonFailureText`), the shared limits (`format/Limits.kt`: `BP_TOTAL`, `DISPLAY_NAME_MAX`, `DESCRIPTION_MAX`, `NOTES_MAX`, `FIRST_DATE`, `DAYS_AHEAD_MAX`, §9.6) and `savedSplitLabel` (S-8, S-16), shared strings (D-118). Its test fixtures (`src/testFixtures`, package `se.hagstedt.kvitt.design.testing`) are the shared test support of every Android module (D-118); they are not a module | `: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` | | `: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` |
| `:data` | Android library | Room DB, DAOs, entities, Ktor API client and DTOs, sync engine, WorkManager workers, token store, repository implementations, APK updater | `:core:model`, `:domain` | | `:data` | Android library | Room DB, DAOs, entities, Ktor API client and DTOs, sync engine, WorkManager workers, token store, repository implementations, APK updater | `:core:model`, `:domain` |
| `:adapter:fcm` | Android library | `FirebaseMessagingService`, registration by Firebase Installation ID (D-083), maps messages to `PushEvent` (`:core:model`, `model/Push.kt`, moved there by M12e, D-095) through `PushMapping`; `PushListener` stays in the adapter | `:core:model` | | `:adapter:fcm` | Android library | `FirebaseMessagingService`, registration by Firebase Installation ID (D-083), maps messages to `PushEvent` (`:core:model`, `model/Push.kt`, moved there by M12e, D-095) through `PushMapping`; `PushListener` stays in the adapter | `:core:model` |
@@ -434,7 +434,7 @@ Module graph (directory `android/`):
| `:feature:settings` | Android library | Settings hub, profile, categories, recurring templates, devices, invites, about | `:core:model`, `:core:design` | | `: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 | | `: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-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. A screen harness renders one more frame and idles the main looper before each capture; without that `:feature:home` and `:feature:account` captured the previous variant, so their committed `sv_dark` (and some `_sv_light_200` and dialog) goldens were one variant stale, fixed and re-recorded in `dc7a296` and `5b2ed37`. 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. **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. A screen harness renders one more frame and idles the main looper before each capture; without that `:feature:home` and `:feature:account` captured the previous variant, so their committed `sv_dark` (and some `_sv_light_200` and dialog) goldens were one variant stale, fixed and re-recorded in `dc7a296` and `5b2ed37`. 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. Since the Android overhaul (`c582363`, D-118) the harness is shared: `:core:design`'s test fixtures (`src/testFixtures`, package `se.hagstedt.kvitt.design.testing`) hold `Variant`, `TestClock`, `TestEnvironment`, `ScreenSnapshots` with the capture modes `Root`, `Screen` and `ScreenWhenIdle`, `MainDispatcherRule` and `withMain`, consumed as `testImplementation(testFixtures(project(":core:design")))`. Test fixtures are not a module, so the module graph below is unchanged; the architecture check rejects a module's test fixtures in any configuration other than a test configuration. The convention plugin `kvitt.android.screenshots` wires Roborazzi (plugin, `outputDir` `src/test/screenshots`) and the test libraries; feature build scripts carry no screenshot block.
**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 (`a41f240`) 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. **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 (`a41f240`) 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.
@@ -1206,6 +1206,9 @@ history rewrite and first push (18c527f) ─► infra overhaul (2fac1ec..e70fe0d
security review (18c527f) ─┬─► infra security (landed 7fb86f5, D-117) security review (18c527f) ─┬─► infra security (landed 7fb86f5, D-117)
AC-coverage audit (956908a) ─┼─► M12f Android follow-ups (landed dc7a296..a0259ab, D-109 to D-112, D-116) AC-coverage audit (956908a) ─┼─► M12f Android follow-ups (landed dc7a296..a0259ab, D-109 to D-112, D-116)
└─► server overhaul (landed 8103dae..b985ce9; D-114, D-115) ─► router forward ─► M10 └─► server overhaul (landed 8103dae..b985ce9; D-114, D-115) ─► router forward ─► M10
After v0.18 (v0.19):
M12f ─► Android overhaul (landed e4e6e5d..c582363, D-118) ─► push to origin (c582363) ─► owner runs the M2c-pi bundle ─► M10
``` ```
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: `aa765bb`, `6eaccec`, `b09f322`). M12d (D-059) ran in parallel with M6 (landed `557af7e` and `7730a31`); M7a to M7f ran in parallel and landed on 2026-10-18 (`1ea752c`, `0e1d99d`, `0b140aa`, `7380e50`, `ea7d494`, `c4ab35f`), followed by M6b, the `:core:design` fix pass (`b24c8b5`). 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 (`3dda414`; `cb832a7` and `3d9eea8`). The contract pass M12e (D-086 to D-095) ran after them and landed on 2026-10-18 (`8c60715` contract pass, `b7b7483` launcher icon). The M2c-pi bundle (`2476667`, D-103 to D-106), M8e (`a59abeb`, D-107) and M8f (`956908a`, D-108) landed on 2026-10-18; what remains of M2c-pi is the owner running the bundle, and M10 waits for it. 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: `aa765bb`, `6eaccec`, `b09f322`). M12d (D-059) ran in parallel with M6 (landed `557af7e` and `7730a31`); M7a to M7f ran in parallel and landed on 2026-10-18 (`1ea752c`, `0e1d99d`, `0b140aa`, `7380e50`, `ea7d494`, `c4ab35f`), followed by M6b, the `:core:design` fix pass (`b24c8b5`). 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 (`3dda414`; `cb832a7` and `3d9eea8`). The contract pass M12e (D-086 to D-095) ran after them and landed on 2026-10-18 (`8c60715` contract pass, `b7b7483` launcher icon). The M2c-pi bundle (`2476667`, D-103 to D-106), M8e (`a59abeb`, D-107) and M8f (`956908a`, D-108) landed on 2026-10-18; what remains of M2c-pi is the owner running the bundle, and M10 waits for it.
@@ -1271,6 +1274,7 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
* **Server gap pass** (landed `8660b7f`, D-113): `kvitt-server invite-partner` (§10.4), which closes the ops request of the M8 test; tests for `invite-partner`, `migrate` (schema at `SCHEMA_VERSION`, idempotent), `healthcheck` (200, 503, nothing listening) and the SIGTERM drain of `serve`; the push sender's `push sent` info line (§8.7); `tools/contracts-check.py` follows `invite_kind` (D-086), `app_version_name` (D-090) and `fid` (D-096), 134 checks pass. * **Server gap pass** (landed `8660b7f`, D-113): `kvitt-server invite-partner` (§10.4), which closes the ops request of the M8 test; tests for `invite-partner`, `migrate` (schema at `SCHEMA_VERSION`, idempotent), `healthcheck` (200, 503, nothing listening) and the SIGTERM drain of `serve`; the push sender's `push sent` info line (§8.7); `tools/contracts-check.py` follows `invite_kind` (D-086), `app_version_name` (D-090) and `fid` (D-096), 134 checks pass.
* **Server overhaul** (landed `8103dae` to `b985ce9`, D-114, D-115): shared stored-row reads and inserts (`src/api/rows.rs`, `src/sync/rows.rs`), shared invite creation and CLI plumbing, one test harness (`tests/common/`, §14.7), clippy pedantic wins, synchronous `tasks::start` (§6.3), `not_implemented` no longer produced (§14.3.2); the security fixes H-1 (D-115), M-3 and L-4 (D-114). Gate at landing: 268 server tests; all crates at their latest versions. * **Server overhaul** (landed `8103dae` to `b985ce9`, D-114, D-115): shared stored-row reads and inserts (`src/api/rows.rs`, `src/sync/rows.rs`), shared invite creation and CLI plumbing, one test harness (`tests/common/`, §14.7), clippy pedantic wins, synchronous `tasks::start` (§6.3), `not_implemented` no longer produced (§14.3.2); the security fixes H-1 (D-115), M-3 and L-4 (D-114). Gate at landing: 268 server tests; all crates at their latest versions.
* **Infra overhaul** (landed `2fac1ec` to `e70fe0d`, no decision): one Gradle lock helper `tools/gradle-lock.sh` for `tools/gradle-serial.sh`, `tools/clean-builds.sh` and `infra/build-images.sh`; the pre-commit hook runs the Android tests through `tools/gradle-serial.sh` with JDK 25 on `PATH`, `JAVA_HOME` untouched; `install.sh` keeps the deployed `KVITT_TAG` (§10.3); `--provenance=false` on buildx pushes and the workflow token through the environment (§10.3); `RCLONE_REMOTE` dropped from the development env (§10 intro). **Infra security** (landed `7fb86f5`, D-117): Caddy log redaction and response headers (§8.7, §10.5.2), the non-root Caddy container (§8.9, §10.2), the development stack's single secret (§10 intro), the digest record (§10.3, §10.10), `kvitt_cicd` with `write:package` only (§10.3), the restricted cert-sync key (§10.5.1). * **Infra overhaul** (landed `2fac1ec` to `e70fe0d`, no decision): one Gradle lock helper `tools/gradle-lock.sh` for `tools/gradle-serial.sh`, `tools/clean-builds.sh` and `infra/build-images.sh`; the pre-commit hook runs the Android tests through `tools/gradle-serial.sh` with JDK 25 on `PATH`, `JAVA_HOME` untouched; `install.sh` keeps the deployed `KVITT_TAG` (§10.3); `--provenance=false` on buildx pushes and the workflow token through the environment (§10.3); `RCLONE_REMOTE` dropped from the development env (§10 intro). **Infra security** (landed `7fb86f5`, D-117): Caddy log redaction and response headers (§8.7, §10.5.2), the non-root Caddy container (§8.9, §10.2), the development stack's single secret (§10 intro), the digest record (§10.3, §10.10), `kvitt_cicd` with `write:package` only (§10.3), the restricted cert-sync key (§10.5.1).
* **Overhaul outcomes** (2026-10-18). Server: landed through `b985ce9` (see the server overhaul above). Infra: landed through `e70fe0d` (see the infra overhaul above). **Android overhaul** (landed `e4e6e5d` to `c582363`, D-118, no prompt file, orchestrator brief): one screenshot and test harness in `:core:design`'s test fixtures and the convention plugin `kvitt.android.screenshots` (§6.2); the shared failure texts, limits and `savedSplitLabel` in `:core:design` (§6.2); dead code removed (`KvittMotion` down to the seesaw spring, `KvittIcon.Description`, unused dimens and shapes, catalogue and fake samples); the catalog entries `roborazzi-junit-rule`, `roborazzi-compose`, `androidx-hilt-navigation-compose` and `androidx-lifecycle-viewmodel-compose` removed; one `CopyCategoryKeywords` task in `build-logic` shared by `:data` and `:core:fakes` (D-055 holds); one `configureAndroidModule` for the application and library plugins; S-7's state types in `ExpenseEditorState.kt`. Gate at landing: 941 tests, no golden changed; release APK 10.46 MB. Pushed to `origin` at `c582363`.
* **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. * **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.
### 11.4 Validation reports ### 11.4 Validation reports
@@ -2827,3 +2831,4 @@ Rule: a change to any other contract file, or a semantic change to `contracts/op
| 0.16 | 2026-10-18 | M8c (`75b549f`, `a0c25b0`, `7c453d8`), the R8 release build (`c30a49e`) and M8d (`bacbd18`). 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. | | 0.16 | 2026-10-18 | M8c (`75b549f`, `a0c25b0`, `7c453d8`), the R8 release build (`c30a49e`) and M8d (`bacbd18`). 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. |
| 0.17 | 2026-10-18 | The M2c-pi bundle (`2476667`), M8e (`a59abeb`), M8f (`956908a`), the chips cleanup (`1328afe`) and the Dockerfile `CARGO_BUILD_JOBS` argument (`b4d9467`). D-103: the `infra/pi/` bundle (`compose.yaml`, was `infra/compose.yaml`; `.env.example`, was `infra/env.example`; `Caddyfile`; `install.sh`, `deploy.sh <tag> [--skip-backup]` with a backup before every deploy and `/opt/kvitt/previous-tag`, `bootstrap.sh`, `backup-now.sh`, `install-apk.sh`), the APK path `infra/publish-apk.sh` → `infra/pi/out/apk/` → `/opt/kvitt/apk/` → `install-apk.sh` into the `data` volume, mappings outside the volume, `.env` keys `KVITT_REGISTRY_USER`, `KVITT_REGISTRY_TOKEN`, `KVITT_PI_HOST`, `KVITT_PI_SSH_USER`, gate `docker compose -f infra/pi/compose.yaml config`; fulfils D-100 (§10 intro, §10.2, §10.3, §10.6, §10.7, §10.10, §11.2, §14.7). D-104: images `${KVITT_REGISTRY}/kvitt-server|kvitt-cert-sync|kvitt-backup` with `KVITT_REGISTRY=gitea.lan/jacobh2`, the Gitea workflow `.gitea/workflows/build-images.yaml` (runner `host`, secret `kvitt_cicd`, `v1.0.N` plus `latest`, git tag pushed, `CARGO_BUILD_JOBS=1`), `infra/build-images.sh` as the Mac fallback, supercronic instead of busybox crond, `openssl` in cert-sync, pinned versions and Docker Engine from Docker's Debian arm64 repository (§10.1, §10.2, §10.3, §12 OQ-1). D-105: `backup` runs as uid 10001 on the Pi, `rclone.conf` 0600 owned 10001:10001, a temporary copy for refreshed tokens, volume owners set once by `install.sh`; revises D-023 for `rclone.conf` (§10.2). D-106: cert-sync always requests the `tar` stream (both `authorized_keys` variants), refuses a mismatched key or an earlier-expiring certificate, an unreachable admin API at first start is no failure; backup uploads every pending file, confirms each by name and size, deletes and writes `last-success` only after all are confirmed (§10.5.1, §10.9). D-107: S-19's account, payer and split as three fixed `EditorOptionRows` (Konto, Betalt av, Delning), extending D-102 (§13.3, design-spec §3.7, S-19). D-108: navigation is instant, no enter, exit, pop or size transition on the app's `NavHost` (§13.2, design-spec §2.11). Without a decision id: the Drive token, the cert-sync key and the Gitea secret `kvitt_cicd` exist; `server/Dockerfile` honours `CARGO_BUILD_JOBS` (§10.3); the unused `AccountChip`, `PayerChip`, `SplitChip` removed (design-spec §3.7); Caddy's `Authorization` redaction verified (§10.5.2); APKs about 11 MB (§10.10); §11.1 graph. | | 0.17 | 2026-10-18 | The M2c-pi bundle (`2476667`), M8e (`a59abeb`), M8f (`956908a`), the chips cleanup (`1328afe`) and the Dockerfile `CARGO_BUILD_JOBS` argument (`b4d9467`). D-103: the `infra/pi/` bundle (`compose.yaml`, was `infra/compose.yaml`; `.env.example`, was `infra/env.example`; `Caddyfile`; `install.sh`, `deploy.sh <tag> [--skip-backup]` with a backup before every deploy and `/opt/kvitt/previous-tag`, `bootstrap.sh`, `backup-now.sh`, `install-apk.sh`), the APK path `infra/publish-apk.sh` → `infra/pi/out/apk/` → `/opt/kvitt/apk/` → `install-apk.sh` into the `data` volume, mappings outside the volume, `.env` keys `KVITT_REGISTRY_USER`, `KVITT_REGISTRY_TOKEN`, `KVITT_PI_HOST`, `KVITT_PI_SSH_USER`, gate `docker compose -f infra/pi/compose.yaml config`; fulfils D-100 (§10 intro, §10.2, §10.3, §10.6, §10.7, §10.10, §11.2, §14.7). D-104: images `${KVITT_REGISTRY}/kvitt-server|kvitt-cert-sync|kvitt-backup` with `KVITT_REGISTRY=gitea.lan/jacobh2`, the Gitea workflow `.gitea/workflows/build-images.yaml` (runner `host`, secret `kvitt_cicd`, `v1.0.N` plus `latest`, git tag pushed, `CARGO_BUILD_JOBS=1`), `infra/build-images.sh` as the Mac fallback, supercronic instead of busybox crond, `openssl` in cert-sync, pinned versions and Docker Engine from Docker's Debian arm64 repository (§10.1, §10.2, §10.3, §12 OQ-1). D-105: `backup` runs as uid 10001 on the Pi, `rclone.conf` 0600 owned 10001:10001, a temporary copy for refreshed tokens, volume owners set once by `install.sh`; revises D-023 for `rclone.conf` (§10.2). D-106: cert-sync always requests the `tar` stream (both `authorized_keys` variants), refuses a mismatched key or an earlier-expiring certificate, an unreachable admin API at first start is no failure; backup uploads every pending file, confirms each by name and size, deletes and writes `last-success` only after all are confirmed (§10.5.1, §10.9). D-107: S-19's account, payer and split as three fixed `EditorOptionRows` (Konto, Betalt av, Delning), extending D-102 (§13.3, design-spec §3.7, S-19). D-108: navigation is instant, no enter, exit, pop or size transition on the app's `NavHost` (§13.2, design-spec §2.11). Without a decision id: the Drive token, the cert-sync key and the Gitea secret `kvitt_cicd` exist; `server/Dockerfile` honours `CARGO_BUILD_JOBS` (§10.3); the unused `AccountChip`, `PayerChip`, `SplitChip` removed (design-spec §3.7); Caddy's `Authorization` redaction verified (§10.5.2); APKs about 11 MB (§10.10); §11.1 graph. |
| 0.18 | 2026-10-18 | M12f (`dc7a296`, `5b2ed37`, `2bd6b58`, `c366fbd`, `a0259ab`), the server gap pass (`8660b7f`), the server overhaul (`8103dae` to `b985ce9`), the infra overhaul (`2fac1ec` to `e70fe0d`) and infra security fixes (`7fb86f5`), the AC-coverage audit and the security review. D-109: S-16 shows a rejected change's lost version read-only, `Rejection.lost: RejectedVersion?` with `RejectedVersion(row, accountName, payer, mode, shares, notes, deleted)` from `rejected_payload`, null when undecodable or a user or the category is missing; dialog "Din version", "Du tog bort den här", "Visa nuvarande version"; ledger rows take a null `onClick` (read-only); four `home_sync_lost_*` strings (§13.3 S-16, §14.2.4, §14.4.6, design-spec S-16). D-110: `AboutState.schemaVersion`; S-22 reads `/health` when it opens online and shows "<version> (schema <n>)" (`settings_about_server_version_schema`), header fallback offline (§13.3 S-22, §14.2.4, E-1, design-spec S-22). D-111: `SettlementRepository.start` and `recompute` return `SettleState?` (null without a session or partner), `save` returns `Invalid(payer, validation_failed)`; no throws (§14.2.4, AC-5.1). D-112: a failed E-17 send is retried after each successful sync until accepted, a newer FID replaces it (AC-13.7). D-113: `kvitt-server invite-partner` (0 ok, 2 household full, 3 no admin) and the `push sent` info line (§8.7, §10.4); `tools/contracts-check.py` 134 checks. D-116: notifications `VISIBILITY_PRIVATE` with the generic public version (new AC-13.9); L-6 kept as built, S-1's server row expanded for a non-default server, the confirm step listed as OQ-6 (AC-2.6, AC-2.8, §12). D-117: Caddy log redaction of `/i/<code>` and query strings, `Content-Security-Policy`, `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` (§8.7, §10.5.2, which now quotes `infra/pi/Caddyfile` at `7fb86f5`); Caddy as uid 10002 with group 1000, read-only, `cap_drop ALL` plus `NET_BIND_SERVICE`, tmpfs; `/certs` 0750 1000:1000, `caddy_data` 10002, extending D-105 (§8.9, §10.2); the development stack copies only `fcm-service-account.json` into `infra/dev/secrets/` (§10 intro); `/opt/kvitt/deployed-digests` with a WARNING on a changed digest and a workflow digest step; `kvitt_cicd` with `write:package` only (§10.3, §10.10); the plain root key (variant B) removed (§10.5.1); the `remote_ip` check (§10.10). D-114: pictures at most 16 000 000 pixels and 64 MB decoded, `image_invalid` (AC-4.4); `fcm_token` 1 to 512 characters, else `invalid_request` (§9.6, E-17). D-115: a token is accepted only after a 2xx, an unaccepted token counts against its IP, a 401 withdraws acceptance, entries pruned when their window expires; revises D-032 (§8.6). Without a decision id: the server overhaul (`8103dae` to `b985ce9`: shared row reads and inserts, invite creation and CLI helpers, one test harness `tests/common/`, clippy pedantic wins, synchronous `tasks::start`, `not_implemented` no longer produced; 268 server tests; all crates at their latest versions) (§6.3, §14.3.2, §14.7). Without a decision id: `install.sh` keeps the deployed `KVITT_TAG`, `--provenance=false`, the shared `tools/gradle-lock.sh`, `RCLONE_REMOTE` dropped from the development env (§10 intro, §10.3, §11.3); `strings.csv` 577 to 562 rows (§13.4); `localeFilters` en, sv (AC-19.1); `work-testing` and the screenshot harness fix (§6.2); release APK 10.0 MB (§10.6, §10.10); M12f row in §11.2 (941 tests); §11.4 validation reports (`docs/validation/ac-coverage.md`, `docs/validation/security-review.md`); the history rewrite and first push to `origin` (`18c527f`): every commit hash cited in this file is the rewritten one; `7c453d8` (the oxipng pass) no longer exists as a commit, its PNGs are in `18c527f`. | | 0.18 | 2026-10-18 | M12f (`dc7a296`, `5b2ed37`, `2bd6b58`, `c366fbd`, `a0259ab`), the server gap pass (`8660b7f`), the server overhaul (`8103dae` to `b985ce9`), the infra overhaul (`2fac1ec` to `e70fe0d`) and infra security fixes (`7fb86f5`), the AC-coverage audit and the security review. D-109: S-16 shows a rejected change's lost version read-only, `Rejection.lost: RejectedVersion?` with `RejectedVersion(row, accountName, payer, mode, shares, notes, deleted)` from `rejected_payload`, null when undecodable or a user or the category is missing; dialog "Din version", "Du tog bort den här", "Visa nuvarande version"; ledger rows take a null `onClick` (read-only); four `home_sync_lost_*` strings (§13.3 S-16, §14.2.4, §14.4.6, design-spec S-16). D-110: `AboutState.schemaVersion`; S-22 reads `/health` when it opens online and shows "<version> (schema <n>)" (`settings_about_server_version_schema`), header fallback offline (§13.3 S-22, §14.2.4, E-1, design-spec S-22). D-111: `SettlementRepository.start` and `recompute` return `SettleState?` (null without a session or partner), `save` returns `Invalid(payer, validation_failed)`; no throws (§14.2.4, AC-5.1). D-112: a failed E-17 send is retried after each successful sync until accepted, a newer FID replaces it (AC-13.7). D-113: `kvitt-server invite-partner` (0 ok, 2 household full, 3 no admin) and the `push sent` info line (§8.7, §10.4); `tools/contracts-check.py` 134 checks. D-116: notifications `VISIBILITY_PRIVATE` with the generic public version (new AC-13.9); L-6 kept as built, S-1's server row expanded for a non-default server, the confirm step listed as OQ-6 (AC-2.6, AC-2.8, §12). D-117: Caddy log redaction of `/i/<code>` and query strings, `Content-Security-Policy`, `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` (§8.7, §10.5.2, which now quotes `infra/pi/Caddyfile` at `7fb86f5`); Caddy as uid 10002 with group 1000, read-only, `cap_drop ALL` plus `NET_BIND_SERVICE`, tmpfs; `/certs` 0750 1000:1000, `caddy_data` 10002, extending D-105 (§8.9, §10.2); the development stack copies only `fcm-service-account.json` into `infra/dev/secrets/` (§10 intro); `/opt/kvitt/deployed-digests` with a WARNING on a changed digest and a workflow digest step; `kvitt_cicd` with `write:package` only (§10.3, §10.10); the plain root key (variant B) removed (§10.5.1); the `remote_ip` check (§10.10). D-114: pictures at most 16 000 000 pixels and 64 MB decoded, `image_invalid` (AC-4.4); `fcm_token` 1 to 512 characters, else `invalid_request` (§9.6, E-17). D-115: a token is accepted only after a 2xx, an unaccepted token counts against its IP, a 401 withdraws acceptance, entries pruned when their window expires; revises D-032 (§8.6). Without a decision id: the server overhaul (`8103dae` to `b985ce9`: shared row reads and inserts, invite creation and CLI helpers, one test harness `tests/common/`, clippy pedantic wins, synchronous `tasks::start`, `not_implemented` no longer produced; 268 server tests; all crates at their latest versions) (§6.3, §14.3.2, §14.7). Without a decision id: `install.sh` keeps the deployed `KVITT_TAG`, `--provenance=false`, the shared `tools/gradle-lock.sh`, `RCLONE_REMOTE` dropped from the development env (§10 intro, §10.3, §11.3); `strings.csv` 577 to 562 rows (§13.4); `localeFilters` en, sv (AC-19.1); `work-testing` and the screenshot harness fix (§6.2); release APK 10.0 MB (§10.6, §10.10); M12f row in §11.2 (941 tests); §11.4 validation reports (`docs/validation/ac-coverage.md`, `docs/validation/security-review.md`); the history rewrite and first push to `origin` (`18c527f`): every commit hash cited in this file is the rewritten one; `7c453d8` (the oxipng pass) no longer exists as a commit, its PNGs are in `18c527f`. |
| 0.19 | 2026-10-18 | The Android overhaul (`e4e6e5d` to `c582363`). D-118: shared test support in `:core:design`'s test fixtures (`se.hagstedt.kvitt.design.testing`: `Variant`, `TestClock`, `TestEnvironment`, `ScreenSnapshots` with `Root`/`Screen`/`ScreenWhenIdle`, `MainDispatcherRule`, `withMain`) through `testImplementation(testFixtures(project(":core:design")))`, not a module, the architecture check rejects test fixtures outside test configurations; the convention plugin `kvitt.android.screenshots`; `format/Failures.kt`, `format/Limits.kt` and `savedSplitLabel` in `:core:design`; `KvittMotion` only the seesaw spring, `KvittIcon.Description` and unused dimens and shapes removed; four catalog entries removed; one `CopyCategoryKeywords` task (§6.2 `:core:design` row and Screenshot tests, §11.1, §11.3; design-spec §2.11 and the icon table). Without a decision id: overhaul outcomes in §11.3 (Android `c582363`, server `b985ce9`, infra `e70fe0d`); 941 tests, no golden changed, release APK 10.46 MB; pushed to `origin` at `c582363`; `infra/README.md` APK size about 10 MB; pre-rewrite hashes in `PLAN-*.md` replaced by the rewritten ones. |
+26 -20
View File
@@ -3,9 +3,11 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this. Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand ## 1. Where things stand
- `main` at `b985ce9` (the server overhaul, landed after M12f's `a0259ab`) plus the SPEC v0.18 docs commit from `agent-docs-v018` once it lands. `origin/main` (Gitea) at `18c527f`; everything after it is local until the next push. - `main` at `c582363` (the Android overhaul, landed after the SPEC v0.18 docs commit `48fe119`) plus the SPEC v0.19 docs commit from `agent-docs-v019` once it lands. `origin/main` (Gitea) at `c582363`: everything up to the Android overhaul is pushed. Nothing in flight; tonight's queue is complete.
- History rewrite and first push, 2026-10-18 (owner authorised): superseded PNG versions were dropped from the whole history (no LFS), the current goldens and UX mock-ups restored in `18c527f`; pack 45 MiB; `origin/main` pushed at `18c527f`. A bundle of the pre-rewrite history (`kvitt-pre-rewrite.bundle`) is kept outside the repository, in the orchestrator's scratchpad. Every commit from M2b on got a new hash; the oxipng pass (`7c453d8` before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC, `docs/decisions.md`, this file and `prompts/README.md` cite the rewritten hashes since v0.18 (`7c453d8` stays as the oxipng pass's name; `docs/validation/ac-coverage.md` keeps its pre-rewrite hashes verbatim); plans, spike reports and READMEs may still cite pre-rewrite hashes: find a commit by its subject with `git log --oneline --grep`. - History rewrite and first push, 2026-10-18 (owner authorised): superseded PNG versions were dropped from the whole history (no LFS), the current goldens and UX mock-ups restored in `18c527f`; pack 45 MiB; `origin/main` pushed at `18c527f`. A bundle of the pre-rewrite history (`kvitt-pre-rewrite.bundle`) is kept outside the repository, in the orchestrator's scratchpad. Every commit from M2b on got a new hash; the oxipng pass (`7c453d8` before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC, `docs/decisions.md`, this file and `prompts/README.md` cite the rewritten hashes since v0.18 (`7c453d8` stays as the oxipng pass's name; `docs/validation/ac-coverage.md` keeps its pre-rewrite hashes verbatim); since v0.19 the plans cite rewritten hashes too (eight pre-rewrite hashes in `PLAN-*.md` replaced by the commit with the same subject); `docs/spikes/pi-image-arm64.md` and the READMEs had none stale. For any other old hash, find the commit by its subject with `git log --oneline --grep`.
- History (rewritten hashes): `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `c277ee3` M2b, `06f48ef` M2a, `6dce4c7` SPEC v0.4, `7869ec4` M3, `8f0e156` M5, `853c038` SPEC v0.5, `15056dc` M4, `d9dd584` SPEC v0.6, `50339ae` M9, `86e08a1` SPEC v0.7, `aa765bb` M12a, `d39eb2c` SPEC v0.8, `b09f322` M12c, `6eaccec` M12b, `0006bd7` UX Kalas categories and tokens, `6da6b1b` UX sprint U2, `e676812` SPEC v0.9, `557af7e` M12d, `7730a31` M6, `9ee67d4` SPEC v0.10, `a41f240` and `6453932` app wiring before M8, `1ea752c` M7a, `ea7d494` M7e, `7380e50` M7d, `0e1d99d` M7b, `0b140aa` M7c, `c4ab35f` M7f, `b24c8b5` M6b, `f4dc10c` SPEC v0.11, `3dda414` M7g, `cb832a7` and `3d9eea8` M8, `6159e18` SPEC v0.12, `8c60715` M12e, `b7b7483` M12e launcher icon, `51d6ff8` SPEC v0.13, `a312758` Vico removal, `f56dcbe` FCM `fid`, `3e6c337` Pi image arm64 check, `e2dba84` SPEC v0.14, `328c466` M8b, `6eb3f85` `FcmMessageBody.fid`, `bfde12c` SPEC v0.15, `75b549f` M8c goldens, `a0c25b0` M8c licences row, `c30a49e` R8 release build, `bacbd18` M8d, `bfc3acb` SPEC v0.16, `2476667` M2c-pi bundle, `a59abeb` M8e, `b4d9467` Dockerfile `CARGO_BUILD_JOBS`, `956908a` M8f, `1328afe` chips cleanup, `400eac9` SPEC v0.17, `18c527f` goldens restored after the rewrite; then `2fac1ec` to `e70fe0d` infra overhaul, `8660b7f` server gap pass, `7fb86f5` infra security, `dc7a296` to `a0259ab` M12f. - History (rewritten hashes): `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `c277ee3` M2b, `06f48ef` M2a, `6dce4c7` SPEC v0.4, `7869ec4` M3, `8f0e156` M5, `853c038` SPEC v0.5, `15056dc` M4, `d9dd584` SPEC v0.6, `50339ae` M9, `86e08a1` SPEC v0.7, `aa765bb` M12a, `d39eb2c` SPEC v0.8, `b09f322` M12c, `6eaccec` M12b, `0006bd7` UX Kalas categories and tokens, `6da6b1b` UX sprint U2, `e676812` SPEC v0.9, `557af7e` M12d, `7730a31` M6, `9ee67d4` SPEC v0.10, `a41f240` and `6453932` app wiring before M8, `1ea752c` M7a, `ea7d494` M7e, `7380e50` M7d, `0e1d99d` M7b, `0b140aa` M7c, `c4ab35f` M7f, `b24c8b5` M6b, `f4dc10c` SPEC v0.11, `3dda414` M7g, `cb832a7` and `3d9eea8` M8, `6159e18` SPEC v0.12, `8c60715` M12e, `b7b7483` M12e launcher icon, `51d6ff8` SPEC v0.13, `a312758` Vico removal, `f56dcbe` FCM `fid`, `3e6c337` Pi image arm64 check, `e2dba84` SPEC v0.14, `328c466` M8b, `6eb3f85` `FcmMessageBody.fid`, `bfde12c` SPEC v0.15, `75b549f` M8c goldens, `a0c25b0` M8c licences row, `c30a49e` R8 release build, `bacbd18` M8d, `bfc3acb` SPEC v0.16, `2476667` M2c-pi bundle, `a59abeb` M8e, `b4d9467` Dockerfile `CARGO_BUILD_JOBS`, `956908a` M8f, `1328afe` chips cleanup, `400eac9` SPEC v0.17, `18c527f` goldens restored after the rewrite; then `2fac1ec` to `e70fe0d` infra overhaul, `8660b7f` server gap pass, `7fb86f5` infra security, `dc7a296` to `a0259ab` M12f, `8103dae` to `b985ce9` server overhaul, `48fe119` SPEC v0.18, `e4e6e5d` to `c582363` Android overhaul.
- Landed since SPEC v0.18 (recorded in v0.19):
- Android overhaul (`e4e6e5d`, `8229ed6`, `515b60f`, `e08aaf0`, `0714242`, `a92c236`, `74b5fbd`, `c582363`; D-118): shared test support in `:core:design`'s test fixtures (`se.hagstedt.kvitt.design.testing`), `kvitt.android.screenshots` wires Roborazzi and the test libraries; `format/Failures.kt`, `format/Limits.kt`, `savedSplitLabel` in `:core:design`; dead code removed (`KvittMotion` only the seesaw spring, `KvittIcon.Description`, unused dimens and shapes); four catalog entries removed; one `CopyCategoryKeywords` task; one `configureAndroidModule`; S-7's state types in `ExpenseEditorState.kt`. Gate at landing: 941 tests, no golden changed. Release APK 10.46 MB.
- Landed since SPEC v0.17 (recorded in v0.18): - Landed since SPEC v0.17 (recorded in v0.18):
- Infra overhaul (`2fac1ec`, `61f92c8`, `fc6126c`, `b647faf`, `e70fe0d`; no decision): `tools/gradle-lock.sh` shared by `gradle-serial.sh`, `clean-builds.sh` and `infra/build-images.sh`; the pre-commit hook runs through `gradle-serial.sh` with JDK 25 on `PATH` (hooks reinstalled in `main` after `e70fe0d`); `install.sh` keeps the deployed `KVITT_TAG`; `--provenance=false` on buildx pushes, the workflow token through env; `RCLONE_REMOTE` dropped from the dev env. - Infra overhaul (`2fac1ec`, `61f92c8`, `fc6126c`, `b647faf`, `e70fe0d`; no decision): `tools/gradle-lock.sh` shared by `gradle-serial.sh`, `clean-builds.sh` and `infra/build-images.sh`; the pre-commit hook runs through `gradle-serial.sh` with JDK 25 on `PATH` (hooks reinstalled in `main` after `e70fe0d`); `install.sh` keeps the deployed `KVITT_TAG`; `--provenance=false` on buildx pushes, the workflow token through env; `RCLONE_REMOTE` dropped from the dev env.
- Server gap pass (`8660b7f`, D-113): `kvitt-server invite-partner` (0 ok, 2 household full, 3 no admin), so a scripted partner needs no second admin device; `push sent` info line; CLI tests (`invite-partner`, `migrate`, `healthcheck`, SIGTERM drain); `tools/contracts-check.py` 134 checks. - Server gap pass (`8660b7f`, D-113): `kvitt-server invite-partner` (0 ok, 2 household full, 3 no admin), so a scripted partner needs no second admin device; `push sent` info line; CLI tests (`invite-partner`, `migrate`, `healthcheck`, SIGTERM drain); `tools/contracts-check.py` 134 checks.
@@ -13,30 +15,31 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- M12f (`dc7a296`, `5b2ed37`, `2bd6b58`, `c366fbd`, `a0259ab`; D-109 to D-112, D-116): S-16 lost version read-only, S-22 "<version> (schema <n>)" from `/health`, `SettlementRepository` returns null instead of throwing, E-17 retry after each successful sync, invite links dropped while paired, private notifications on the lock screen, `localeFilters` en/sv, `:data` tests (429, D-040, WorkManager via `work-testing`), the home/account screenshot harness fix (stale `sv_dark` goldens re-recorded), 19 unused string keys removed (`strings.csv` 562 rows). Gate at landing: 941 tests. Release APK 10.0 MB. - M12f (`dc7a296`, `5b2ed37`, `2bd6b58`, `c366fbd`, `a0259ab`; D-109 to D-112, D-116): S-16 lost version read-only, S-22 "<version> (schema <n>)" from `/health`, `SettlementRepository` returns null instead of throwing, E-17 retry after each successful sync, invite links dropped while paired, private notifications on the lock screen, `localeFilters` en/sv, `:data` tests (429, D-040, WorkManager via `work-testing`), the home/account screenshot harness fix (stale `sv_dark` goldens re-recorded), 19 unused string keys removed (`strings.csv` 562 rows). Gate at landing: 941 tests. Release APK 10.0 MB.
- Validation reports (SPEC §11.4): `docs/validation/ac-coverage.md` (taken at `956908a`, `ed1b130` before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. `docs/validation/security-review.md` (taken at `18c527f`): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (`e70fe0d`); H-1 (`725901c`, D-115), M-3 (`baa5806`) and L-4 (`b985ce9`, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4); M-1 and L-3 wait for the owner (§4). H-1 must be fixed before port 8443 is opened. - Validation reports (SPEC §11.4): `docs/validation/ac-coverage.md` (taken at `956908a`, `ed1b130` before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. `docs/validation/security-review.md` (taken at `18c527f`): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (`e70fe0d`); H-1 (`725901c`, D-115), M-3 (`baa5806`) and L-4 (`b985ce9`, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4); M-1 and L-3 wait for the owner (§4). H-1 must be fixed before port 8443 is opened.
- Server overhaul (landed on `main` `8103dae` to `b985ce9`): shared helpers and one test harness, clippy pedantic wins; H-1 rate limiter (`725901c`, D-115), pictures at most 16 MP and 64 MB decoded (`baa5806`, D-114), `fcm_token` at most 512 characters (`b985ce9`, D-114); shared row inserts, invite creation and CLI helpers, `tests/common/` harness, synchronous `tasks::start`; 268 server tests; all crates at their latest versions. Recorded in SPEC v0.18. - Server overhaul (landed on `main` `8103dae` to `b985ce9`): shared helpers and one test harness, clippy pedantic wins; H-1 rate limiter (`725901c`, D-115), pictures at most 16 MP and 64 MB decoded (`baa5806`, D-114), `fcm_token` at most 512 characters (`b985ce9`, D-114); shared row inserts, invite creation and CLI helpers, `tests/common/` harness, synchronous `tasks::start`; 268 server tests; all crates at their latest versions. Recorded in SPEC v0.18.
- In flight: the Android overhaul agent only (and this docs pass); then a push to `origin`. - In flight: nothing.
- Earlier outcomes (details in SPEC §11.2, §11.3 and `docs/decisions.md`): M8 end-to-end test passed on the owner's S26 against the dev server on 2026-10-18 (pairing, scripted partner, offline expense synced, push by `fid` delivered); M8b to M8f UI fixes; the M2c-pi bundle `infra/pi/` (D-103 to D-106, owner checklist in `infra/README.md` "Pi: what the owner does"); release builds R8-shrunk and signed (alias `kvitt`, RSA 4096, v2). - Earlier outcomes (details in SPEC §11.2, §11.3 and `docs/decisions.md`): M8 end-to-end test passed on the owner's S26 against the dev server on 2026-10-18 (pairing, scripted partner, offline expense synced, push by `fid` delivered); M8b to M8f UI fixes; the M2c-pi bundle `infra/pi/` (D-103 to D-106, owner checklist in `infra/README.md` "Pi: what the owner does"); release builds R8-shrunk and signed (alias `kvitt`, RSA 4096, v2).
- Compatibility: an APK from M12e on cannot pair or list devices against a pre-M12e server, so the server is rebuilt and redeployed before a new APK is installed (SPEC §14.8). The dev server already runs schema 3. - Compatibility: an APK from M12e on cannot pair or list devices against a pre-M12e server, so the server is rebuilt and redeployed before a new APK is installed (SPEC §14.8). The dev server already runs schema 3.
- Remote: `origin` on Gitea, SSH key works. The owner created the Gitea secret `kvitt_cicd` and the Drive token (`infra/secrets/rclone.conf`, `[gdrive]`; folder id in `.env` as `RCLONE_FOLDER_ID`); the cert-sync key `infra/secrets/ha_ssh_key` exists (all git-ignored). The first push triggered the Gitea Actions workflow; its result is the owner's §3 item 1. - Remote: `origin` on Gitea, SSH key works; pushed at `c582363`. The owner created the Gitea secret `kvitt_cicd` and the Drive token (`infra/secrets/rclone.conf`, `[gdrive]`; folder id in `.env` as `RCLONE_FOLDER_ID`); the cert-sync key `infra/secrets/ha_ssh_key` exists (all git-ignored). The first push triggered the Gitea Actions workflow; its result is the owner's §3 item 1.
- 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. - 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.
- Importer: the dry run of 2026-10-18 reconciled on both real files (Vardag 1746 rows, Hemma 37 rows, 0 unmapped). The CSV files stay in `export/` (git-ignored). - Importer: the dry run of 2026-10-18 reconciled on both real files (Vardag 1746 rows, Hemma 37 rows, 0 unmapped). The CSV files stay in `export/` (git-ignored).
- Development stack (M2c-dev): running on the Mac at `http://192.168.1.210:8080`, database at schema 3, data kept; backups `infra/dev/backups/kvitt-20261018T093143Z.sqlite.gz` and `kvitt-20261018T130443Z.sqlite.gz` (git-ignored). On this Podman setup run `infra/dev/up.sh --down`, then `infra/dev/up.sh`, to swap in a rebuilt image. Since `7fb86f5` `up.sh` copies only `fcm-service-account.json` into `infra/dev/secrets/` (git-ignored). The owner's phone is paired with it; a scripted partner exists (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`. - Development stack (M2c-dev): running on the Mac at `http://192.168.1.210:8080`, database at schema 3, data kept; backups `infra/dev/backups/kvitt-20261018T093143Z.sqlite.gz` and `kvitt-20261018T130443Z.sqlite.gz` (git-ignored). On this Podman setup run `infra/dev/up.sh --down`, then `infra/dev/up.sh`, to swap in a rebuilt image. Since `7fb86f5` `up.sh` copies only `fcm-service-account.json` into `infra/dev/secrets/` (git-ignored). The owner's phone is paired with it; a scripted partner exists (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`.
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug`, paired against the dev server; the next install carries M8b to M8f and M12f. - Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug`, paired against the dev server; the next install carries M8b to M8f, M12f and the Android overhaul.
- Disk: the MacBook had about 400 MB free during this docs pass (2026-10-18); run `tools/clean-builds.sh` before the next Gradle build. - Disk: the MacBook had about 12 GB free during the v0.19 docs pass (2026-10-18); run `tools/clean-builds.sh` when it drops under about 5 GB.
## 2. Open work (one line each, with who owns it: agent or owner) ## 2. Open work (one line each, with who owns it: agent or owner)
- Land `agent-docs-v018` (orchestrator): SPEC v0.18, D-109 to D-117, `docs/validation/ac-coverage.md`, `docs/validation/security-review.md`, HANDOFF, `prompts/README.md`, design-spec S-16, S-22 and the S-1 permission line. - Land `agent-docs-v019` (orchestrator): SPEC v0.19, D-118, HANDOFF, `prompts/README.md`, design-spec §2.11 and the icon table, `infra/README.md` APK size, the `PLAN-*.md` hash remap.
- L-10 (a member gets `forbidden` rather than `not_found` for another user's device, so the id's owner is visible): left as the spec says; owner decision pending (§4). - Restore drill AC-18.2 (`docs/validation/restore-drill.md`, M10; the one Missing row of the AC-coverage audit).
- Tooling request (no plan yet): `cargo-audit` in the server gate. - AC-18.1, AC-20.1 and AC-16.3 shown on the Pi and the phones (M2c-pi run, M10).
- Android overhaul (agent, running). - A `KvittWorkerFactory.createWorker` test (Android agent, no plan yet).
- `infra/README.md` lines (infra agent, no plan yet): drop "(milestone M2a)" on the `build.sh` server line; a `buildx rm` note for the builder config; the operations table still says "APKs about 20 MB each" (10 MB since R8, SPEC §10.10). - Security review L-6: an S-1 design state if the owner wants a confirm step for an unusual server (OQ-6, default not in v1; owner, §4).
- Audit items still open: the AC-18.2 restore drill (`docs/validation/restore-drill.md`, M10); AC-18.1, AC-20.1 and AC-16.3 shown on the Pi and phones (M2c-pi, M10); a `KvittWorkerFactory` test (Android agent); an S-1 design state if the owner wants a confirm step for an unusual server (OQ-6, default not in v1). - Security review L-10: `forbidden` or `not_found` for the other user's device id (owner decision, §4).
- Tooling: `cargo-audit` in the server gate (agent, once the owner has installed it, §3 item 6).
- State-class naming: `*Ui`, `*UiState` and `*State` are mixed across features; needs a decision if a convention is wanted (owner; no plan).
- Unverified risk: the workflow pushes a git tag; whether that re-triggers the path-filtered workflow is not known (watch the first runs). - Unverified risk: the workflow pushes a git tag; whether that re-triggers the path-filtered workflow is not known (watch the first runs).
- Push the commits after `18c527f` to `origin` (orchestrator, when the owner asks; pushes need an explicit ask).
- Install the next debug build and have the owner look (§3 item 2); build a signed release for the smoke taps (§3 item 3). - Install the next debug build and have the owner look (§3 item 2); build a signed release for the smoke taps (§3 item 3).
- Owner runs the M2c-pi bundle on the Kvitt Pi (§3 item 5); record the accepted cert-sync key form and the router-forward answer in `infra/README.md`. - Owner runs the M2c-pi bundle on the Kvitt Pi (§3 item 5); record the accepted cert-sync key form and the router-forward answer in `infra/README.md`.
- Plans (SPEC §11.2 v0.18), in dependency order; prompt `prompts/<id>.md`: - Plans (SPEC §11.2 v0.19), 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 to M8f, M9, M12a to M12f, U1, U2, R8 release build, M2c-pi bundle, server gap pass, infra overhaul, infra security and the server overhaul: landed. - M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b to M8f, M9, M12a to M12f, U1, U2, R8 release build, M2c-pi bundle, server gap pass, infra overhaul, infra security, the server overhaul and the Android overhaul: landed.
- Next: the Android overhaul; a server image with the H-1 fix before the router forward; the owner runs the M2c-pi bundle on the Kvitt Pi (D-100); M10 First release after it (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). - Next: a server image with the H-1 fix before the router forward; the owner runs the M2c-pi bundle on the Kvitt Pi (D-100); M10 First release after it (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) ## 3. HUMAN steps pending (relay in chat)
1. Gitea, the repository, Actions: did the run "Build Kvitt images" triggered by the first push succeed (yes/no), and how long did it take? 1. Gitea, the repository, Actions: did the run "Build Kvitt images" triggered by the first push succeed (yes/no), and how long did it take?
@@ -58,20 +61,23 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
7. Router, WAN, Virtual Server / Port Forwarding: TCP 8443 to the Kvitt Pi's 8443 (only after the H-1 fix is deployed); on the phone with Wi-Fi off, `https://icehack.asuscomm.com:8443/health` shows `"status":"ok"` without a certificate warning (yes/no); then the `remote_ip` check of the README's operations table shows a public address (yes/no). 7. Router, WAN, Virtual Server / Port Forwarding: TCP 8443 to the Kvitt Pi's 8443 (only after the H-1 fix is deployed); on the phone with Wi-Fi off, `https://icehack.asuscomm.com:8443/health` shows `"status":"ok"` without a certificate warning (yes/no); then the `remote_ip` check of the README's operations table shows a public address (yes/no).
8. `/opt/kvitt/backup-now.sh` ends with "last-success: <stamp>"; `/opt/kvitt/bootstrap.sh` prints the admin invite code (first install only). 8. `/opt/kvitt/backup-now.sh` ends with "last-success: <stamp>"; `/opt/kvitt/bootstrap.sh` prints the admin invite code (first install only).
9. New versions: `/opt/kvitt/deploy.sh v1.0.N` after the Gitea workflow pushed that tag; it prints the image digests and warns if a recorded tag changed digest. 9. New versions: `/opt/kvitt/deploy.sh v1.0.N` after the Gitea workflow pushed that tag; it prints the image digests and warns if a recorded tag changed digest.
6. Install cargo-audit on the MacBook (`cargo install cargo-audit`) so it can join the server gate: yes/no.
## 4. Owner questions (asked, unanswered) ## 4. Owner questions (asked, unanswered)
- The §3 item 1 workflow result and duration. - The §3 item 1 workflow result and duration.
- The five security-review decisions of §3 item 4 (M-1 backup encryption, M-2 key form, L-3 APK download rate, `write:repository`, the 16 MP and 512-character assumptions). - The five security-review decisions of §3 item 4 (M-1 backup encryption, M-2 key form, L-3 APK download rate, `write:repository`, the 16 MP and 512-character assumptions).
- OQ-6 (SPEC §12): a confirm step on S-1 for an unusual server; default "not in v1". - OQ-6 (SPEC §12): a confirm step on S-1 for an unusual server; default "not in v1".
- Security review L-10: should a member get `not_found` instead of `forbidden` for a device id of the other user (hides whose it is)? As built and as the spec says: `forbidden`. - Security review L-10: should a member get `not_found` instead of `forbidden` for a device id of the other user (hides whose it is)? As built and as the spec says: `forbidden`.
- §3 item 6: install `cargo-audit`, yes/no.
- State-class naming (`*Ui`, `*UiState`, `*State` mixed): fix one convention, or leave as built?
## 5. Paused (waiting on something outside the repo) ## 5. Paused (waiting on something outside the repo)
- Nothing. - Nothing.
## 6. Numbering ## 6. Numbering
- Decisions: D-001 to D-117 used, except D-008 and D-009 (never used); none reserved. Next free: **D-118**. - Decisions: D-001 to D-118 used, except D-008 and D-009 (never used); none reserved. Next free: **D-119**.
- Spec version: v0.18 written; next **v0.19**. - Spec version: v0.19 written; next **v0.20**.
- Server schema version **3** (migration 0003); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Both unchanged by v0.18. - Server schema version **3** (migration 0003); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Both unchanged by v0.19.
- Sync protocol **1**, push payload **1**. - Sync protocol **1**, push payload **1**.
## 7. How the work is run ## 7. How the work is run
+1
View File
@@ -122,3 +122,4 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-115 | 2026-10-18 | Rate-limit token acceptance, revising D-032: a bearer token is accepted only when a response to it is 2xx; a request with a token not yet accepted counts against its client IP's unauthenticated limit before it runs and is refused with `rate_limited` once the IP is over; a 2xx gives that count back; a 401 for an accepted token counts against the IP and withdraws the acceptance; token entries are pruned once their window expires, accepted or not; every 401 counts against the IP exactly once. Landed by the server overhaul (`725901c`), with unit tests per rule and an integration test of random tokens without the protocol header (60 × 426, then 429, the paired device unaffected). | Security review H-1: a random token without `X-Kvitt-Protocol` (426) became accepted for good, bypassed the IP limit and was never pruned, so a stranger could grow the in-memory map without bound. | SPEC §8.6 | | D-115 | 2026-10-18 | Rate-limit token acceptance, revising D-032: a bearer token is accepted only when a response to it is 2xx; a request with a token not yet accepted counts against its client IP's unauthenticated limit before it runs and is refused with `rate_limited` once the IP is over; a 2xx gives that count back; a 401 for an accepted token counts against the IP and withdraws the acceptance; token entries are pruned once their window expires, accepted or not; every 401 counts against the IP exactly once. Landed by the server overhaul (`725901c`), with unit tests per rule and an integration test of random tokens without the protocol header (60 × 426, then 429, the paired device unaffected). | Security review H-1: a random token without `X-Kvitt-Protocol` (426) became accepted for good, bypassed the IP limit and was never pruned, so a stranger could grow the in-memory map without bound. | SPEC §8.6 |
| D-116 | 2026-10-18 | Notifications are posted with `VISIBILITY_PRIVATE` and a public version carrying the generic "Kvitt" / "Open Kvitt to see it" (`notif_generic_title`, `notif_generic_body`), so a secure lock screen shows no description, amount, account or name (new AC-13.9; `NotificationPrivacyTest`). L-6 (a link can prefill any server on S-1) stays as built: S-1 opens its server row expanded when the link's server is not the release default, release builds require `https://`, and a paired phone ignores links (D-084); a confirmation step would need a design state and is not in v1 (OQ-6). Landed by M12f (`2bd6b58`). | Security review L-7 and L-6. | SPEC §4 (AC-2.6, AC-2.8, AC-13.9), §12 (OQ-6) | | D-116 | 2026-10-18 | Notifications are posted with `VISIBILITY_PRIVATE` and a public version carrying the generic "Kvitt" / "Open Kvitt to see it" (`notif_generic_title`, `notif_generic_body`), so a secure lock screen shows no description, amount, account or name (new AC-13.9; `NotificationPrivacyTest`). L-6 (a link can prefill any server on S-1) stays as built: S-1 opens its server row expanded when the link's server is not the release default, release builds require `https://`, and a paired phone ignores links (D-084); a confirmation step would need a design state and is not in v1 (OQ-6). Landed by M12f (`2bd6b58`). | Security review L-7 and L-6. | SPEC §4 (AC-2.6, AC-2.8, AC-13.9), §12 (OQ-6) |
| D-117 | 2026-10-18 | Infra security fixes, landed `7fb86f5`. (1) Caddy's access log and error log (`log default`) filter `request>uri` with ``regexp `^(/i/)[^?]+\|([?]).+` `${1}${2}REDACTED` `` (`wrap json`), so invite codes and the `?from=` name never reach a log; `Authorization` stays redacted by default. (2) Deferred response headers `Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"`, `X-Content-Type-Options nosniff`, `X-Frame-Options DENY`, `Referrer-Policy no-referrer`. (3) The `caddy` container runs as `10002:10002` with `group_add` 1000, `read_only`, `cap_drop: [ALL]` plus `cap_add: [NET_BIND_SERVICE]` (the image's binary has that file capability and cannot be exec'd without it), `no-new-privileges`, tmpfs `/config` and `/tmp`; `install.sh` sets `/certs` 0750 1000:1000 and `caddy_data` to 10002 (extends D-105). (4) The development stack mounts `infra/dev/secrets/` (git-ignored), into which `up.sh` copies only `fcm-service-account.json`. (5) `deploy.sh` and `install.sh` print each pulled image's digest and append `<UTC time> <KVITT_TAG> <image reference> <digest>` to `/opt/kvitt/deployed-digests`, with a WARNING when a recorded reference changed digest; the workflow prints the pushed digests. (6) `kvitt_cicd` needs only `write:package` (the tag push uses the checkout credentials). (7) The plain root key (variant B) for cert-sync is no longer acceptable: variant A (restricted) or a dedicated non-root user. (8) One-off check after the router forward that Caddy's `remote_ip` shows public addresses. | Security review at `18c527f`: L-1, L-2, L-5, L-9, M-2, I-5. | SPEC §8.7, §8.9, §10 intro, §10.2, §10.3, §10.5.1, §10.5.2, §10.10; `infra/pi/`, `infra/dev/`, `.gitea/workflows/build-images.yaml`, `infra/README.md` | | D-117 | 2026-10-18 | Infra security fixes, landed `7fb86f5`. (1) Caddy's access log and error log (`log default`) filter `request>uri` with ``regexp `^(/i/)[^?]+\|([?]).+` `${1}${2}REDACTED` `` (`wrap json`), so invite codes and the `?from=` name never reach a log; `Authorization` stays redacted by default. (2) Deferred response headers `Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"`, `X-Content-Type-Options nosniff`, `X-Frame-Options DENY`, `Referrer-Policy no-referrer`. (3) The `caddy` container runs as `10002:10002` with `group_add` 1000, `read_only`, `cap_drop: [ALL]` plus `cap_add: [NET_BIND_SERVICE]` (the image's binary has that file capability and cannot be exec'd without it), `no-new-privileges`, tmpfs `/config` and `/tmp`; `install.sh` sets `/certs` 0750 1000:1000 and `caddy_data` to 10002 (extends D-105). (4) The development stack mounts `infra/dev/secrets/` (git-ignored), into which `up.sh` copies only `fcm-service-account.json`. (5) `deploy.sh` and `install.sh` print each pulled image's digest and append `<UTC time> <KVITT_TAG> <image reference> <digest>` to `/opt/kvitt/deployed-digests`, with a WARNING when a recorded reference changed digest; the workflow prints the pushed digests. (6) `kvitt_cicd` needs only `write:package` (the tag push uses the checkout credentials). (7) The plain root key (variant B) for cert-sync is no longer acceptable: variant A (restricted) or a dedicated non-root user. (8) One-off check after the router forward that Caddy's `remote_ip` shows public addresses. | Security review at `18c527f`: L-1, L-2, L-5, L-9, M-2, I-5. | SPEC §8.7, §8.9, §10 intro, §10.2, §10.3, §10.5.1, §10.5.2, §10.10; `infra/pi/`, `infra/dev/`, `.gitea/workflows/build-images.yaml`, `infra/README.md` |
| D-118 | 2026-10-18 | Shared test support lives in `:core:design`'s test fixtures (`src/testFixtures`, package `se.hagstedt.kvitt.design.testing`: `Variant`, `TestClock`, `TestEnvironment`, `ScreenSnapshots` with capture modes `Root`/`Screen`/`ScreenWhenIdle`, `MainDispatcherRule`, `withMain`), consumed as `testImplementation(testFixtures(project(":core:design")))`; not a module, so the §6.2 graph is unchanged; the architecture check rejects test fixtures outside test configurations. The convention plugin `kvitt.android.screenshots` wires Roborazzi and the test libraries; feature build scripts carry no screenshot block. Shared failure texts (`format/Failures.kt`: `FAILURE_NETWORK`, `commonFailureText`), limits (`format/Limits.kt`: `BP_TOTAL`, `DISPLAY_NAME_MAX`, `DESCRIPTION_MAX`, `NOTES_MAX`, `FIRST_DATE`, `DAYS_AHEAD_MAX`) and `savedSplitLabel` live in `:core:design`. `KvittMotion` holds only the seesaw spring; `KvittIcon.Description` and unused dimens/shapes removed; catalog entries `roborazzi-junit-rule`, `roborazzi-compose`, `androidx-hilt-navigation-compose`, `androidx-lifecycle-viewmodel-compose` removed; one `CopyCategoryKeywords` task shared by `:data` and `:core:fakes`. 941 tests, no golden changed, release APK 10.46 MB. Landed by the Android overhaul (`e4e6e5d` to `c582363`). | Eight modules each had a copy of the same screenshot harness, and M12f had to fix the same capture bug in two of them; test fixtures rather than a `:core:testing` module let `:core:design`'s own tests use them, add no module to the §6.2 graph and are treated by lint as test sources. The failure-text map, the limits (`BP_TOTAL` in five copies) and the saved-split label were duplicated across features; the removed tokens, icon and catalog entries were never referenced. | SPEC §6.2 (`:core:design` row, Screenshot tests), §11.1, §11.3; `docs/ux/design-spec.md` §2.11 and the icon table; `android/README.md` |
+2 -2
View File
@@ -334,7 +334,7 @@ Icons are dp-sized (24 default; 18 in chips; 15–16 in row meta; 20 in buttons)
| `schedule`, `pause`, `event_repeat`, `drag_indicator`, `restart_alt`, `lock` | S-3, S-19, S-8, S-15 | | `schedule`, `pause`, `event_repeat`, `drag_indicator`, `restart_alt`, `lock` | S-3, S-19, S-8, S-15 |
| `system_update`, `download`, `downloading`, `refresh`, `install_mobile` | S-17 | | `system_update`, `download`, `downloading`, `refresh`, `install_mobile` | S-17 |
| `phonelink_erase`, `smartphone`, `block`, `devices`, `add_link`, `key`, `share`, `content_copy` | S-18, S-20, S-21, S-13 | | `phonelink_erase`, `smartphone`, `block`, `devices`, `add_link`, `key`, `share`, `content_copy` | S-18, S-20, S-21, S-13 |
| `memory`, `description`, `open_in_new` | S-22, S-16 | | `memory`, `open_in_new` | S-22, S-16 (`description` removed as unused, D-118; add back when a screen needs it) |
| `arrow_forward` | S-9, S-10 direction | | `arrow_forward` | S-9, S-10 direction |
### 2.13 Default categories (UX-8) ### 2.13 Default categories (UX-8)
@@ -369,7 +369,7 @@ Every default category has its own colour; `teal` is shared with the archived ex
| §2.7 | `Typography(...)` for the 15 M3 styles (default font family, weights and letter spacing above) plus `KvittTypography(amountHero, amountLarge, amountMedium, amountSmall)` in `LocalKvittTypography` | | §2.7 | `Typography(...)` for the 15 M3 styles (default font family, weights and letter spacing above) plus `KvittTypography(amountHero, amountLarge, amountMedium, amountSmall)` in `LocalKvittTypography` |
| §2.8 | `object KvittSpacing` (or `LocalKvittSpacing`) with `s1` … `s12`, `gutter`, `rowMin`, `ledgerRowMin` | | §2.8 | `object KvittSpacing` (or `LocalKvittSpacing`) with `s1` … `s12`, `gutter`, `rowMin`, `ledgerRowMin` |
| §2.9 | `Shapes(extraSmall … extraLarge)` plus `KvittShapes(categoryTile, accountTile, avatarLarge, illustration, card, accountCard, groupOuter, groupInner, field, fab)` in `LocalKvittShapes` | | §2.9 | `Shapes(extraSmall … extraLarge)` plus `KvittShapes(categoryTile, accountTile, avatarLarge, illustration, card, accountCard, groupOuter, groupInner, field, fab)` in `LocalKvittShapes` |
| §2.11 | `MotionScheme` on `MaterialTheme`; named motions as constants in `KvittMotion` | | §2.11 | `MotionScheme` on `MaterialTheme`; the seesaw spring in `KvittMotion`; navigation has no motion (D-108) |
| Entry point | `KvittTheme(darkTheme: Boolean = isSystemInDarkTheme(), content)`, accessors `KvittTheme.colors`, `.typography`, `.shapes`, `.spacing` | | Entry point | `KvittTheme(darkTheme: Boolean = isSystemInDarkTheme(), content)`, accessors `KvittTheme.colors`, `.typography`, `.shapes`, `.spacing` |
`ColorToken` gains `SLATE` and `AMBER` (UX-5) and `DefaultCategoryKey` gains `HOUSING`, `INSURANCE`, `CHILDREN`, `FEES` (UX-8); `KvittColors.category` is exhaustive over the enum. `ColorToken` gains `SLATE` and `AMBER` (UX-5) and `DefaultCategoryKey` gains `HOUSING`, `INSURANCE`, `CHILDREN`, `FEES` (UX-8); `KvittColors.category` is exhaustive over the enum.
+1 -1
View File
@@ -465,7 +465,7 @@ condition from `depends_on` in `/opt/kvitt/compose.yaml`.
| Certificate expiry and issuer | `docker compose exec cert-sync openssl x509 -in /certs/fullchain.pem -noout -enddate -issuer` | | Certificate expiry and issuer | `docker compose exec cert-sync openssl x509 -in /certs/fullchain.pem -noout -enddate -issuer` |
| Extra device invite | `docker compose exec server kvitt-server pair-device --user <user_id>` | | Extra device invite | `docker compose exec server kvitt-server pair-device --user <user_id>` |
| Memory | `docker stats --no-stream` (server RSS under 100 MB with both phones syncing) | | Memory | `docker stats --no-stream` (server RSS under 100 MB with both phones syncing) |
| Disk | `df -h /` and `docker system df`; SQLite a few MB, pictures under 1 MB, APKs about 20 MB each, 5 kept | | Disk | `df -h /` and `docker system df`; SQLite a few MB, pictures under 1 MB, APKs about 10 MB each since R8, 5 kept |
| No token in Caddy's log | `docker compose logs caddy \| grep -c 'kvt_'` prints `0` | | No token in Caddy's log | `docker compose logs caddy \| grep -c 'kvt_'` prints `0` |
| No invite code or name in Caddy's log | `docker compose logs caddy \| grep '"uri":"/i/' \| grep -vc '/i/REDACTED'` prints `0` | | No invite code or name in Caddy's log | `docker compose logs caddy \| grep '"uri":"/i/' \| grep -vc '/i/REDACTED'` prints `0` |
| Image digests deployed | `tail -n 8 /opt/kvitt/deployed-digests` | | Image digests deployed | `tail -n 8 /opt/kvitt/deployed-digests` |
+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). 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.18). Status as of 2026-10-18: M0 done, M1 landed (`c6df2ab`), M2c-dev landed (`c885aba`), M2a landed (`06f48ef`), M2b landed (`c277ee3`), M3 landed (`7869ec4`), M5 landed (`8f0e156`), M4 landed (`15056dc`), M9 landed (`50339ae`); SPEC v0.7 landed (`86e08a1`); M12a landed (`aa765bb`); SPEC v0.8 landed (`d39eb2c`); M12c landed (`b09f322`); M12b landed (`6eaccec`); U1 and U2 landed (`6da6b1b`, owner approved); SPEC v0.9 landed (`e676812`); M12d landed (`557af7e`); M6 landed (`7730a31`); SPEC v0.10 landed (`9ee67d4`); M7a landed (`1ea752c`), M7e (`ea7d494`), M7d (`7380e50`), M7b (`0e1d99d`), M7c (`0b140aa`), M7f (`c4ab35f`); M6b `:core:design` fixes landed (`b24c8b5`); SPEC v0.11 landed (`f4dc10c`, the M7 and M6b outcomes, D-069 to D-080); M7g feature cleanups landed (`3dda414`, no prompt file; brief by the orchestrator); M8 landed (`cb832a7`, `3d9eea8`), 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 (`8c60715`, `b7b7483`); SPEC v0.13 records its outcomes (D-086 to D-095, server schema 3) and landed (`51d6ff8`); the Vico removal (`a312758`), the FCM `fid` target (`f56dcbe`) and the Pi image arm64 check (`3e6c337`) landed; SPEC v0.14 records them (D-096); the M8 phone test ran on 2026-10-18 and M8b UI fixes landed (`328c466`, D-097) with the `fid` follow-up (`6eb3f85`); 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 (`75b549f`, `a0c25b0`, `7c453d8`; goldens in three variants at 1x, the S-22 licences row removed), the R8 release build (`c30a49e`) and M8d (`bacbd18`, S-7 editor rows) landed; SPEC v0.16 records them (D-098, D-099, D-102) and landed (`bfc3acb`); the M2c-pi bundle (`2476667`, D-103 to D-106), M8e (`a59abeb`, D-107), the Dockerfile `CARGO_BUILD_JOBS` argument (`b4d9467`), M8f (`956908a`, D-108, instant navigation) and the chips cleanup (`1328afe`) landed; SPEC v0.17 records them (D-103 to D-108); what remains of M2c-pi is the owner running the bundle on the Kvitt Pi, then M10; v0.16 D-098, D-099, D-102; v0.17 D-103 to D-108; after v0.17 the history rewrite and first push to `origin` (`18c527f`, all later hashes in this file are the rewritten ones), the infra overhaul (`2fac1ec` to `e70fe0d`), the server gap pass (`8660b7f`, D-113), the infra security fixes (`7fb86f5`, D-117) and M12f (`dc7a296` to `a0259ab`, D-109 to D-112, D-116; no prompt file, orchestrator brief) landed; SPEC v0.18 records them with the AC-coverage audit and the security review (`docs/validation/`); the server overhaul (`8103dae` to `b985ce9`, D-114, D-115) landed too and is recorded in v0.18; an Android overhaul runs. Dependencies follow SPEC §11.1 and §11.2 (v0.19). Status as of 2026-10-18: M0 done, M1 landed (`c6df2ab`), M2c-dev landed (`c885aba`), M2a landed (`06f48ef`), M2b landed (`c277ee3`), M3 landed (`7869ec4`), M5 landed (`8f0e156`), M4 landed (`15056dc`), M9 landed (`50339ae`); SPEC v0.7 landed (`86e08a1`); M12a landed (`aa765bb`); SPEC v0.8 landed (`d39eb2c`); M12c landed (`b09f322`); M12b landed (`6eaccec`); U1 and U2 landed (`6da6b1b`, owner approved); SPEC v0.9 landed (`e676812`); M12d landed (`557af7e`); M6 landed (`7730a31`); SPEC v0.10 landed (`9ee67d4`); M7a landed (`1ea752c`), M7e (`ea7d494`), M7d (`7380e50`), M7b (`0e1d99d`), M7c (`0b140aa`), M7f (`c4ab35f`); M6b `:core:design` fixes landed (`b24c8b5`); SPEC v0.11 landed (`f4dc10c`, the M7 and M6b outcomes, D-069 to D-080); M7g feature cleanups landed (`3dda414`, no prompt file; brief by the orchestrator); M8 landed (`cb832a7`, `3d9eea8`), 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 (`8c60715`, `b7b7483`); SPEC v0.13 records its outcomes (D-086 to D-095, server schema 3) and landed (`51d6ff8`); the Vico removal (`a312758`), the FCM `fid` target (`f56dcbe`) and the Pi image arm64 check (`3e6c337`) landed; SPEC v0.14 records them (D-096); the M8 phone test ran on 2026-10-18 and M8b UI fixes landed (`328c466`, D-097) with the `fid` follow-up (`6eb3f85`); 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 (`75b549f`, `a0c25b0`, `7c453d8`; goldens in three variants at 1x, the S-22 licences row removed), the R8 release build (`c30a49e`) and M8d (`bacbd18`, S-7 editor rows) landed; SPEC v0.16 records them (D-098, D-099, D-102) and landed (`bfc3acb`); the M2c-pi bundle (`2476667`, D-103 to D-106), M8e (`a59abeb`, D-107), the Dockerfile `CARGO_BUILD_JOBS` argument (`b4d9467`), M8f (`956908a`, D-108, instant navigation) and the chips cleanup (`1328afe`) landed; SPEC v0.17 records them (D-103 to D-108); what remains of M2c-pi is the owner running the bundle on the Kvitt Pi, then M10; v0.16 D-098, D-099, D-102; v0.17 D-103 to D-108; after v0.17 the history rewrite and first push to `origin` (`18c527f`, all later hashes in this file are the rewritten ones), the infra overhaul (`2fac1ec` to `e70fe0d`), the server gap pass (`8660b7f`, D-113), the infra security fixes (`7fb86f5`, D-117) and M12f (`dc7a296` to `a0259ab`, D-109 to D-112, D-116; no prompt file, orchestrator brief) landed; SPEC v0.18 records them with the AC-coverage audit and the security review (`docs/validation/`); the server overhaul (`8103dae` to `b985ce9`, D-114, D-115) landed too and is recorded in v0.18; the Android overhaul (`e4e6e5d` to `c582363`, D-118; no prompt file, orchestrator brief) landed and was pushed to `origin` at `c582363`; SPEC v0.19 records it. Nothing is in flight; what remains is the owner's M2c-pi run on the Kvitt Pi, then M10 and M11.
## Dependency order and waves ## Dependency order and waves
@@ -36,7 +36,7 @@ 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 | done (`cb832a7`, `3d9eea8`); on-phone test run 2026-10-18; M8b fixes `328c466`; M8c `75b549f`, `a0c25b0`, `7c453d8`; M8d `bacbd18`; M8e `a59abeb`; M8f `956908a`; chips cleanup `1328afe` (no prompt files; briefs by the orchestrator) | | 4 | M8 App integration | `M8.md` | M3, M4, M5, M7a to M7f, M2c-dev | done (`cb832a7`, `3d9eea8`); on-phone test run 2026-10-18; M8b fixes `328c466`; M8c `75b549f`, `a0c25b0`, `7c453d8`; M8d `bacbd18`; M8e `a59abeb`; M8f `956908a`; chips cleanup `1328afe` (no prompt files; briefs by the orchestrator) |
| 4 | M12e Contract pass | none (orchestrator brief) | M8, SPEC v0.12 (§11.3 list) | done (`8c60715`, `b7b7483`) | | 4 | M12e Contract pass | none (orchestrator brief) | M8, SPEC v0.12 (§11.3 list) | done (`8c60715`, `b7b7483`) |
| 4 | M12f Audit and security follow-ups (Android) | none (orchestrator brief) | M8f, AC-coverage audit, security review | done (`dc7a296`, `5b2ed37`, `2bd6b58`, `c366fbd`, `a0259ab`; D-109 to D-112, D-116) | | 4 | M12f Audit and security follow-ups (Android) | none (orchestrator brief) | M8f, AC-coverage audit, security review | done (`dc7a296`, `5b2ed37`, `2bd6b58`, `c366fbd`, `a0259ab`; D-109 to D-112, D-116) |
| 4 | Server gap pass; infra overhaul; infra security | none (orchestrator briefs) | M2c-pi bundle, security review | done (`8660b7f`, D-113; `2fac1ec` to `e70fe0d`; `7fb86f5`, D-117); server overhaul `8103dae` to `b985ce9` (D-114, D-115) | | 4 | Server gap pass; infra overhaul; infra security | none (orchestrator briefs) | M2c-pi bundle, security review | done (`8660b7f`, D-113; `2fac1ec` to `e70fe0d`; `7fb86f5`, D-117); server overhaul `8103dae` to `b985ce9` (D-114, D-115); Android overhaul `e4e6e5d` to `c582363` (D-118) |
| 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 | bundle done (`2476667`, D-103 to D-106); the owner's run on the Pi remains | | 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 | bundle done (`2476667`, D-103 to D-106); the owner's run on the Pi remains |
| 6 | M10 First release | `M10.md` | M2c-pi, M3, M4, M8 | none | | 6 | M10 First release | `M10.md` | M2c-pi, M3, M4, M8 | none |
| 7 | M11 Import history | `M11.md` | M9, M10 | none | | 7 | M11 Import history | `M11.md` | M9, M10 | none |