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
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
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`).
5. SPEC §11.2 (row M12b), §11.3 (note M12), §14.7 (row M12b).
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).
## 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).
- 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).
- 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).
- 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
## 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
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
- `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).
- 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).
- `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.
- 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
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
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.
4. `:core:model` changes and `ContractTest`; build.
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`
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
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`
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
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).
- During development the debug build pairs against the server on the Mac over the home Wi-Fi (D-001); development data on that server is disposable.
- Application ids `se.hagstedt.kvitt.debug` (debug), `se.hagstedt.kvitt` (release) (CLAUDE.md).
- M5's contract requests, landed with SPEC v0.5 (§11.3 note M8, D-041): `:app` provides `AppBuildInfo(versionCode, versionName, debug)` to `:data` and includes `DataBindingsModule` (`@DisableInstallInCheck`) in the release graph (done pre-M8 in `e6ad858`, with `AppBuildInfo` from `BuildConfig`); debug binds the fakes through the temporary `android/app/src/debug/.../DebugFakesModule.kt` (`72f21b2`), which M8 deletes so both variants bind `:data`; WorkManager is configured with `KvittWorkerFactory` and the default `WorkManagerInitializer` disabled in the manifest; `:app` calls `SyncTriggers.onAppStart`, `SyncTriggers.onForeground` and `SyncTriggers.syncForPush` (push handling, 20 s wait), and `UpdateRepository.check` on cold start and every resume; the `ImageLoader` provided by `:data` is set as Coil's singleton.
- Open follow-up (D-027): on the first sync attempt after a `clock_skew` rejection the phone re-stamps `updated_at` of each such pending row to the current clock before pushing. Still not in `:data` at `3a1bd14`; since SPEC v0.11 it is on the M12e contract-pass list (§11.3), not M8's.
- 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 `b24c8b5`; since SPEC v0.11 it is on the M12e contract-pass list (§11.3), not M8's.
## 4. Owned paths
From SPEC §14.7, verbatim: `android/adapter/fcm/**`, `android/app/**` (after the M2b shell) (owner M8; after landing: "later integration plans"). Catalog entries and module includes may be added (§14.7, `android/gradle/**` row).
+8 -3
View File
@@ -1,6 +1,6 @@
# 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.
@@ -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 |
| `: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` |
| `: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` |
@@ -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` |
| `: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.
@@ -1206,6 +1206,9 @@ history rewrite and first push (18c527f) ─► infra overhaul (2fac1ec..e70fe0d
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)
└─► 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.
@@ -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 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).
* **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.
### 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.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.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.
## 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.
- 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 (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.
- `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); 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, `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):
- 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.
@@ -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.
- 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.
- 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).
- 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.
- 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`.
- 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.
- 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.
- 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 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)
- 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.
- 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).
- Tooling request (no plan yet): `cargo-audit` in the server gate.
- Android overhaul (agent, running).
- `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).
- 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).
- 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.
- Restore drill AC-18.2 (`docs/validation/restore-drill.md`, M10; the one Missing row of the AC-coverage audit).
- AC-18.1, AC-20.1 and AC-16.3 shown on the Pi and the phones (M2c-pi run, M10).
- A `KvittWorkerFactory.createWorker` test (Android agent, no plan yet).
- 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).
- 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).
- 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).
- 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`:
- 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.
- 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).
- 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, the server overhaul and the Android overhaul: landed.
- 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)
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).
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.
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)
- 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).
- 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`.
- §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)
- Nothing.
## 6. Numbering
- Decisions: D-001 to D-117 used, except D-008 and D-009 (never used); none reserved. Next free: **D-118**.
- Spec version: v0.18 written; next **v0.19**.
- 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.
- Decisions: D-001 to D-118 used, except D-008 and D-009 (never used); none reserved. Next free: **D-119**.
- 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.19.
- Sync protocol **1**, push payload **1**.
## 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-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-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 |
| `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 |
| `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 |
### 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.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.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` |
`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` |
| 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) |
| 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 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` |
+2 -2
View File
@@ -2,7 +2,7 @@
One ready-to-paste start prompt per plan (`PLAN-<id>.md` at the repository root), plus `ORCHESTRATOR-prompt.md`. Before pasting, the orchestrator fills `{{NEXT_DECISION}}`, `{{SPEC_VERSION}}`, `{{SCHEMA_VERSION}}` (disjoint ranges for agents that run in parallel, CLAUDE.md › Numbering) and `{{ORCHESTRATOR_NOTES}}` (answers to the plan's open points, phone assignment, overrides). `U2.md` also takes `{{OWNER_PICK_AND_COMMENTS}}`. U1 and U2 run in the owner's own Claude session in `~/orca/workspaces/kvitt/UX-sprint` on branch `UX-sprint`. The numbers are for proposed entries in agents' reports: implementation agents never edit `SPEC.md` or `docs/decisions.md`; a docs agent records what the orchestrator accepts (CLAUDE.md › Orchestration).
Dependencies follow SPEC §11.1 and §11.2 (v0.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
@@ -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 | 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 | 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 |
| 6 | M10 First release | `M10.md` | M2c-pi, M3, M4, M8 | none |
| 7 | M11 Import history | `M11.md` | M9, M10 | none |