SPEC v0.9: M12b and M12c outcomes (D-055 to D-058), U2 values, UX-9 (D-059, M12d)
- D-055 build-time copy of the keyword table, D-056 parseCategoryKeywords, D-057 enum declaration orders, D-058 E-7 example shows server_version 15 and 16 and its test compares them again. - D-051 fulfilled: §9.7 hex values from docs/ux/design-spec.md §2.3; design-spec.md binding, strings.csv the source of string resources. - D-059: ExpenseEditorOptions.frequentCategoryIds and CategoryRepository.deletable; new milestone M12d with PLAN-M12d and prompts/M12d.md; M7a-f depend on M6 and M12d. - suggestCategory readings (null when other is archived), §11.2 landed marks and the 2026-10-18 dry run, dev stack on schema 2, PLAN-M12b SQLite 3.51.x, design-spec §6 notes in PLAN-M6/M7c/M7f; HANDOFF rewritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+1
-1
@@ -15,7 +15,7 @@ Bring the landed server and importer to the category contract v2 (D-053, D-054):
|
||||
|
||||
## 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 SQLite 3.50 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 (`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.
|
||||
- `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.
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ Bring the landed Android contract and data layer to the category contract v2 (D-
|
||||
- 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).
|
||||
- `: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 the Java resource `android/data/src/main/resources/category-keywords.csv`, byte-identical to `contracts/category-keywords.csv`, checked by a test; 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 `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 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`).
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# PLAN-M12d: UI contract additions (UX-9)
|
||||
|
||||
## 1. Goal
|
||||
Add the two UI-contract members that U2 found the design needs (UX-9, D-059) to `:core:model`, implement them in `:data` from Room and in `:core:fakes` from each scenario's own data: `ExpenseEditorOptions.frequentCategoryIds` (S-7's and S-19's `CategoryQuickRow`) and `CategoryRepository.deletable(id)` (S-15's category editor). No server, sync or Room schema change. Runs in parallel with M6; M7a to M7f start after both M6 and this plan have landed, and M7c and M7f consume the two members.
|
||||
|
||||
## 2. Read first
|
||||
1. SPEC §14.2.4: `ExpenseEditorOptions`, `ExpenseRepository`, `CategoryRepository`, and "Changes in v0.9" (the readings of both members, the UI text choice, the fallbacks), all of it.
|
||||
2. SPEC AC-7.2 (`sort_order`), AC-7.5, AC-7.6 (deletability), AC-8.6, §9.11 CS-1 (the suggestion is not this plan's), §6.2 (layering, fakes), §14.2.1 (mirrored tables), §14.8 (the change rule).
|
||||
3. SPEC §11.2 (row M12d), §11.3 (note M12d), §14.7 (row D-059).
|
||||
4. `docs/decisions.md` D-018 (empty scenario), D-054, D-059.
|
||||
5. `docs/ux/proposed-decisions.md` UX-9 and `docs/ux/design-spec.md` §3.7 (`CategoryQuickRow`), §4 S-15, §6 open points 1 and 2 (for context; the spec text wins).
|
||||
6. Files: `android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Expenses.kt` and `Settings.kt`, `android/core/model/src/test/.../ContractTest.kt`; `android/data/src/main/kotlin/se/hagstedt/kvitt/data/repo/ExpenseRepositoryImpl.kt`, `SettingsRepositories.kt`, the DAOs and the `:data` test harness; `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/FakeExpenseRepository.kt`, `FakeSettingsRepositories.kt`, `FakeRepositories.kt`, `FakeScenarios.kt`; `android/README.md`.
|
||||
7. `CLAUDE.md` (Android gate, layering, no mocking libraries, no silencing).
|
||||
|
||||
## 3. Already established
|
||||
- `ExpenseEditorOptions` gains `val frequentCategoryIds: Map<AccountId, List<CategoryId>>` as its last parameter; `CategoryRepository` gains `fun deletable(id: CategoryId): Flow<Boolean>` (§14.2.4, D-059). Nothing else in `:core:model` changes.
|
||||
- `frequentCategoryIds`: one entry per active account; the active (non-archived, non-deleted) categories ordered by the number of non-deleted expenses in that account that use them, descending; ties by `sort_order` ascending; at most 5; categories with no use in the account are left out (the list may be shorter or empty). Counted over every expense in Room (both users, pending, imported and recurring included); not a per-phone history. The `editorOptions()` flow re-emits when expenses or categories change (§14.2.4).
|
||||
- `deletable(id)`: false for a default category (`defaultKey != null`) or a category used by a non-deleted expense or a non-deleted recurring template; true otherwise. Computed from Room; the server stays the authority (`delete` can still return `Failed("category_in_use")` or `Failed("category_is_default")`) (AC-7.6, §14.2.4). An unknown or deleted id: emit false.
|
||||
- The UI rules (filling the row to five, the fifth place for the selected category, the three delete-blocked texts) are M7c's and M7f's, not this plan's.
|
||||
- Fakes: every scenario computes both members from its own data, with the same rules (the empty scenario of D-018 has no expenses, so every list is empty and every default category is not deletable).
|
||||
- No Room migration (Room schema stays 1); no new DAO table; queries over existing tables only.
|
||||
|
||||
## 4. Owned paths
|
||||
From SPEC §14.7 (row D-059, a contract-change plan): `android/core/model/**` (`ExpenseEditorOptions`, `CategoryRepository`, `ContractTest`), `android/data/**` (both members and their tests), `android/core/fakes/**` (both members, outside the `fakes/<feature>/` folders that M7x own).
|
||||
|
||||
Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. M6 runs at the same time in `android/core/design/**`; do not touch it.
|
||||
|
||||
## 5. Deliverables
|
||||
- `:core:model`: the two members; `ContractTest` updated.
|
||||
- `:data`: `frequentCategoryIds` in the expense repository's `editorOptions()`; `deletable` in the category repository; repository tests over Room: ordering by count, tie by `sort_order`, cap at 5, unused categories left out, archived and deleted categories left out, deleted expenses not counted, pending and both users' expenses counted, one entry per active account and none for an archived one, re-emission after a new expense; `deletable` false for a default, for one used by an expense, for one used only by a recurring template, true for an unused custom one and after the last using expense is deleted, false for an unknown id.
|
||||
- `:core:fakes`: both members in every fake repository, computed from the scenario data, with a test over the empty scenario and one scenario with expenses.
|
||||
- Every existing caller or test that constructs `ExpenseEditorOptions` updated, each listed in the report.
|
||||
|
||||
## 6. Steps
|
||||
1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m12d -b agent-m12d main`, then in `.claude/worktrees/agent-m12d`: `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 SPEC v0.9 is on `main` (§14.2.4 "Changes in v0.9"); 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. `:core:fakes`: both members and their test.
|
||||
6. `:data`: queries, both members, repository tests.
|
||||
7. Run the gate (§7). Commit on `agent-m12d`, `git rebase main` (M6 may have landed meanwhile; paths are disjoint), rerun the gate. Never merge, push or remove the worktree.
|
||||
|
||||
## 7. Gate
|
||||
From SPEC §11.2, verbatim: "Android gate".
|
||||
Command, from `android/`: `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check` (Android agents share one Gradle build at a time; `gradle-serial.sh` queues behind M6).
|
||||
Green means: `BUILD SUCCESSFUL`; test counts from fresh XML show 0 failures and include the new repository and fake tests.
|
||||
|
||||
## 8. HUMAN steps
|
||||
From SPEC §11.2, verbatim: "none".
|
||||
|
||||
## 9. Out of scope
|
||||
- Any UI: `CategoryQuickRow` (M6), the S-7 row filling (M7c), the S-15 delete texts (M7f).
|
||||
- The category suggestion (§9.11, landed with M12c); the server; `contracts/**`; a Room migration.
|
||||
- Fake scenarios for feature states (M7x, under `fakes/<feature>/`).
|
||||
|
||||
## 10. Report
|
||||
- Branch `agent-m12d`, head commit hash after rebasing on `main`.
|
||||
- Gate: BUILD line, actionable tasks, test counts from fresh XML.
|
||||
- Dependency-report lines.
|
||||
- Callers and tests changed for the new `ExpenseEditorOptions` parameter, each with its reason.
|
||||
- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences.
|
||||
- Contract requests, open questions, spec gaps (with section numbers).
|
||||
|
||||
## 11. Numbering
|
||||
Decision ids, spec version and schema version are assigned by the orchestrator in the start prompt; use `TODO(spec)` if none was given. Room schema stays 1 in this plan (next would be 2).
|
||||
@@ -27,6 +27,8 @@ Implement `:core:design` from the approved `docs/ux/design-spec.md`: theme, colo
|
||||
- M2b created the module shell (build file, manifest, empty string files); M6 owns it and fills it in (D-004).
|
||||
- Screenshot goldens are committed; open every changed PNG before committing (CLAUDE.md).
|
||||
- `:core:design` depends only on `:core:model` (§6.2).
|
||||
- Design-spec open points (`docs/ux/design-spec.md` §6, SPEC v0.9): (1) **System font on One UI**: `FontFamily.Default` may resolve to a One UI font instead of Roboto; check on the owner's S26 that amounts stay tabular (tabular figures) and weights 500, 600 and 700 are distinct, and report it (D-020 stands either way). (2) **Material 3 Expressive API status**: check every Expressive API of design-spec §3.1 against the Material 3 release pinned in `android/gradle/libs.versions.toml`; any that is not usable (not in the pinned release, or experimental without a stable path) takes its listed stable fallback without a design change; list each with its outcome in the report. (3) S-19 has no category suggestion (CS-1); `CategoryQuickRow` must work without one.
|
||||
- The §9.7 hex values are in SPEC v0.9 (from design-spec §2.3; D-051 fulfilled); `design-spec.md` §2 and §3 and `strings.csv` are the sources (§13.4). M6 started 2026-10-18. `CategoryQuickRow` takes its list from the caller: `ExpenseEditorOptions.frequentCategoryIds` (D-059) is added by M12d in parallel; the component must not depend on it existing.
|
||||
- Avatars (v0.5, §14.2.4 readings): `:data` always loads pictures at size 512 (E-11); the 128 variant is unused. If the avatar component needs the small size, ask the orchestrator for a size parameter (a contract request on `:core:model` and `:data`).
|
||||
|
||||
## 4. Owned paths
|
||||
|
||||
@@ -31,6 +31,8 @@ Build `:feature:expense`: the full-screen expense editor (S-7) and the expense d
|
||||
- Defaults: account = last used on this phone if active else oldest active; category for a new expense = the suggestion of §9.11 (D-049): the editor keeps a "picked by hand" flag (CS-2) and, while it is false, calls `ExpenseRepository.suggestCategory(accountId, description)` on every description and account change (CS-3), showing the result with no marker (CS-7); before any description it is `ExpenseEditorOptions.defaultCategoryFor` (last used in that account if active else `other`); editing never changes the category (CS-1); split mode `default`; payer = current user; date = today in the phone's zone (AC-8.3, AC-8.4, AC-8.6). Defaults come from `ExpenseEditorOptions`.
|
||||
- Split modes `default` ("Account default 60/40"), `ratio`, `exact` (type one share, the other is amount minus it, read-only; a share larger than the amount blocks saving inline), `full` (one bearer); changing account in `default` re-copies the ratio; live preview from `ExpenseRepository.preview` (AC-8.5, AC-8.7, AC-8.8, §13.3 S-7).
|
||||
- Validation errors come back as `SaveResult.Invalid` with §14.5.3 tokens, `required`, or `validation_failed` for a §9.6 limit without a domain token (lengths, dates) (§9.6, §14.2.4); deleting asks for confirmation (AC-8.9).
|
||||
- Category quick row (D-059, SPEC §14.2.4 "Changes in v0.9"): `ExpenseEditorOptions.frequentCategoryIds[accountId]`, read once on open and on an account change, filled up to five from the remaining active categories in `sort_order`; the selected category (suggested, defaulted or picked in "Alla") takes the fifth place when it is not among them. M12d must have landed (§11.2). When the suggestion returns null (`other` archived and no active last-used category), the editor shows no category until the user picks one, and saving is blocked as for a missing category (AC-8.6).
|
||||
- Design-spec open points (`docs/ux/design-spec.md` §6) carried here: tabular figures for amounts as M6 delivers them (M6 checks them on the S26); Material 3 Expressive components only as M6 provides them, with M6's stable fallbacks; the recurring editor S-19 (M7f) has no category suggestion, so nothing in this feature is shared with it beyond the `:core:design` editor fields.
|
||||
- Detail shows amount, description, category, account, date, payer, shares and mode, notes, "Recurring: <template>" when generated, and the timeline: newest last, one line per entry, field names in the UI language, ids resolved, pending line "Your change is waiting to sync"; a deleted state when opened from a deletion notification (AC-8.11, AC-12.4, AC-12.5, §13.3 S-8).
|
||||
|
||||
## 4. Owned paths
|
||||
|
||||
@@ -29,6 +29,9 @@ Build `:feature:settings`: the settings hub (S-13), profile (S-14), categories (
|
||||
- Hub rows: Profile, Categories, Recurring expenses, Devices, Invite partner or Pair a device (admin only), Sync status (opens S-16 via `:app`), About; own and partner's avatar and name (§13.3 S-13, §8.4).
|
||||
- Profile: own name and picture only, from the Android photo picker, online only (AC-4.2, AC-4.3, §9.9).
|
||||
- Categories: list with icon, colour, name and an archived section; editor with "Reset name" for defaults, the 33 icons and 14 colour swatches of §9.7 (D-054); reorder by drag; delete blocked with the reason (`category_in_use`, `category_is_default`), archive offered instead; all online only (FR-7, §13.3 S-15).
|
||||
- Category editor delete (D-059, SPEC §14.2.4 "Changes in v0.9"): "Ta bort kategori" is disabled with its reason before any tap, from `CategoryRepository.deletable(id)` and `defaultKey`: default → `settings_category_delete_blocked_default_short`; in use and archived → `settings_category_delete_blocked_in_use_archived`; in use and active → `settings_category_delete_blocked_in_use`; a `Failed("category_in_use")` or `Failed("category_is_default")` after a tap still shows the same reason inline. M12d must have landed (§11.2).
|
||||
- Recurring editor S-19 has **no category suggestion** (§9.11 CS-1, design-spec §6 point 3): its default category is the AC-8.6-style default (last category of the account, else Övrigt); its `CategoryQuickRow` uses `ExpenseEditorOptions.frequentCategoryIds` like S-7 (D-059).
|
||||
- Design-spec open points (`docs/ux/design-spec.md` §6) carried here: tabular figures as M6 delivers them (checked by M6 on the S26); Material 3 Expressive components only as M6 provides them, with M6's stable fallbacks.
|
||||
- Recurring: list with description, amount, account, "Every month on day 25", next date, paused badge; editor with the expense fields of S-7 except date, plus day of month, first month (default per AC-9.8, up to 12 months back), optional last month, pause switch; online only (FR-9, §13.3 S-19).
|
||||
- Every settings write is online: a refused value comes back as `OnlineResult.Failed` with the §14.3.2 code and `field` (for example `validation_failed` on `name`), shown inline at that field (§9.6, §14.2.4, §14.3.2).
|
||||
- Devices: rows per AC-3.1 ("This device" marked); revoke with confirmation; the current device cannot be revoked; admin sees all, member own (AC-3.1, AC-3.2).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Kvitt: specification
|
||||
|
||||
Version **0.8** (2026-10-18). Schema version **2**. Sync protocol version **1**. Push payload version **1**.
|
||||
Version **0.9** (2026-10-18). Schema version **2**. Sync protocol version **1**. Push payload version **1**.
|
||||
|
||||
This file is the single source of truth for Kvitt. It is written for implementation agents that have no other context. If code, a plan or `CLAUDE.md` disagrees with this file, this file is right. Words in `code` are literal identifiers, paths, enum values or error codes and must be used exactly as written. Anything not decided here is listed in §12 as `TODO(spec)`; do not invent a value for it.
|
||||
|
||||
@@ -197,7 +197,7 @@ The server starts with an empty database. The household is created from the comm
|
||||
* AC-8.3 The payer is either user. The editor defaults the payer to the current user.
|
||||
* AC-8.4 The date is any date within §9.6 limits, past or future. It defaults to today in the phone's time zone. This date is the only date used in analytics and lists.
|
||||
* AC-8.5 Split modes (enum §9.1): `default` (the account's default ratio, copied into the expense when saved), `ratio` (a custom ratio), `exact` (exact öre per user), `full` (the whole amount borne by one chosen user, who may be the payer or not). Share computation per §9.1.
|
||||
* AC-8.6 The editor defaults: account = the account last used on this phone if still active, else the oldest active account; category = the category suggestion of §9.11 for a new expense, whose last step is the category last used in that account on this phone if still active, else `other` (D-049); editing an existing expense keeps its category; split mode `default`.
|
||||
* AC-8.6 The editor defaults: account = the account last used on this phone if still active, else the oldest active account; category = the category suggestion of §9.11 for a new expense, whose last step is the category last used in that account on this phone if still active, else `other` (D-049); when `other` is archived too and no active last-used category exists, the editor shows no category until the user picks one (v0.9); editing an existing expense keeps its category; split mode `default`.
|
||||
* AC-8.7 Exact mode: the user types one user's share; the other user's share is the amount minus it and is shown read-only. A typed share larger than the amount blocks saving with an inline error.
|
||||
* AC-8.8 Changing the account of an expense in mode `default` re-copies the new account's ratio. In other modes the split is kept.
|
||||
* AC-8.9 Deleting asks for confirmation and is a soft delete. Deleted expenses disappear from lists, balances and analytics on both phones after sync.
|
||||
@@ -749,24 +749,24 @@ Category icon tokens are Material Symbols names. The allowed set (exhaustive, 33
|
||||
|
||||
`shopping_cart`, `restaurant`, `local_cafe`, `home`, `bolt`, `water_drop`, `wifi`, `directions_car`, `train`, `local_gas_station`, `flight`, `hotel`, `luggage`, `movie`, `music_note`, `sports_esports`, `fitness_center`, `medical_services`, `medication`, `shopping_bag`, `checkroom`, `pets`, `child_care`, `school`, `card_giftcard`, `celebration`, `build`, `chair`, `subscriptions`, `category`, `key`, `shield`, `account_balance`.
|
||||
|
||||
Colour tokens (exhaustive, 14 tokens; `slate` and `amber` added by D-054) with provisional values. The direction is U1's "Kalas" with its muted palette (D-047); U2 replaces these hex values with the muted Kalas values and must keep the token names (D-051). `slate` and `amber` have no value until U2 reports (`TODO(U2)`); M6 starts only after the values have landed here (§11.1).
|
||||
Colour tokens (exhaustive, 14 tokens; `slate` and `amber` added by D-054) with the muted Kalas values of `docs/ux/design-spec.md` §2.3 (D-047, D-051; recorded in v0.9). The table holds the base colour of each token; `design-spec.md` §2.3 also gives each token's container (the colour over `surface` at 18 % in light, 24 % in dark) and the contrast ratios, all at least 3:1 as graphics. Category colours are never used for text.
|
||||
|
||||
| Token | Light | Dark |
|
||||
|---|---|---|
|
||||
| `green` | `#2E7D32` | `#81C784` |
|
||||
| `orange` | `#E65100` | `#FFB74D` |
|
||||
| `brown` | `#6D4C41` | `#BCAAA4` |
|
||||
| `yellow` | `#F9A825` | `#FFF176` |
|
||||
| `blue` | `#1565C0` | `#90CAF9` |
|
||||
| `teal` | `#00796B` | `#80CBC4` |
|
||||
| `purple` | `#6A1B9A` | `#CE93D8` |
|
||||
| `red` | `#C62828` | `#EF9A9A` |
|
||||
| `pink` | `#AD1457` | `#F48FB1` |
|
||||
| `grey` | `#546E7A` | `#B0BEC5` |
|
||||
| `indigo` | `#283593` | `#9FA8DA` |
|
||||
| `lime` | `#827717` | `#DCE775` |
|
||||
| `slate` | `TODO(U2)` | `TODO(U2)` |
|
||||
| `amber` | `TODO(U2)` | `TODO(U2)` |
|
||||
| `green` | `#4E7A55` | `#8FB896` |
|
||||
| `orange` | `#B26A3E` | `#E8AA7E` |
|
||||
| `brown` | `#6B5244` | `#B8A090` |
|
||||
| `yellow` | `#9C7C2A` | `#E6C67A` |
|
||||
| `blue` | `#4A78A8` | `#98C0E6` |
|
||||
| `teal` | `#367F82` | `#8CCAC8` |
|
||||
| `purple` | `#865891` | `#C59CDA` |
|
||||
| `red` | `#A84B45` | `#DC8F86` |
|
||||
| `pink` | `#AE5D7C` | `#EFB0BE` |
|
||||
| `grey` | `#6C7177` | `#B0B2B3` |
|
||||
| `indigo` | `#45508D` | `#969DD8` |
|
||||
| `lime` | `#73822F` | `#B9CC86` |
|
||||
| `slate` | `#5E2D4E` | `#B0719A` |
|
||||
| `amber` | `#937D63` | `#DFCFC0` |
|
||||
|
||||
The server validates icon and colour against these lists (`validation_failed`).
|
||||
|
||||
@@ -829,7 +829,7 @@ Diacritics are kept (å, ä, ö and é stay distinct; the keyword table lists bo
|
||||
1. **History, exact:** among the non-deleted expenses on the phone (every account, either user, pending, imported and recurring ones included) whose normalised description equals the normalised typed description, the category used most often; on a tie, the one used most recently.
|
||||
2. **History, first token:** among the non-deleted expenses whose first token equals the typed description's first token, the category used most often; on a tie, the one used most recently.
|
||||
3. **Keywords:** the category of the winning entry of the keyword table (CS-6).
|
||||
4. **Default:** the AC-8.6 fallback: the category last used in the selected account on this phone if still active, else `other`.
|
||||
4. **Default:** the AC-8.6 fallback: the category last used in the selected account on this phone if still active, else `other`; when `other` is archived too, no category (AC-8.6).
|
||||
|
||||
"Used most often" counts the matching expenses per category. "Most recently" compares the latest matching expense of each tied category by (`expense_date`, then `created_at`), the expense-list order of AC-8.10; if that still ties, the lower category id (string order) wins. Archived and deleted categories are left out of the count; when no category is left, the next step applies (owner, 2026-10-18: most often, not most recent).
|
||||
|
||||
@@ -851,7 +851,7 @@ Evidence (D-049): a chronological replay of the household's 1 775 Splitwise expe
|
||||
|
||||
All infrastructure files live in `infra/` (ownership §14.7). Secrets live in `infra/secrets/` on the Mac (git-ignored) and in `/opt/kvitt/secrets/` on the Pi; neither is ever committed.
|
||||
|
||||
**Development first (D-001).** During development the server runs locally on the owner's M4 Mac (Docker compose from `infra/dev/compose.yaml`, or plain `cargo run` in `server/`), and debug builds pair against it over the home Wi-Fi (debug builds accept `http://` to private LAN addresses, AC-2.8). The development stack (built by M2c-dev, landed `c885aba`, D-022) has the `server` service and the `backup` job runnable on demand without the Drive upload; it has no Caddy and no TLS (plain `http://`, which debug builds accept on private LAN addresses). Its data is a bind-mounted folder on the Mac (`infra/dev/data/`, git-ignored, and `infra/dev/backups/` for local snapshots); it mounts the whole `infra/secrets/` folder read-only at `/secrets` instead of a single key file (a missing `fcm-service-account.json` means push is disabled, §10.4); its env file is `infra/dev/.env.example`; its `server` healthcheck is `kvitt-server healthcheck`; its `backup` runs as uid 10001 so that it can open the WAL files the server owns. The Pi's env file stays `infra/env.example`, written by M2c-pi. Firebase (§10.7) is set up during development because push is needed then. Everything in this section that concerns the Pi (the Pi hosts and router of §10.1, §10.2, the registry and Pi deploy of §10.3, §10.5 TLS, publishing to the Pi in §10.6, the Drive upload of §10.9) is built by milestone M2c-pi, which runs after M8 and before M10.
|
||||
**Development first (D-001).** During development the server runs locally on the owner's M4 Mac (Docker compose from `infra/dev/compose.yaml`, or plain `cargo run` in `server/`), and debug builds pair against it over the home Wi-Fi (debug builds accept `http://` to private LAN addresses, AC-2.8). The development stack (built by M2c-dev, landed `c885aba`, D-022) has the `server` service and the `backup` job runnable on demand without the Drive upload; it has no Caddy and no TLS (plain `http://`, which debug builds accept on private LAN addresses). Its data is a bind-mounted folder on the Mac (`infra/dev/data/`, git-ignored, and `infra/dev/backups/` for local snapshots); it mounts the whole `infra/secrets/` folder read-only at `/secrets` instead of a single key file (a missing `fcm-service-account.json` means push is disabled, §10.4); its env file is `infra/dev/.env.example`; its `server` healthcheck is `kvitt-server healthcheck`; its `backup` runs as uid 10001 so that it can open the WAL files the server owns. The Pi's env file stays `infra/env.example`, written by M2c-pi. The development stack's database was migrated to schema 2 (migration 0002, §14.1.1) on 2026-10-18 after a backup. Firebase (§10.7) is set up during development because push is needed then. Everything in this section that concerns the Pi (the Pi hosts and router of §10.1, §10.2, the registry and Pi deploy of §10.3, §10.5 TLS, publishing to the Pi in §10.6, the Drive upload of §10.9) is built by milestone M2c-pi, which runs after M8 and before M10.
|
||||
|
||||
### 10.1 Hosts and network
|
||||
|
||||
@@ -1126,19 +1126,20 @@ M0 spec ──┬─► U1 directions ─(owner picks)─► U2 design spec ─
|
||||
M11 import real history ◄── M9, M10
|
||||
|
||||
Category contract v2 (D-054), after SPEC v0.8:
|
||||
M12a contracts + :domain suggestCategory (landed 6b60ef7) ─┬─► M12b server (migration 0002, importer table)
|
||||
└─► M12c android (:core:model, :domain, :data, fakes) ─► M6, M7c
|
||||
M12a contracts + :domain suggestCategory (landed 6b60ef7) ─┬─► M12b server (migration 0002, importer table; landed 2e2ebe7)
|
||||
└─► M12c android (:core:model, :domain, :data, fakes; landed e499107) ─► M6, M7c
|
||||
M12d UI contract additions (D-059: frequentCategoryIds, deletable) ─► M7a..M7f (runs in parallel with M6)
|
||||
```
|
||||
|
||||
After M1, M2a and M2b land, these run in parallel without touching each other's files (§14.7): M3, M4 (lands after M3, D-015), M5, M2c-dev, M6 (once U2 is approved and the §9.7 colour change from U2's proposal has landed in this spec), M7a to M7f (once M6 lands), M9 (once M4 lands). M2c-pi runs after M8 and before M10 (D-001). Group M12 "Category contract v2" (D-049, D-054): M12a first; then M12b and M12c in parallel; M6 and M7c wait for M12c.
|
||||
After M1, M2a and M2b land, these run in parallel without touching each other's files (§14.7): M3, M4 (lands after M3, D-015), M5, M2c-dev, M6 (once U2 is approved and the §9.7 colour change from U2's proposal has landed in this spec), M7a to M7f (once M6 lands), M9 (once M4 lands). M2c-pi runs after M8 and before M10 (D-001). Group M12 "Category contract v2" (D-049, D-054): M12a first; then M12b and M12c in parallel; M6 and M7c wait for M12c (all three landed: `6b60ef7`, `2e2ebe7`, `e499107`). M12d (D-059) runs in parallel with M6; M7a to M7f wait for both.
|
||||
|
||||
### 11.2 Milestone table
|
||||
|
||||
| Id | Name | Deliverable | Gate | HUMAN | Depends on |
|
||||
|---|---|---|---|---|---|
|
||||
| M0 | Spec v0.1 | `SPEC.md`, filled `CLAUDE.md`, repository bootstrap | none (docs only) | Review the spec; answer §12 | none |
|
||||
| U1 | UX directions | §13.4 phase 1 files in `docs/ux/` | Files exist and open; each direction shows the six phase-1 screens of §13.4 (S-4, S-7, S-5, S-9, S-11, S-8) in light and dark; Swedish copy, no placeholder text | Review 2-3 directions, pick one, give comments | M0 |
|
||||
| U2 | UX design spec | §13.4 phase 2 files in `docs/ux/` | Every string key has `en` and `sv`; every screen has every state that `design-spec.md` lists for it (§13.3) | Approve the design spec | U1 and the owner's pick |
|
||||
| U1 | UX directions | Landed `16c4d01` (direction 3 "Kalas" muted picked, D-047). §13.4 phase 1 files in `docs/ux/` | Files exist and open; each direction shows the six phase-1 screens of §13.4 (S-4, S-7, S-5, S-9, S-11, S-8) in light and dark; Swedish copy, no placeholder text | Review 2-3 directions, pick one, give comments | M0 |
|
||||
| U2 | UX design spec | Landed `16c4d01`, approved by the owner: `docs/ux/design-spec.md` (binding, §13.4), `strings.csv` (558 rows), `screens/`; UX-9 (D-059). §13.4 phase 2 files in `docs/ux/` | Every string key has `en` and `sv`; every screen has every state that `design-spec.md` lists for it (§13.3) | Approve the design spec | U1 and the owner's pick |
|
||||
| M1 | Contracts and vectors | `contracts/openapi.yaml`, `contracts/schema/server-v1.sql`, `contracts/test-vectors/*.json`, `contracts/examples/**/*.json`, `tools/contracts-check.py` | `python3 tools/contracts-check.py` passes: all JSON parses, every vector file matches §14.5.5, a stdlib-only Python reference of §9.1, §9.3, §9.4 reproduces every expected value, every example validates against the shapes of §14.3 by key set | none | M0 |
|
||||
| M2a | Server skeleton and `kvitt-domain` | Cargo workspace, three crates, config, DB pool, migration `0001_init.sql` byte-identical to `contracts/schema/server-v1.sql`, error type with every §14.3.2 code, auth extractor (§8.2), all routes of §7 registered with handlers returning 501 `not_implemented`, utoipa document equal to `contracts/openapi.yaml`, `GET /health` and `GET /setup/status` implemented, `kvitt-domain` passing all vectors plus `next_occurrence_date`, `server/Dockerfile`; shell files in M3, M4 and M9 paths (D-004); the seams of §6.3 (D-015) | Server gate (includes the OpenAPI match test, the migration identity test, the vector tests) | none | M1 |
|
||||
| M2b | Android skeleton and `:domain` | Gradle project in `android/` with every module of §6.2, version catalog, architecture check, `:core:model` types and repository interfaces of §14.2.4, `:domain` passing all vectors (including analytics), `:core:fakes` with the empty scenario (§6.2, D-018) for each repository, the Room 3 entities of §14.2.1 and §14.2.2 and `KvittDatabase` version 1 in `:data` with the exported schema (D-012), screenshot tests with Roborazzi (D-010), module shells for every module with one empty entry composable per feature (D-004), the feature convention of §6.2 in `build-logic` (D-017), `:app` launching to an empty themed screen, `:app` string files with the app name only in both languages, lint `MissingTranslation` as an error in every module | Android gate | Plug the phone in once to confirm `installDebug` works | M1 |
|
||||
@@ -1148,19 +1149,20 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
||||
| M4 | Server sync, audit, push, scheduler | Landed `bd6f10a` (D-035 to D-039). E-31, E-32, the real `AuditWriter` (writer and diff) and `PushQueue` (FCM sender), `src/tasks/**` starting them, `sync_mutations` pruning daily at 04:00 (D-021), scheduler, `rotate-epoch` | Server gate; integration tests for every sync rule of §14.4 and the scheduler cases of §9.5 (run at start, daily 06:00 on DST days, catch-up through M3's generation function); FCM sender tested against a local stub HTTP server | none | M2a; lands after M3 (D-015) |
|
||||
| M5 | Android data layer | Landed `c28c52b` (D-026 to D-029, D-040 to D-043). DAOs on M2b's Room 3 entities and `KvittDatabase` v1 (§14.2, D-012; any change to the entities or the exported schema is a contract change, §14.8), Ktor client and DTOs, token store, sync engine and workers, real repositories, APK updater | Android gate; DTO round-trip tests over every file in `contracts/examples/`; sync engine tests with Ktor `MockEngine` covering §14.4 including the 14-day offline case (AC-15.6) | none | M1, M2b |
|
||||
| M12a | Category contract v2: contracts | Landed `6b60ef7`. `contracts/category-keywords.csv` (148 entries), `contracts/schema/migrations/0002_categories_v2.sql` (§14.1.1), `contracts/test-vectors/suggest_category.json` (65 cases), E-1 example `schema_version` 2, `tools/contracts-check.py` (keyword-table rules, 0002 loaded on a fresh and an in-use v1 database, Python reference of §9.11, new icon and colour tokens), and `:domain`'s `suggestCategory` with `UnicodeText` passing the vectors (§14.5.6). The OpenAPI `DefaultCategoryKey` extension and the new category examples were held back for M12b | `python3 tools/contracts-check.py` passes | none | M1, SPEC v0.8 |
|
||||
| M12b | Category contract v2: server | Migration `server/crates/kvitt-server/migrations/0002_categories_v2.sql` byte-identical to the contract file with its identity test; `SCHEMA_VERSION` 2; seed test of the fourteen defaults (ids, keys, icons, colours, sort orders, `server_version` from the sequence) on a fresh database and on a v1 database holding expenses (rows and references kept); icon and colour lists of §9.7 in `validate.rs`; `DefaultCategoryKey` with the fourteen keys in `contracts/openapi.yaml` (held back by M12a) and in the utoipa wire types, so the OpenAPI match test passes; the category examples M12a held back; importer table `splitwise_categories.csv` per §10.8 with its spec-parity test; importer schema check 2 (D-054); E-31 and other examples only where they enumerate the defaults | Server gate; `python3 tools/contracts-check.py` passes | none | M12a, M3, M4, M9 |
|
||||
| M12c | Category contract v2: Android | `:core:model`: `DefaultCategoryKey` (14), `ColorToken` (`SLATE`, `AMBER`), `CategoryIcons.ALL` (33), `ExpenseRepository.suggestCategory`; `:data`: the `UnicodeText` binding (`java.text.Normalizer`, `Character`), `suggestCategory` from Room, DataStore and the keyword table bundled as the resource `android/data/src/main/resources/category-keywords.csv` (byte-identical to the contract file, with a test); `:core:fakes`: fourteen defaults in the empty scenario and a fake `suggestCategory`; default-name string resources for the four new categories in English and Swedish (AC-7.2) where default names already live | Android gate | none | M12a, M2b, M5 |
|
||||
| M6 | Design system | `:core:design` theme, tokens, typography (Material 3 default typeface, D-020), shared components including the ledger rows and expense editor fields (D-019), charts, formatters (§9.8), shared strings (including the notification copy) per `docs/ux/design-spec.md` and `docs/ux/strings.csv`, screenshot tests of every component in light and dark, Swedish and English | Android gate | Look at the component screenshots the agent opens and posts (yes/no) | U2, M2b, M12c, the §9.7 colour change |
|
||||
| M7a | Feature: onboarding | `:feature:onboarding` screens S-1, S-2, S-3, S-18 | Android gate with screenshot tests of every state, sv and en, light and dark | none | M6 |
|
||||
| M7b | Feature: home and account | `:feature:home`, `:feature:account` screens S-4, S-5, S-6, S-16, S-17 | as M7a | none | M6 |
|
||||
| M7c | Feature: expense | `:feature:expense` screens S-7, S-8, with the category suggestion of §9.11 (CS-2, CS-3, CS-7) | as M7a | none | M6, M12c |
|
||||
| M7d | Feature: settle | `:feature:settle` screens S-9, S-10 | as M7a | none | M6 |
|
||||
| M7e | Feature: analytics | `:feature:analytics` screens S-11, S-12 | as M7a | none | M6 |
|
||||
| M7f | Feature: settings | `:feature:settings` screens S-13, S-14, S-15, S-19, S-20, S-21, S-22 | as M7a | none | M6 |
|
||||
| M12b | Category contract v2: server | Landed `2e2ebe7` (D-057, D-058). Migration `server/crates/kvitt-server/migrations/0002_categories_v2.sql` byte-identical to the contract file with its identity test; `SCHEMA_VERSION` 2; seed test of the fourteen defaults (ids, keys, icons, colours, sort orders, `server_version` from the sequence) on a fresh database and on a v1 database holding expenses (rows and references kept); icon and colour lists of §9.7 in `validate.rs`; `DefaultCategoryKey` with the fourteen keys in `contracts/openapi.yaml` (held back by M12a) and in the utoipa wire types, so the OpenAPI match test passes; the category examples M12a held back; importer table `splitwise_categories.csv` per §10.8 with its spec-parity test; importer schema check 2 (D-054); E-31 and other examples only where they enumerate the defaults | Server gate; `python3 tools/contracts-check.py` passes | none | M12a, M3, M4, M9 |
|
||||
| M12c | Category contract v2: Android | Landed `e499107` (D-055 to D-057). `:core:model`: `DefaultCategoryKey` (14), `ColorToken` (`SLATE`, `AMBER`), `CategoryIcons.ALL` (33), `ExpenseRepository.suggestCategory`; `:data`: the `UnicodeText` binding (`java.text.Normalizer`, `Character`), `suggestCategory` from Room, DataStore and the keyword table copied from `contracts/category-keywords.csv` at build time (D-055; a test asserts byte identity and the §14.8 rules), parsed by `:domain`'s `parseCategoryKeywords` (D-056); `:core:fakes`: fourteen defaults in the empty scenario and a fake `suggestCategory`; default-name string resources for the four new categories in English and Swedish (AC-7.2) where default names already live | Android gate | none | M12a, M2b, M5 |
|
||||
| M12d | UI contract additions (D-059) | `:core:model`: `ExpenseEditorOptions.frequentCategoryIds` and `CategoryRepository.deletable(id)` (§14.2.4 "Changes in v0.9"); `:data`: both computed from Room, with repository tests; `:core:fakes`: both computed from each scenario's own data; `ContractTest` updated | Android gate | none | M12c, M5 |
|
||||
| M6 | Design system | Started 2026-10-18 from `docs/ux/design-spec.md` §2 and §3 and `docs/ux/strings.csv`. `:core:design` theme, tokens, typography (Material 3 default typeface, D-020), shared components including the ledger rows and expense editor fields (D-019), charts, formatters (§9.8), shared strings (including the notification copy) per `docs/ux/design-spec.md` and `docs/ux/strings.csv`, screenshot tests of every component in light and dark, Swedish and English | Android gate | Look at the component screenshots the agent opens and posts (yes/no) | U2, M2b, M12c, the §9.7 colour change |
|
||||
| M7a | Feature: onboarding | `:feature:onboarding` screens S-1, S-2, S-3, S-18 | Android gate with screenshot tests of every state, sv and en, light and dark | none | M6, M12d |
|
||||
| M7b | Feature: home and account | `:feature:home`, `:feature:account` screens S-4, S-5, S-6, S-16, S-17 | as M7a | none | M6, M12d |
|
||||
| M7c | Feature: expense | `:feature:expense` screens S-7, S-8, with the category suggestion of §9.11 (CS-2, CS-3, CS-7) | as M7a | none | M6, M12c, M12d |
|
||||
| M7d | Feature: settle | `:feature:settle` screens S-9, S-10 | as M7a | none | M6, M12d |
|
||||
| M7e | Feature: analytics | `:feature:analytics` screens S-11, S-12 | as M7a | none | M6, M12d |
|
||||
| M7f | Feature: settings | `:feature:settings` screens S-13, S-14, S-15, S-19, S-20, S-21, S-22 | as M7a | none | M6, M12d |
|
||||
| M8 | App integration | `:app` navigation and deep links, Hilt bindings from interfaces to `:data` and `:domain`, `:adapter:fcm`, notification building (AC-13.5), update banner wiring, revoke wipe, network security config (§8.5), manifest hardening (§8.8), release signing and version properties (§10.6), `google-services.json` copied with `infra/dev/copy-google-services.sh` | Android gate; on the owner's phone against the development server on the Mac (D-001): pair, add an expense offline, reconnect, see it synced | Tap through the pairing and the offline test as relayed by the orchestrator | M3, M4, M5, M7a-M7f, M2c-dev (Firebase, development server) |
|
||||
| M9 | Splitwise importer | Landed `ab298ab` (D-044 to D-046). `kvitt-import` per §10.8 with tests over synthetic CSV fixtures (`crates/kvitt-import/tests/import.rs`), reconciliation against the totals rows, the committed category table | Server gate | Provide the CSV files (kept outside git) when OQ-2 is answered | M2a, M4 |
|
||||
| M10 | First release | Release build signed, published, both phones paired, push working both ways, nightly backup on Drive, restore drill report, memory check | `docs/validation/first-release.md` and `docs/validation/restore-drill.md` complete; server RSS under 100 MB | Create the release keystore with the `keytool` command of §10.6, outside the repository; install the APK on both phones from the landing page; allow "Install unknown apps"; partner pairs with the invite; confirm notifications arrive | M2c-pi, M3, M4, M8 |
|
||||
| M11 | Import history | Rehearsed on 2026-10-18: the real run reconciled on both files in a throwaway database (Vardag 1746 rows, Hemma 37 rows; 21 rows unmapped before D-044, 0 expected after it). Real import run on the Pi per §10.8 "Production run" (D-025), `docs/validation/splitwise-import.md` with per-account totals compared to Splitwise's balances (aggregates only) | Imported balances equal Splitwise's "Total balance" rows per group | Provide the CSVs; confirm the balances on the phone match Splitwise | M9, M10 |
|
||||
| M11 | Import history | Rehearsed on 2026-10-18: the real run reconciled on both files in a throwaway database (Vardag 1746 rows, Hemma 37 rows; 21 rows unmapped before D-044). Dry run of 2026-10-18 with the M12b mapping (D-053): Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled. Real import run on the Pi per §10.8 "Production run" (D-025), `docs/validation/splitwise-import.md` with per-account totals compared to Splitwise's balances (aggregates only) | Imported balances equal Splitwise's "Total balance" rows per group | Provide the CSVs; confirm the balances on the phone match Splitwise | M9, M10 |
|
||||
|
||||
### 11.3 Notes per milestone
|
||||
|
||||
@@ -1175,8 +1177,9 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
||||
* **M10**: the orchestrator relays every HUMAN step in chat. The keystore is created with the command of §10.6 (D-024).
|
||||
* **M2c-pi**: the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive are requested from the owner at the start of that sprint (D-023).
|
||||
* **M9** (landed `ab298ab`) reuses the server's own push validation (`kvitt_server::api::validate`, `rows`) and the `SqlAuditWriter`, so imported rows pass the same checks as synced ones (§14.4.4). Its tests are one binary, `crates/kvitt-import/tests/import.rs`, over synthetic fixtures only; one test checks the §10.8 category table against `data/splitwise_categories.csv`.
|
||||
* **M12a, M12b, M12c** (D-049 to D-054) carry the owner's UX-session decisions into the contracts and the landed code: M3, M5 and M9 were built with ten defaults, so this is a contract change against `main`, not only the spec. M12a (landed `6b60ef7`) wrote `contracts/`, the checker and `:domain`'s `suggestCategory`; M12b and M12c now run in parallel (disjoint paths, §14.7). The Room schema stays at version 1: category rows reach the phone through sync. The colour hex values of `slate` and `amber` are not needed by M12 (`:core:model` holds token names only); they arrive with U2 and M6.
|
||||
* **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 and 0 are expected after it, to be confirmed by the next dry run.
|
||||
* **M12a, M12b, M12c** (D-049 to D-054) carry the owner's UX-session decisions into the contracts and the landed code: M3, M5 and M9 were built with ten defaults, so this is a contract change against `main`, not only the spec. M12a (landed `6b60ef7`) wrote `contracts/`, the checker and `:domain`'s `suggestCategory`; M12b (landed `2e2ebe7`) and M12c (landed `e499107`) ran in parallel (disjoint paths, §14.7); their outcomes are D-055 to D-058. The Room schema stays at version 1: category rows reach the phone through sync. The colour hex values of `slate` and `amber` are not needed by M12 (`:core:model` holds token names only); they arrive with U2 and M6.
|
||||
* **M12d** (D-059) adds the two UI-contract members UX-9 found (`ExpenseEditorOptions.frequentCategoryIds`, `CategoryRepository.deletable`) to `:core:model`, `:data` and `:core:fakes`, in parallel with M6 (disjoint paths). M7c and M7f consume them; until M12d lands, the design spec's fallbacks apply (`docs/ux/design-spec.md` §3.7 and §6).
|
||||
* **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -1287,7 +1290,7 @@ Suggested starting points the UX agent may use, replace or merge (it must delive
|
||||
2. **"Skärgård"**: Material 3 Expressive with a cool sea-and-granite palette, large rounded shapes, bold balance header, playful but quiet.
|
||||
3. **"Kontor"**: dense, utilitarian, data-forward. Neutral greys, strong typographic hierarchy, charts as the hero on Analytics, minimal decoration.
|
||||
|
||||
**Phase 2 (U2): design spec for the chosen direction.** After the owner picks (and comments), in `docs/ux/`. The owner picked direction 3 "Kalas" with the muted palette of `docs/ux/directions/3-kalas/`, typeface per D-020 (D-047):
|
||||
**Phase 2 (U2): design spec for the chosen direction.** After the owner picks (and comments), in `docs/ux/`. The owner picked direction 3 "Kalas" with the muted palette of `docs/ux/directions/3-kalas/`, typeface per D-020 (D-047). U2 landed `16c4d01`, approved by the owner. `docs/ux/design-spec.md` is the binding design document for `:core:design` (§2 tokens, §3 components) and for every screen (§4, §5); this spec wins over it on rules. `docs/ux/strings.csv` (558 rows, header included; keys with the D-017 prefixes; plurals as `key:one` / `key:other`) is the source of every string resource:
|
||||
|
||||
* `design-spec.md`: final tokens (colour roles light and dark, category colour hex values, typography scale, shapes, spacing scale in dp, elevation, motion durations and easing), component inventory (each with anatomy, states, sizes), and for every screen S-1 to S-22: layout description, all states of §13.3, interaction details, TalkBack labels.
|
||||
* `strings.csv`: every user-visible string of the app with columns `key,en,sv,notes`, keys in `snake_case` prefixed by the feature module that holds them (`home_balance_owes_you`, `expense_save`; shared strings for `:core:design` never start with a feature name; the app name is the only `:app` string; §6.2, D-017), placeholders as Android format arguments (`%1$s`), plurals marked in `notes` with their quantities. This becomes `values/strings.xml` and `values-sv/strings.xml` in M6 and M7; feature agents add no strings that are not in this file without a decisions entry.
|
||||
@@ -1770,7 +1773,7 @@ interface AccountRepository {
|
||||
|
||||
// Expenses (S-7, S-8)
|
||||
data class ExpenseDraft(val id: ExpenseId?, val accountId: AccountId?, val categoryId: CategoryId?, val amountOre: Long?, val description: String, val notes: String, val date: String, val payer: UserId, val mode: SplitMode, val ratioBp: Map<UserId, Int>?, val exactOre: Map<UserId, Long>?, val fullOn: UserId?)
|
||||
data class ExpenseEditorOptions(val me: User, val partner: User, val accounts: List<Account>, val categories: List<Category>, val defaultAccountId: AccountId?, val defaultCategoryFor: Map<AccountId, CategoryId>, val today: String)
|
||||
data class ExpenseEditorOptions(val me: User, val partner: User, val accounts: List<Account>, val categories: List<Category>, val defaultAccountId: AccountId?, val defaultCategoryFor: Map<AccountId, CategoryId>, val today: String, val frequentCategoryIds: Map<AccountId, List<CategoryId>>) // frequentCategoryIds: v0.9, D-059
|
||||
data class TimelineLine(val actor: User?, val action: AuditAction, val source: AuditSource, val clientAt: String, val changes: List<FieldChange>, val pendingLocal: Boolean)
|
||||
data class FieldChange(val field: String, val oldDisplay: String?, val newDisplay: String?) // ids already resolved to names; amounts and dates raw (öre as String, ISO date), formatted by the UI
|
||||
data class ExpenseDetailState(val id: ExpenseId, val account: Account, val category: Category, val amountOre: Long, val description: String, val notes: String, val date: String, val payer: User, val mode: SplitMode, val shares: List<Share>, val recurringTemplateName: String?, val deleted: Boolean, val pending: Boolean, val timeline: List<TimelineLine>)
|
||||
@@ -1822,6 +1825,7 @@ interface CategoryRepository {
|
||||
suspend fun reorder(idsInOrder: List<CategoryId>): OnlineResult<Unit>
|
||||
suspend fun setArchived(id: CategoryId, archived: Boolean): OnlineResult<Unit>
|
||||
suspend fun delete(id: CategoryId): OnlineResult<Unit>
|
||||
fun deletable(id: CategoryId): Flow<Boolean> // v0.9, D-059: false for a default category or one used by a non-deleted expense or non-deleted recurring template (AC-7.6)
|
||||
}
|
||||
data class RecurringTemplate(val id: TemplateId, val draft: ExpenseDraft, val dayOfMonth: Int, val startPeriod: String, val endPeriod: String?, val paused: Boolean, val nextOccurrence: String?)
|
||||
interface RecurringRepository {
|
||||
@@ -1880,7 +1884,14 @@ Changes in v0.8 (D-049, D-054):
|
||||
|
||||
* **Category suggestion types** live in `:domain`, not here (§14.5.6); `:core:model` gains only `ExpenseRepository.suggestCategory`.
|
||||
* **Fourteen defaults.** `DefaultCategoryKey` gains `HOUSING`, `INSURANCE`, `CHILDREN`, `FEES` (wire values `housing`, `insurance`, `children`, `fees`), `ColorToken` gains `SLATE` and `AMBER`, `CategoryIcons.ALL` gains `key`, `shield`, `account_balance` at the end. The localised default names are those of AC-7.2.
|
||||
* **`ExpenseRepository.suggestCategory`.** Computed in `:data` from Room (every non-deleted expense on the phone, pending ones included, and the active categories), the per-phone last-used category of the account (DataStore, §14.2.2) and the keyword table bundled as the resource `android/data/src/main/resources/category-keywords.csv`, by calling `:domain`'s `suggestCategory` (§14.5.6). `:data` binds `:domain`'s `UnicodeText` with `java.text.Normalizer` and `Character`, since `:domain` may not use `java.*` (§14.5.6). `accountId` null (no active account) gives the fallback `other`. The result is null only while the phone holds no categories (before the first sync). The editor (S-7) calls it on every change of the description and of the account while the category was not picked by hand (§9.11 CS-2, CS-3) and never when editing an existing expense (CS-1). `ExpenseEditorOptions.defaultCategoryFor` stays: it is the step-4 default before any description is typed.
|
||||
* **`ExpenseRepository.suggestCategory`.** Computed in `:data` from Room (every non-deleted expense on the phone, pending ones included, across all accounts and both users; and the active categories, neither archived nor deleted), the per-phone last-used category of the account (DataStore, §14.2.2) and the keyword table copied from `contracts/category-keywords.csv` at build time (D-055) and parsed with `parseCategoryKeywords` (D-056), by calling `:domain`'s `suggestCategory` (§14.5.6). `:data` binds `:domain`'s `UnicodeText` with `java.text.Normalizer` and `Character`, since `:domain` may not use `java.*` (§14.5.6). `accountId` null (no active account) gives the fallback `other`. The result is null while the phone holds no categories (before the first sync), and when `other` is archived and no active last-used category exists; the editor then shows no category until the user picks one (AC-8.6, v0.9). Stored `expense_date` values are valid ISO dates (§9.6), so parsing them for the history cannot fail. The editor (S-7) calls it on every change of the description and of the account while the category was not picked by hand (§9.11 CS-2, CS-3) and never when editing an existing expense (CS-1). `ExpenseEditorOptions.defaultCategoryFor` stays: it is the step-4 default before any description is typed.
|
||||
|
||||
Changes in v0.9 (D-055 to D-057, D-059):
|
||||
|
||||
* **Keyword table on the phone (D-055).** `:data` and `:core:fakes` each copy `contracts/category-keywords.csv` at build time (tasks `:data:copyCategoryKeywords` and `:core:fakes:copyCategoryKeywords`, registered as an AGP generated resource directory); a missing file fails the build. It is packaged as the Java resource `se/hagstedt/kvitt/data/category-keywords.csv` (and `se/hagstedt/kvitt/fakes/category-keywords.csv` for the fakes, debug only). There is no committed copy. A `:data` test asserts byte identity with the contract file and the §14.8 rules.
|
||||
* **Enum declaration order (D-057).** `DefaultCategoryKey` is declared in AC-7.2 sort order. The Rust wire enum, the OpenAPI `DefaultCategoryKey` enum and the 0002 `CHECK` list the ten v1 keys, then `housing`, `insurance`, `children`, `fees`. Wire values are unaffected (lowercase names; the OpenAPI equality test compares enums as sets).
|
||||
* **`ExpenseEditorOptions.frequentCategoryIds` (D-059).** Per active account: the active (non-archived, non-deleted) categories ordered by the number of non-deleted expenses in that account that use them, descending; ties by `sort_order` ascending; at most 5 entries; categories with no use in the account are left out (the list may be shorter or empty). Counted over every expense in the phone's Room copy (both users, imported and recurring included), so both phones show the same order after sync; it is not a per-phone history like AC-8.6's "last used on this phone". The flow re-emits when expenses or categories change; the editor reads it once when it opens and on an account change, so tiles do not move while the user looks at them. UI rule (design, not contract): the row shows the account's list, filled up to five from the remaining active categories in `sort_order`; the selected category takes the fifth place when it is not among them. Fallback until M12d lands: the selected category first, then the first active categories in `sort_order` up to five.
|
||||
* **`CategoryRepository.deletable(id)` (D-059).** Computed from Room: false for a default category (`defaultKey != null`) or one used by a non-deleted expense or non-deleted recurring template (AC-7.6). The server stays the authority: `delete` can still return `Failed("category_is_default")` or `Failed("category_in_use")` (for example when the partner's new expense has not synced yet), and the UI shows the same reason inline. The editor picks the text from `deletable` and `defaultKey`: default → `settings_category_delete_blocked_default_short`; in use and archived → `settings_category_delete_blocked_in_use_archived`; in use and active → `settings_category_delete_blocked_in_use`. Fallback until M12d lands: for a custom category "Ta bort kategori" is enabled and `Failed("category_in_use")` shows the in-use reason inline after the tap. Both members are implemented in `:data` and `:core:fakes` (every fake scenario computes them from its own data) by M12d.
|
||||
|
||||
Readings fixed in v0.5 (no type changes):
|
||||
|
||||
@@ -2101,7 +2112,7 @@ Computed with `kvitt-domain::propose_allocation` over the server's non-deleted r
|
||||
|
||||
#### 14.3.5 Examples as contract
|
||||
|
||||
M1 writes one JSON example per request and response body above, plus one per error code, in `contracts/examples/<E-id>-<name>/{request,response}.json` and `contracts/examples/errors/<code>.json`, and the sync examples of §14.4. Directory names are `E-<n>-<kebab-name>` (for example `E-1-health`, `E-32-sync-push`). The server's tests serialise its responses for the same fixtures and compare; the app's `:data` tests decode every example into its DTOs and re-encode it, comparing JSON values.
|
||||
M1 writes one JSON example per request and response body above, plus one per error code, in `contracts/examples/<E-id>-<name>/{request,response}.json` and `contracts/examples/errors/<code>.json`, and the sync examples of §14.4. Directory names are `E-<n>-<kebab-name>` (for example `E-1-health`, `E-32-sync-push`). The server's tests serialise its responses for the same fixtures and compare; the app's `:data` tests decode every example into its DTOs and re-encode it, comparing JSON values. The server's test harnesses seed the two users after the fourteen category seeds, so the users have `server_version` 15 and 16; `E-7-me/response.json` shows those values and its test compares them (D-058).
|
||||
|
||||
### 14.4 Sync protocol v1
|
||||
|
||||
@@ -2454,6 +2465,7 @@ interface UnicodeText {
|
||||
}
|
||||
|
||||
fun normalizeDescription(text: String, unicode: UnicodeText): String // CS-4
|
||||
fun parseCategoryKeywords(csv: String): List<CategoryKeyword> // the §14.8 table; D-056
|
||||
fun suggestCategory(description: String, history: List<SuggestionHistoryEntry>, keywords: List<CategoryKeyword>, activeCategories: List<ActiveCategory>, fallback: CategoryId?, unicode: UnicodeText): CategoryId?
|
||||
```
|
||||
|
||||
@@ -2463,6 +2475,7 @@ fun suggestCategory(description: String, history: List<SuggestionHistoryEntry>,
|
||||
* `history` holds non-deleted expenses only (the caller's job, as for every domain function). Expenses whose `categoryId` is not in `activeCategories` are ignored (CS-5).
|
||||
* History ranking (CS-5 steps 1 and 2): the highest count wins; on equal count, the category whose latest matching expense is latest by (`expenseDate`, then `createdAt`); then the lower category id (string order).
|
||||
* Keyword ranking (CS-6): among the matching entries, the longest keyword in code points wins, then the earlier entry in table order (`keywords` are passed in table order, §14.8). An entry resolves to the active category with that `defaultKey`; none means the next entry in that ranking is tried.
|
||||
* `parseCategoryKeywords` (D-056) reads the text of `contracts/category-keywords.csv`: header `keyword,match,default_key,source`, entries in file order (table order), `match` mapped to `KeywordMatch`; a malformed table (wrong header, wrong column count, unknown `match`) throws `IllegalArgumentException`. Pure; shared by `:data` and `:core:fakes`, which read the bundled resource (§14.2.4, D-055).
|
||||
* `fallback` is the step-4 category id (AC-8.6), computed by the caller; it is returned as given when no earlier step applies, also when the normalised description is empty.
|
||||
|
||||
### 14.6 Push payload v1
|
||||
@@ -2582,8 +2595,9 @@ Ownership (an agent edits only paths its milestone owns; anything else needs the
|
||||
| git-ignored `infra/secrets/**` in the main checkout | M2c-dev (D-022), then M2c-pi | M8 copies `google-services.json` from it (§10.7) |
|
||||
| `docs/ux/**` | U1, U2 | |
|
||||
| M12 (D-054): `contracts/category-keywords.csv`, `contracts/schema/migrations/**`, `contracts/test-vectors/suggest_category.json` and the other `contracts/**` edits of §11.2 row M12a, `tools/contracts-check.py`, `android/domain/**` (`suggestCategory`, `UnicodeText`) | M12a (contract-change plan, landed `6b60ef7`) | contract-change plans only |
|
||||
| M12 (D-054): `contracts/openapi.yaml` (`DefaultCategoryKey`) and the category examples M12a held back; `kvitt-server` `migrations/0002_categories_v2.sql`, `src/db/mod.rs` (`SCHEMA_VERSION`), `src/api/validate.rs` (token lists), `src/wire/category.rs` and `src/wire/mod.rs` (key enum), their tests; `kvitt-import` `data/splitwise_categories.csv`, `src/categories.rs` and its tests | M12b (contract-change plan) | the usual owners |
|
||||
| M12 (D-054): `android/core/model/**` (enums, icons, `ExpenseRepository`), `android/data/**` (`UnicodeText` binding, `suggestCategory`, the bundled keyword resource), `android/core/fakes/**` (defaults, fake `suggestCategory`) | M12c (contract-change plan) | the usual owners |
|
||||
| M12 (D-054): `contracts/openapi.yaml` (`DefaultCategoryKey`) and the category examples M12a held back; `kvitt-server` `migrations/0002_categories_v2.sql`, `src/db/mod.rs` (`SCHEMA_VERSION`), `src/api/validate.rs` (token lists), `src/wire/category.rs` and `src/wire/mod.rs` (key enum), their tests; `kvitt-import` `data/splitwise_categories.csv`, `src/categories.rs` and its tests | M12b (contract-change plan, landed `2e2ebe7`) | the usual owners |
|
||||
| M12 (D-054): `android/core/model/**` (enums, icons, `ExpenseRepository`), `android/data/**` (`UnicodeText` binding, `suggestCategory`, the build-time copy of the keyword table, D-055), `android/core/fakes/**` (defaults, fake `suggestCategory`) | M12c (contract-change plan, landed `e499107`) | the usual owners |
|
||||
| D-059: `android/core/model/**` (`ExpenseEditorOptions`, `CategoryRepository`), `android/data/**` (`frequentCategoryIds`, `deletable`), `android/core/fakes/**` (both, outside the `fakes/<feature>/` folders) | M12d (contract-change plan) | the usual owners; M7x keep their `fakes/<feature>/` folders |
|
||||
| `docs/validation/<file>` | the milestone that names the file | |
|
||||
|
||||
Module shells (D-004): a shell file or module created by M2a or M2b in a path owned by a later milestone holds only what makes the build and the 501 stubs work; the later milestone owns it from the moment the shell lands and writes the body.
|
||||
@@ -2594,7 +2608,7 @@ Strings: each feature module has its own `src/main/res/values/strings.xml` and `
|
||||
|
||||
Contract files (exhaustive): `contracts/openapi.yaml`, `contracts/schema/server-v1.sql`, the locked migration texts `contracts/schema/migrations/*.sql` (0002 since D-054, §14.1.1), `contracts/category-keywords.csv`, `contracts/test-vectors/*.json`, `contracts/examples/**`, the Room exported schema `android/data/schemas/**` (version 1 exists since M2b, D-012), `android/core/model/**` (the UI contract and domain types), the sync protocol (§14.4) and the push payload (§14.6).
|
||||
|
||||
`contracts/category-keywords.csv` (D-049) is the keyword table of §9.11 CS-6: UTF-8 without BOM, `\n` line ends, header `keyword,match,default_key,source`, no quoting. `keyword` is non-empty and already normalised (CS-4 applied to it gives it back), unique in the file; `match` is `token`, `prefix` or `phrase`; a `token` or `prefix` keyword has no space and a `phrase` keyword has at least one; `default_key` is a key of AC-7.2 other than `other`; `source` is `data` (learned from the household's export) or `curated`, documentation only. Rows are sorted by `default_key`, then `keyword`, in code-point order. The phone bundles a byte-identical copy (`android/data/src/main/resources/category-keywords.csv`, §14.2.4); M12a seeds the file from `docs/ux/category-keywords.csv` (148 entries). Changing an entry is a contract change like any other.
|
||||
`contracts/category-keywords.csv` (D-049) is the keyword table of §9.11 CS-6: UTF-8 without BOM, `\n` line ends, header `keyword,match,default_key,source`, no quoting. `keyword` is non-empty and already normalised (CS-4 applied to it gives it back), unique in the file; `match` is `token`, `prefix` or `phrase`; a `token` or `prefix` keyword has no space and a `phrase` keyword has at least one; `default_key` is a key of AC-7.2 other than `other`; `source` is `data` (learned from the household's export) or `curated`, documentation only. Rows are sorted by `default_key`, then `keyword`, in code-point order. The phone bundles it through a build-time copy (D-055: `:data:copyCategoryKeywords` and `:core:fakes:copyCategoryKeywords`, no committed copy, a missing file fails the build, a `:data` test asserts byte identity and these rules, §14.2.4); M12a seeds the file from `docs/ux/category-keywords.csv` (148 entries). Changing an entry is a contract change like any other.
|
||||
|
||||
Representational edits to `contracts/openapi.yaml` (key order, `$ref` naming, description wording) do not change meaning and need no spec bump or decision entry; they are listed in the report of the plan that made them (D-016). The OpenAPI equality test compares parsed values, not text.
|
||||
|
||||
@@ -2627,3 +2641,4 @@ Rule: a change to any other contract file, or a semantic change to `contracts/op
|
||||
| 0.6 | 2026-10-18 | Decisions from the M4 (`bd6f10a`) implementation report. D-035: a mutation whose `mutation_id`, `entity_type` or `entity.id` is unusable fails the whole request with 400 `invalid_request`; every other shape error is a per-mutation `validation_failed` (§14.4.4 step 3). D-036: `POST /sync` with 0 mutations is 422 `batch_too_large`, the code now reads "outside 1 to 100" (§9.6, §14.3.2, §14.4.4). D-037: a delete of a never-synced row runs the full step-8 validation; every rejection carries the stored row or null as `current`, also at steps 3 and 4; step 11 keeps the stored `created_at`; pushed ids must be canonical lowercase hyphenated UUIDs (§14.4.3, §14.4.4). D-038: FCM retries are one send plus up to three retries at 1 s, 5 s, 25 s; 401 drops the cached access token and is not retried, other 4xx are not retried; retries block the single sender task up to 31 s per device (§14.6). D-039: pruning deletes `received_at` strictly older than 90 days; seam change to D-015: `tasks::start` returns the `AppState` with the real push queue and `serve` builds the router from it, `AppState::new` defaults to `SqlAuditWriter`, `NoopAuditWriter` removed, `NoopPushQueue` kept for push disabled (§6.3, §14.1, §14.4.9; PLAN-M4). Clarifications without a decision id: §14.4.4 step 8 order is field rules, domain rules, references; a negative `since` is `invalid_request` and a pull reads one page in a single read transaction (§9.6, §14.3.4, §14.4.2); push runs in one `BEGIN IMMEDIATE` with a savepoint per mutation; the push queue is bounded at 100 and drops the oldest with a warning; the FCM JWT is RS256 with `kid` = `private_key_id`, the access token cached until 5 minutes before expiry, messages data-only with string values; `UNREGISTERED` or 404 clears the device token only if unchanged; an unusable credentials file disables push like a missing one (§6.3, §14.6); the scheduler reads the injected clock at least every 30 s, runs a missed job once after a forward jump and re-plans after a backward jump of more than a day (§9.5); `rotate-epoch` prints the new epoch, exit 0 or 1 on a database error (§10.4); `serve` uses `into_make_service_with_connect_info` (D-032 fulfilled, §8.6, §11.3); `GET /i/` with an empty code is the HTML 404 (E-4); reqwest (rustls, aws-lc-rs, platform verifier) and jsonwebtoken added, so the arm64 image needs a C toolchain stage and the distroless image CA roots, to be confirmed by M2c-pi or M10; the dev image was rebuilt with them on the Mac on 2026-10-18 (§6.3, §10.3); M4's test binaries and scheduler path (§11.3, §14.7). |
|
||||
| 0.7 | 2026-10-18 | Decisions from the M9 (`ab298ab`) implementation report and the importer rehearsal on the real exports. D-044: nine Swedish Splitwise category names added to the §10.8 table and to `splitwise_categories.csv` with the key of their English equivalent (Städning and Verktyg - Övrigt `utilities`, Tjänster `home`, Cykel `transport`, Flyg `travel`, Spel `entertainment`, Kläder `shopping`, Livet - Övrigt and Utbildning `other`). D-045: reconciliation: the run commits only if each user's balance of the whole account equals the sum of the file's totals rows (one per currency, converted with `--fx`); a mismatch or a missing totals row rolls back and exits 4, a dry run reports the same, an account already holding other rows rolls back, a mismatch after any `--fx` conversion is reported but accepted (§10.8, AC-17.5). D-046: CLI semantics: `--user` takes a user id or an exact unique display name; no database is created or migrated, `meta.schema_version` must be 1; a dry run runs the full transaction and rolls back; exit codes 0 ok, 1 database or file error, 2 usage, 3 cannot import, 4 reconciliation mismatch; the import key uses an existing account's stored name; an empty description or one over 1000 code points makes the row invalid; descriptions over 100 code points are cut with the full text in `notes`; settlement notes cut to 200; both nets zero is mode `full` on the owner; idempotency through the existing `import_key` columns (§10.8, AC-17.5). M9 marked landed, M11 rehearsal recorded (§11.2, §11.3). |
|
||||
| 0.8 | 2026-10-18 | Decisions from the owner's UX session (`docs/ux/proposed-decisions.md` UX-1 to UX-8 on branch `UX-sprint`). D-047: direction 3 "Kalas" with the muted palette, typeface per D-020 (§9.7, §13.4). D-048: navigation stays at three destinations, an "Aktivitet" tab not taken for v1; §13.2 binding. D-049: category suggestion from the description, new §9.11 (CS-1 to CS-7: new expenses only, manual choice wins, history most often then first token, keyword table, AC-8.6 default, no marker), AC-8.6, `ExpenseRepository.suggestCategory` (§14.2.4), Kotlin-only `suggestCategory` in `:domain` with the `UnicodeText` interface bound by `:data` and `suggest_category.json` as landed by M12a (`6b60ef7`) (§14.5.5, §14.5.6), contract file `contracts/category-keywords.csv` (§14.8). D-050: S-1 display-name field on a first pairing (AC-2.4, §13.3). D-051: §9.7 hex values become the muted Kalas values from U2, token names fixed, `slate` and `amber` `TODO(U2)`. D-052: "Utgiften sparades" without undo (S-7), archived account hides the FAB (S-5), per-account settle-up keeps the swap and an editable amount (S-9), avatar tokens `avatar-1` and `avatar-2` (AC-4.5), Swedish effect copy (AC-8.10). D-053: finer Splitwise mapping (§10.8). D-054: fourteen default categories (`housing`, `insurance`, `children`, `fees` added; AC-7.2), icons `key`, `shield`, `account_balance` (33), colours `slate`, `amber` (14), server schema 2 through migration 0002 rebuilding `categories` (§14.1.1, locked as `contracts/schema/migrations/0002_categories_v2.sql`, copied from M12a's file; no `server-v2.sql`, §14.8), importer schema check 2 (§10.8), enums in §14.2.4 and §14.3.3, E-1 `schema_version` 2; Room schema unchanged. New milestone group M12 "Category contract v2" (M12a contracts, then M12b server and M12c Android in parallel; M6 and M7c wait for M12c) (§11). |
|
||||
| 0.9 | 2026-10-18 | Outcomes of M12b (`2e2ebe7`) and M12c (`e499107`), U2 (landed `16c4d01`) and UX-9. D-055: the keyword table is copied from `contracts/category-keywords.csv` at build time by `:data:copyCategoryKeywords` and `:core:fakes:copyCategoryKeywords` (AGP generated resource directory), a missing file fails the build, packaged as `se/hagstedt/kvitt/data/category-keywords.csv` (and the fakes path in debug), no committed copy, a `:data` test asserts byte identity and the §14.8 rules; supersedes the `android/data/src/main/resources` copy (§11.2 M12c, §14.2.4, §14.7, §14.8). D-056: pure `parseCategoryKeywords(csv)` in `:domain`, shared by `:data` and `:core:fakes` (§14.5.6). D-057: `DefaultCategoryKey` declared in AC-7.2 order; the Rust wire enum, the OpenAPI enum and the 0002 `CHECK` list the ten v1 keys then the four new ones; wire values unaffected (§14.2.4). D-058: test harnesses seed users after the fourteen category seeds (`server_version` 15 and 16); `E-7-me/response.json` shows 15 and 16 and its test compares them again (§14.3.5). D-051 fulfilled: §9.7 hex values from `docs/ux/design-spec.md` §2.3; `design-spec.md` is the binding design document and `strings.csv` (558 rows, D-017 prefixes, plurals as `key:one`/`key:other`) the source of string resources (§13.4). D-059: UX-9 accepted as a contract change: `ExpenseEditorOptions.frequentCategoryIds` and `CategoryRepository.deletable(id)` with their readings and fallbacks (§14.2.4), new milestone M12d (§11, §14.7); M7a to M7f depend on M6 and M12d. Clarifications without a decision id: `suggestCategory` history is every non-deleted expense (pending included, all accounts, both users), active categories neither archived nor deleted, fallback last-used in the account if active else `other`, null account gives `other`; when `other` is archived and no active last-used exists the result is null and the editor shows no category until the user picks one (AC-8.6, §9.11 CS-5, §14.2.4); stored `expense_date` values are valid ISO dates (§9.6), so parsing cannot fail. §11.2: M12a, M12b, M12c, U1, U2 landed, M6 started 2026-10-18; dry run of 2026-10-18 (Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled) (§11.2, §11.3). §10: development stack migrated to schema 2 on 2026-10-18 after a backup. PLAN-M12b: bundled SQLite is 3.51.x (3.51.3), not 3.50. |
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"picture_version": null,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T06:12:40.512Z",
|
||||
"server_version": 11
|
||||
"server_version": 15
|
||||
},
|
||||
"device": {
|
||||
"id": "0192a001-0000-7000-8000-000000000001",
|
||||
@@ -26,7 +26,7 @@
|
||||
"picture_version": null,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T06:12:40.512Z",
|
||||
"server_version": 11
|
||||
"server_version": 15
|
||||
},
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000002",
|
||||
@@ -35,7 +35,7 @@
|
||||
"picture_version": 2,
|
||||
"created_at": "2026-10-17T07:01:15.000Z",
|
||||
"updated_at": "2026-10-17T07:05:00.250Z",
|
||||
"server_version": 12
|
||||
"server_version": 16
|
||||
}
|
||||
],
|
||||
"public_url": "https://icehack.asuscomm.com:8443"
|
||||
|
||||
+20
-21
@@ -3,30 +3,29 @@
|
||||
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
|
||||
|
||||
## 1. Where things stand
|
||||
- `main` at `6b60ef7` (M12a) plus the SPEC v0.8 docs commit from `agent-spec8` once it lands. History: `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `fad6fc9` M2b, `fdf2093` M2a, `040bf41` SPEC v0.4, `503cee5` M3, `c28c52b` M5, `982451d` SPEC v0.5, `bd6f10a` M4, `23a2f1e` SPEC v0.6, `ab298ab` M9, `e8ee518` SPEC v0.7, `6b60ef7` M12a.
|
||||
- Landed: M0 spec, M1 contracts and vectors (`c6df2ab`), M2a server skeleton and `kvitt-domain` (`fdf2093`), M2b Android skeleton and `:domain` (`fad6fc9`), M2c-dev infra for development (`c885aba`), M3 server core API (`503cee5`), M4 server sync, audit, push, scheduler (`bd6f10a`), M5 Android data layer (`c28c52b`), M9 Splitwise importer (`ab298ab`), SPEC v0.7 (`e8ee518`), M12a category contract v2 contracts and `:domain` `suggestCategory` (`6b60ef7`; OpenAPI `DefaultCategoryKey` and the new category examples held back for M12b). With `agent-spec8`: SPEC v0.8, the owner's UX-session decisions UX-1 to UX-8 as D-047 to D-054 (Kalas muted, three tabs, category suggestion §9.11, S-1 name field, colour tokens, smaller UX rules, finer import mapping, fourteen default categories with server schema 2), and the milestone group M12 with PLAN-M12a/b/c and prompts, reconciled with what M12a landed (§14.1.1 copied from the contract file, `:domain` types with `UnicodeText`, vector keys).
|
||||
- **Known red test:** from the landing of SPEC v0.8 until M12b lands, `main`'s server gate has exactly one failing test, the importer's `category_table_matches_the_spec`, which parses the §10.8 table that v0.8 changed (D-053). M12b fixes it; land M12b next.
|
||||
- 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/`).
|
||||
- Worktrees in flight under `.claude/worktrees/`: `agent-spec8` (this docs change: SPEC v0.8).
|
||||
- The owner runs the UX sprint in a separate Claude session in `~/orca/workspaces/kvitt/UX-sprint` on branch `UX-sprint`; it commits only under `docs/ux/` and rebases on `main` before landing; its results reach the main line through `docs/ux/proposed-decisions.md` and a docs agent (CLAUDE.md › Orchestration). Direction 3 "Kalas" with the muted palette is picked and approved (D-047); U2 (design spec, renamed `strings.csv`, per-screen PNGs, the §9.7 hex values including `slate` and `amber`) is in progress there.
|
||||
- Importer rehearsal (M11 preparation), 2026-10-18: the real run reconciled on both real files in a throwaway database (Vardag 1746 rows, Hemma 37 rows); 21 rows were unmapped before D-044, 0 are expected after it. After M12b the mapping is finer (D-053); the next dry run confirms it. The CSV files stay in `export/` (git-ignored).
|
||||
- Development stack (M2c-dev): running on the Mac with the M4 image, reachable at `http://192.168.1.210:8080`; the owner confirmed `/health` from the phone on the home Wi-Fi on 2026-10-17. Its database is at schema 1; an image with M12b upgrades it to 2 on start (back it up first). Firebase project `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm`; `infra/secrets/fcm-service-account.json` and `infra/secrets/google-services.json` in the main checkout (git-ignored).
|
||||
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug` built from `main` at `c28c52b`, as of 2026-10-18.
|
||||
- `main` at `16c4d01` (UX sprint U2, on top of `2e2ebe7` M12b) plus the SPEC v0.9 docs commit from `agent-spec9` once it lands. History: `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `fad6fc9` M2b, `fdf2093` M2a, `040bf41` SPEC v0.4, `503cee5` M3, `c28c52b` M5, `982451d` SPEC v0.5, `bd6f10a` M4, `23a2f1e` SPEC v0.6, `ab298ab` M9, `e8ee518` SPEC v0.7, `6b60ef7` M12a, `7fb9f6f` SPEC v0.8, `e499107` M12c, `2e2ebe7` M12b, `16c4d01` UX sprint U2.
|
||||
- Landed: M0 spec, M1 contracts and vectors (`c6df2ab`), M2a server skeleton and `kvitt-domain` (`fdf2093`), M2b Android skeleton and `:domain` (`fad6fc9`), M2c-dev infra for development (`c885aba`), M3 server core API (`503cee5`), M4 server sync, audit, push, scheduler (`bd6f10a`), M5 Android data layer (`c28c52b`), M9 Splitwise importer (`ab298ab`), M12a category contract v2 contracts (`6b60ef7`), SPEC v0.8 (`7fb9f6f`), M12c category contract v2 Android (`e499107`), M12b category contract v2 server (`2e2ebe7`; the importer test `category_table_matches_the_spec` is green again), U1 and U2 (`16c4d01`, `docs/ux/**` only; the owner approved U2).
|
||||
- With `agent-spec9`: SPEC v0.9. D-055 build-time copy of the keyword table (`:data:copyCategoryKeywords`, `:core:fakes:copyCategoryKeywords`), D-056 `parseCategoryKeywords` in `:domain`, D-057 enum declaration orders, D-058 E-7 example shows `server_version` 15 and 16 (example and `tests/core_api/me.rs` changed in that commit), D-059 UX-9 accepted (`ExpenseEditorOptions.frequentCategoryIds`, `CategoryRepository.deletable`) with the new milestone M12d (PLAN-M12d, `prompts/M12d.md`); D-051 fulfilled: §9.7 hex values from `docs/ux/design-spec.md` §2.3; `design-spec.md` binding for `:core:design` and the screens, `strings.csv` (558 rows) the source of string resources (§13.4); the `suggestCategory` readings (null when `other` is archived and no active last-used category, AC-8.6); PLAN-M6, PLAN-M7c, PLAN-M7f carry design-spec §6 notes.
|
||||
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test` (from `server/`). Both are green on `main` as far as reported.
|
||||
- Worktrees in flight under `.claude/worktrees/`: `agent-spec9` (this docs change: SPEC v0.9), `agent-m6` (M6 design system, started 2026-10-18 from design-spec §2 and §3 and `strings.csv`), `agent-m12d` (M12d UI contract additions).
|
||||
- The UX sprint (owner's session in `~/orca/workspaces/kvitt/UX-sprint`, branch `UX-sprint`) has landed; nothing is in flight there.
|
||||
- Importer: the dry run of 2026-10-18 with M12b's mapping reconciled on both real files (Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped). The CSV files stay in `export/` (git-ignored).
|
||||
- Development stack (M2c-dev): running on the Mac, reachable at `http://192.168.1.210:8080`; its database was migrated to schema 2 on 2026-10-18 after a backup. Firebase project `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm`; `infra/secrets/fcm-service-account.json` and `infra/secrets/google-services.json` in the main checkout (git-ignored).
|
||||
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug` built from `c28c52b` (older than `main`), as of 2026-10-18.
|
||||
|
||||
## 2. Open work (one line each, with who owns it: agent or owner)
|
||||
- Land `agent-spec8` (orchestrator): SPEC v0.8, decisions D-047 to D-054, PLAN-M12a/b/c, prompts M12a/b/c, CLAUDE.md UX-sprint note, plan touch-ups (M6, M7c, M7f, U2).
|
||||
- Next: M12b (server; also the OpenAPI enum and the held-back examples) and M12c (Android) in parallel.
|
||||
- Land `agent-spec9` (orchestrator): SPEC v0.9, D-055 to D-059, PLAN-M12d and its prompt, plan touch-ups (M6, M7c, M7f, M12b, M12c), the E-7 example and its test.
|
||||
- M6 Design system (`agent-m6`, in flight) and M12d UI contract additions (`agent-m12d`, in flight), in parallel; disjoint paths.
|
||||
- Image build for the Pi (M2c-pi or M10): since M4 the arm64 server image needs a C toolchain in the build stage (aws-lc-rs) and CA roots in the distroless image (§10.3); confirm both and one FCM send from the shipped image.
|
||||
- D-027 re-stamp (orchestrator assigns: M8 as a contract request on `:data`, a small `:data` agent, or fold into M12c): on the first sync attempt after a `clock_skew` rejection the phone re-stamps `updated_at` of each such pending row to its current clock before pushing. Not in `:data` at `c28c52b`.
|
||||
- D-027 re-stamp (orchestrator assigns: M8 as a contract request on `:data`, or a small `:data` agent): on the first sync attempt after a `clock_skew` rejection the phone re-stamps `updated_at` of each such pending row to its current clock before pushing. Not in `:data` yet.
|
||||
- Picture size (M6, if needed): `:data` always fetches E-11 at 512; a size parameter for the 128 variant is a contract request on `:core:model` and `:data`.
|
||||
- After U2 reports: a docs agent records the §9.7 hex values (muted Kalas, `slate` and `amber`), U2's other spec proposals, and the four new default names in `docs/ux/strings.csv` (D-051, D-054).
|
||||
- Plans (SPEC §11.2 v0.8), in dependency order; prompt `prompts/<id>.md`:
|
||||
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M9: landed.
|
||||
- M12a Category contract v2, contracts: landed (`6b60ef7`).
|
||||
- Next waves: M12b (server) and M12c (Android) in parallel now; U2 (owner's session; owner approves); then M6 Design system after U2, the §9.7 values and M12c; M7a to M7f after M6 in parallel (M7c also after M12c); M8 App integration after M7; M2c-pi Infra on the Pi after M8 (owner: answer OQ-3, grant the Pi, Home Assistant Pi, Gitea and Drive allowances at the start, D-023); M10 First release after M2c-pi (owner: keystore per §10.6, D-024); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045).
|
||||
- Plans (SPEC §11.2 v0.9), in dependency order; prompt `prompts/<id>.md`:
|
||||
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M9, M12a, M12b, M12c, U1, U2: landed.
|
||||
- Now: M6 and M12d in parallel.
|
||||
- Next waves: M7a to M7f in parallel after M6 and M12d (M7c and M7f consume M12d); M8 App integration after M7; M2c-pi Infra on the Pi after M8 (owner: answer OQ-3, grant the Pi, Home Assistant Pi, Gitea and Drive allowances at the start, D-023); M10 First release after M2c-pi (owner: keystore per §10.6, D-024); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045).
|
||||
|
||||
## 3. HUMAN steps pending (relay in chat)
|
||||
- U2 approval: the owner approves U2's design spec in the UX-sprint session.
|
||||
- When M6 reports: look at M6's component screenshots and answer yes/no (SPEC §11.2 row M6).
|
||||
|
||||
## 4. Owner questions (asked, unanswered)
|
||||
- OQ-3 LAN addresses and SSH users of the Kvitt Pi and the Home Assistant Pi; SSH port of the HA Pi; certificate directory there; which SSH add-on (SPEC §12). Needed by M2c-pi only. The only open question (OQ-1, OQ-2, OQ-4 answered: D-001, D-003, §10.9).
|
||||
@@ -35,9 +34,9 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
|
||||
- Nothing.
|
||||
|
||||
## 6. Numbering
|
||||
- Decisions: D-001 to D-054 used (D-008 and D-009 unused). Next free: **D-055**.
|
||||
- Spec version: v0.8 written; next **v0.9**.
|
||||
- Server schema version **2** (migration 0002, implemented by M12b); next **3**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`, unchanged by M12); next **2**.
|
||||
- Decisions: D-001 to D-059 used (D-008 and D-009 unused). Next free: **D-060**.
|
||||
- Spec version: v0.9 written; next **v0.10**.
|
||||
- Server schema version **2** (migration 0002, landed with M12b); next **3**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`, unchanged by M12 and M12d); next **2**.
|
||||
- Sync protocol **1**, push payload **1**.
|
||||
|
||||
## 7. How the work is run
|
||||
|
||||
+6
-1
@@ -55,7 +55,12 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
||||
| D-048 | 2026-10-18 | Navigation stays at three destinations (Översikt, Statistik, Inställningar); §13.2 is binding. An "Aktivitet" feed (the other person's changes across accounts) was considered as a fourth tab and not taken for v1. | Owner's answer (UX-2). | SPEC §13.2 |
|
||||
| D-049 | 2026-10-18 | Category suggestion (§9.11 CS-1 to CS-7): for a new expense, while the category was not picked by hand in this editor session, the editor selects (1) the category used most often among expenses with the same normalised description, (2) else most often among expenses with the same first token, ties by the most recent expense in list order, then category id, (3) else the longest matching entry of the fixed keyword table `contracts/category-keywords.csv`, (4) else the AC-8.6 default. Recomputed on every description and account change; no marker; editing never changes a category. `ExpenseRepository.suggestCategory(accountId, description)` in `:data`; pure `suggestCategory(description, history, keywords, activeCategories, fallback, unicode: UnicodeText): CategoryId?` in `:domain` (Kotlin only) with `suggest_category.json`. `UnicodeText` (`nfc`, `isLetter(codePoint)`, `isDigit(codePoint)` meaning Nd) is an interface in `:domain` that `:data` binds with `java.text.Normalizer` and `Character`, because pure modules cannot use `java.*`; the records `SuggestionHistoryEntry(description, categoryId, expenseDate, createdAt)`, `CategoryKeyword(keyword, match, defaultKey: String)`, `ActiveCategory(id, defaultKey: String?)` and the enum `KeywordMatch` live in `:domain`, with default keys as strings. History ties: count, then the latest (`expense_date`, `created_at`), then the lower category id; code points walked by surrogate pairs; keywords rank by code-point length, then table order. As landed by M12a (`6b60ef7`). | Owner (UX-3): most expenses repeat the same shops; fewer taps in the ten-second flow (§13.1). Replay of the household's 1 775 Splitwise expenses: 79.7 % right versus 30.5 % for the AC-8.6 default alone; "most often" beats "most recent" by about one point (`docs/ux/category-suggestion.md`). | SPEC AC-8.6, §9.11, §13.3 (S-7), §14.2.4, §14.5.5, §14.5.6, §14.7, §14.8 |
|
||||
| D-050 | 2026-10-18 | S-1 has a required "Ditt namn" display-name field (1 to 40 code points, §9.6) on a first pairing, hidden in the "Para ihop telefonen igen" variant; a partner confirms it again on S-2 (AC-2.10). | UX-4: AC-2.4 sends a display name with the accept call, but S-1 listed no field for it. Owner OK. | SPEC AC-2.4, §13.3 (S-1) |
|
||||
| D-051 | 2026-10-18 | The §9.7 colour hex values are replaced by the muted Kalas values when U2 reports; the token names are fixed now, the two new ones being `slate` (insurance) and `amber` (fees), whose values are `TODO(U2)`. M6 starts only after the values have landed in §9.7. | UX-5: several provisional values (light orange, yellow, lime) fail contrast as icon colours on tinted containers; UX-8 needs two more tokens. | SPEC §9.7, §13.4 |
|
||||
| D-051 | 2026-10-18 | The §9.7 colour hex values are replaced by the muted Kalas values when U2 reports; the token names are fixed now, the two new ones being `slate` (insurance) and `amber` (fees), whose values are `TODO(U2)`. M6 starts only after the values have landed in §9.7. Fulfilled in SPEC v0.9 (2026-10-18): the §9.7 values are those of `docs/ux/design-spec.md` §2.3 (U2, landed `16c4d01`), `slate` `#5E2D4E` / `#B0719A`, `amber` `#937D63` / `#DFCFC0`, light `brown` `#6B5244`. | UX-5: several provisional values (light orange, yellow, lime) fail contrast as icon colours on tinted containers; UX-8 needs two more tokens. | SPEC §9.7, §13.4 |
|
||||
| D-052 | 2026-10-18 | Smaller UX rules: saving an expense shows "Utgiften sparades" without "Ångra" (S-7); an archived account's detail hides the add-expense FAB and keeps settling (S-5); the per-account settle-up keeps the direction swap and an editable amount, its one account row fixed (S-9); avatar initials use two colour tokens `avatar-1`, `avatar-2`, the slot a stable function of the user id (AC-4.5). The owner approved the Swedish wording "Ni är kvitt", "Gör upp", "Du lånade ut / Du lånade", "Inte inblandad" (AC-8.10; the copy lives in `docs/ux/strings.csv`). U2 renames the `strings.csv` keys to the feature prefixes of D-017. | UX-6, found during U1: an undo would be an audited create and delete that also notifies the partner; archived accounts take no new expenses (AC-6.4); §9.4 allows per-account settle-ups either way and AC-11.4 says the amount "defaults to". | SPEC AC-4.5, AC-8.10, §13.3 (S-5, S-7, S-9) |
|
||||
| D-053 | 2026-10-18 | Splitwise categories are mapped as finely as the fourteen categories allow: Rent, Mortgage, Hyra, Avbetalning/Amortering → `housing`; Insurance, Försäkringar → `insurance`; Childcare, Barnomsorg → `children`; Taxes, Skatter → `fees`; the Swedish labels El, Värme/gas, Vatten, Sophämtning (`utilities`) and Husdjur (`home`) added by analogy, to be checked against Splitwise (an unseen name only stays unmapped). The §10.8 table and `server/crates/kvitt-import/data/splitwise_categories.csv` change together (M12b). | UX-7 (owner, 2026-10-18): §10.8 collapsed Försäkringar (26 rows), Barnomsorg (73) and Skatter (19) into `other` and Hyra (106) and Avbetalning/Amortering (48) into `home`; "Hemförsäkring" must not end up in Övrigt. | SPEC §10.8 |
|
||||
| D-054 | 2026-10-18 | Fourteen default categories: `housing` Boende (`key`, `indigo`, 25), `insurance` Försäkringar (`shield`, `slate`, 45), `children` Barn (`child_care`, `lime`, 85), `fees` Avgifter & skatter (`account_balance`, `amber`, 95) join the ten; `home` keeps furniture, supplies, maintenance, electronics and services. Ids `01920000-0000-7000-8000-000000000011` to `…014`, `created_at` `2026-10-18T00:00:00.000Z`, `server_version` from the global sequence. Server schema 2 through migration 0002, which rebuilds `categories` (the v1 `CHECK` lists ten keys) under `PRAGMA defer_foreign_keys`, inserts the four rows and sets `schema_version` 2; its text is locked as `contracts/schema/migrations/0002_categories_v2.sql` (§14.1.1); `server-v1.sql` stays byte-locked. Icons 33 (`key`, `shield`, `account_balance`), colours 14, `DefaultCategoryKey` and `ColorToken` extended, OpenAPI enum, E-1 `schema_version` 2; the importer requires schema 2 (revises D-046's "1"). Room schema unchanged (rows arrive through sync). Carried out by milestone group M12 (M12a contracts, then M12b server and M12c Android in parallel; M6 and M7c after M12c). | UX-8 (owner, 2026-10-18): the household's history has these as large separate groups (Hyra + Amortering 154 rows, Barnomsorg 73, Försäkringar 26, Skatter 19) that were collapsed into Hem and Övrigt; separate categories make analytics and the suggestion meaningful. M3, M5 and M9 landed with ten defaults, so this is a contract change against `main`. | SPEC AC-7.2, §6.2, §9.7, §10.8, §11, §13.3 (S-15), §14.1, §14.1.1, §14.2.4, §14.3.3, §14.3.4 (E-1), §14.7, §14.8 |
|
||||
| D-055 | 2026-10-18 | The keyword table reaches the phone by a build-time copy: `:data:copyCategoryKeywords` and `:core:fakes:copyCategoryKeywords` copy `contracts/category-keywords.csv` into an AGP generated resource directory; a missing file fails the build. Packaged as the Java resource `se/hagstedt/kvitt/data/category-keywords.csv` (and `se/hagstedt/kvitt/fakes/category-keywords.csv` for the fakes, debug only); no committed copy. A `:data` test asserts byte identity with the contract file and the §14.8 rules. Supersedes the "byte-identical copy at `android/data/src/main/resources`" wording of v0.8. | M12c report (`e499107`): a committed copy can drift from the contract file; a build-time copy cannot. | SPEC §11.2 (M12c), §14.2.4, §14.7, §14.8; PLAN-M12c |
|
||||
| D-056 | 2026-10-18 | `:domain` exposes the pure `parseCategoryKeywords(csv: String): List<CategoryKeyword>`: header `keyword,match,default_key,source`, entries in file order, `IllegalArgumentException` on a malformed table; shared by `:data` and `:core:fakes`. | M12c report (`e499107`): two callers needed the same parser; `:domain` is the shared pure module. | SPEC §14.5.6 |
|
||||
| D-057 | 2026-10-18 | `DefaultCategoryKey` in `:core:model` is declared in AC-7.2 sort order. The Rust wire enum, the OpenAPI `DefaultCategoryKey` enum and the 0002 `CHECK` list the ten v1 keys, then `housing`, `insurance`, `children`, `fees`. Wire values are unaffected. | M12b (`2e2ebe7`) and M12c (`e499107`) reports: the declaration orders differ by side; enums compare as sets on the wire. | SPEC §14.2.4 |
|
||||
| D-058 | 2026-10-18 | The server's test harnesses seed the two users after the fourteen category seeds, so they have `server_version` 15 and 16. `contracts/examples/E-7-me/response.json` shows 15 and 16 (was 11 and 12, from schema 1's ten seeds), and the E-7 test compares `server_version` with the example again instead of treating it as volatile. | M12b report (`2e2ebe7`): migration 0002 moved the users' versions; the example should match what the server produces. | SPEC §14.3.5; `contracts/examples/E-7-me/response.json`, `server/crates/kvitt-server/tests/core_api/me.rs` |
|
||||
| D-059 | 2026-10-18 | UX-9 accepted as a contract change: `ExpenseEditorOptions.frequentCategoryIds: Map<AccountId, List<CategoryId>>` (per active account up to 5 active categories by use count in that account over the whole Room copy, ties by `sort_order`, unused ones left out; read once on open and on an account change) and `CategoryRepository.deletable(id): Flow<Boolean>` (false for a default or a category used by a non-deleted expense or template, AC-7.6; the server stays the authority), with the readings, UI text choice and fallbacks of §14.2.4 "Changes in v0.9". Implemented in `:core:model`, `:data` and `:core:fakes` by the new milestone M12d, run in parallel with M6; M7c and M7f consume it, and M7a to M7f depend on M6 and M12d. | U2 (`docs/ux/proposed-decisions.md` UX-9): S-7's `CategoryQuickRow` and S-15's category editor have binding states the UI contract did not carry. | SPEC §11.1, §11.2, §11.3, §14.2.4, §14.7; PLAN-M12d, PLAN-M7c, PLAN-M7f |
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Start prompt: M12d UI contract additions (UX-9)
|
||||
|
||||
You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/kvitt`), milestone M12d: UI contract additions (UX-9). You have no context other than this prompt and the files it names.
|
||||
|
||||
Read PLAN-M12d.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both.
|
||||
|
||||
Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration):
|
||||
- Next free decision id: {{NEXT_DECISION}}
|
||||
- Spec version a proposed spec change would get: {{SPEC_VERSION}}
|
||||
- Schema version for any schema change: {{SCHEMA_VERSION}}
|
||||
If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report.
|
||||
|
||||
Orchestrator notes for this run:
|
||||
(To fill: Confirm SPEC v0.9 has landed. M6 runs in parallel in `android/core/design/**`; Gradle builds are serialised by `tools/gradle-serial.sh`. No phone is needed.)
|
||||
{{ORCHESTRATOR_NOTES}}
|
||||
|
||||
Essentials (the plan has the detail):
|
||||
- First step: from the main checkout, `git worktree add .claude/worktrees/agent-m12d -b agent-m12d main`; work only there, on branch `agent-m12d`; then copy the git-ignored files the plan names.
|
||||
- Edit only the Android paths of PLAN-M12d §4 (`:core:model`, `:data`, `:core:fakes` outside the `fakes/<feature>/` folders); anything else is a contract request to the orchestrator.
|
||||
- Gate: `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check` in `android/`. Report only results you ran in this session.
|
||||
- Stop and ask when the plan says so, when a step could touch the owner's real data, or when the spec is ambiguous in a way that changes a decision.
|
||||
- Commit on your branch when the gate is green, `git rebase main`, rerun the gate. Never merge, push or remove the worktree.
|
||||
|
||||
Report: end with the report described in PLAN-M12d.md §10: branch, head commit hash, gate result lines, versions chosen with sources, numbering used, how the orchestrator notes were applied, HUMAN steps outstanding (worded for relay), contract requests, open questions and spec gaps with section numbers.
|
||||
+15
-14
@@ -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.8). Status as of 2026-10-18: M0 done, M1 landed (`c6df2ab`), M2c-dev landed (`c885aba`), M2a landed (`fdf2093`), M2b landed (`fad6fc9`), M3 landed (`503cee5`), M5 landed (`c28c52b`), M4 landed (`bd6f10a`), M9 landed (`ab298ab`); SPEC v0.7 landed (`e8ee518`); M12a landed (`6b60ef7`); U1 done on `UX-sprint`, direction 3 "Kalas" muted picked (D-047), U2 in progress there. 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.
|
||||
Dependencies follow SPEC §11.1 and §11.2 (v0.9). Status as of 2026-10-18: M0 done, M1 landed (`c6df2ab`), M2c-dev landed (`c885aba`), M2a landed (`fdf2093`), M2b landed (`fad6fc9`), M3 landed (`503cee5`), M5 landed (`c28c52b`), M4 landed (`bd6f10a`), M9 landed (`ab298ab`); SPEC v0.7 landed (`e8ee518`); M12a landed (`6b60ef7`); SPEC v0.8 landed (`7fb9f6f`); M12c landed (`e499107`); M12b landed (`2e2ebe7`); U1 and U2 landed (`16c4d01`, owner approved); M6 and M12d started 2026-10-18; SPEC v0.9 records the M12b and M12c outcomes (D-055 to D-058), the §9.7 values and UX-9 (D-059). The plans' open points are resolved in SPEC v0.3 and recorded in each plan's §4; v0.4 records the M2a and M2b decisions (D-005 to D-014); v0.5 the M3 and M5 decisions (D-026 to D-034, D-040 to D-043); v0.6 the M4 decisions (D-035 to D-039); v0.7 the M9 decisions (D-044 to D-046); v0.8 the owner's UX-session decisions (D-047 to D-054) and the milestone group M12; v0.9 D-055 to D-059 and M12d.
|
||||
|
||||
## Dependency order and waves
|
||||
|
||||
@@ -11,24 +11,25 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7
|
||||
| Wave | Plan | Prompt | Depends on | Parallel with |
|
||||
|---|---|---|---|---|
|
||||
| 0 | M1 Contracts and vectors | `M1.md` | M0 | done (`c6df2ab`) |
|
||||
| 0 | U1 UX directions | `U1.md` | M0 | everything in waves 0 to 2 (owner's session) |
|
||||
| 0 | U1 UX directions | `U1.md` | M0 | done (`16c4d01`) |
|
||||
| 1 | M2a Server skeleton and `kvitt-domain` | `M2a.md` | M1 | done (`fdf2093`) |
|
||||
| 1 | M2b Android skeleton and `:domain` | `M2b.md` | M1 | done (`fad6fc9`); `installDebug` on the phone outstanding |
|
||||
| 1 | M2c-dev Infra for development | `M2c-dev.md` | M0 (its health check needs M2a's image) | done (`c885aba`) |
|
||||
| 1 | U2 UX design spec | `U2.md` | U1 and the owner's pick | M2a, M2b, M2c-dev, wave 2 |
|
||||
| 1 | U2 UX design spec | `U2.md` | U1 and the owner's pick | done (`16c4d01`) |
|
||||
| 2 | M3 Server core API | `M3.md` | M2a | done (`503cee5`) |
|
||||
| 2 | M4 Server sync, audit, push, scheduler | `M4.md` | M2a; lands after M3 (D-015) | done (`bd6f10a`) |
|
||||
| 2 | M5 Android data layer | `M5.md` | M1, M2b | done (`c28c52b`) |
|
||||
| 2 | M12a Category contract v2: contracts | `M12a.md` | M1, SPEC v0.8 | done (`6b60ef7`) |
|
||||
| 2 | M12b Category contract v2: server | `M12b.md` | M12a (M3, M4, M9 landed) | M12c, U2 |
|
||||
| 2 | M12c Category contract v2: Android | `M12c.md` | M12a (M2b, M5 landed) | M12b, U2 |
|
||||
| 2 | M6 Design system | `M6.md` | U2 approved, M2b, M12c, the §9.7 colour spec change | M12b |
|
||||
| 3 | M7a Feature onboarding | `M7a.md` | M6 | M7b to M7f, M9 |
|
||||
| 3 | M7b Feature home and account | `M7b.md` | M6 | M7a, M7c to M7f, M9 |
|
||||
| 3 | M7c Feature expense | `M7c.md` | M6, M12c | M7a, M7b, M7d to M7f, M9 |
|
||||
| 3 | M7d Feature settle | `M7d.md` | M6 | M7a to M7c, M7e, M7f, M9 |
|
||||
| 3 | M7e Feature analytics | `M7e.md` | M6 | M7a to M7d, M7f, M9 |
|
||||
| 3 | M7f Feature settings | `M7f.md` | M6 | M7a to M7e, M9 |
|
||||
| 2 | M12b Category contract v2: server | `M12b.md` | M12a (M3, M4, M9 landed) | done (`2e2ebe7`) |
|
||||
| 2 | M12c Category contract v2: Android | `M12c.md` | M12a (M2b, M5 landed) | done (`e499107`) |
|
||||
| 2 | M12d UI contract additions (UX-9) | `M12d.md` | M12c, SPEC v0.9 | M6 |
|
||||
| 2 | M6 Design system | `M6.md` | U2 approved, M2b, M12c, the §9.7 colour spec change (v0.9) | M12d |
|
||||
| 3 | M7a Feature onboarding | `M7a.md` | M6, M12d | M7b to M7f, M9 |
|
||||
| 3 | M7b Feature home and account | `M7b.md` | M6, M12d | M7a, M7c to M7f, M9 |
|
||||
| 3 | M7c Feature expense | `M7c.md` | M6, M12c, M12d | M7a, M7b, M7d to M7f, M9 |
|
||||
| 3 | M7d Feature settle | `M7d.md` | M6, M12d | M7a to M7c, M7e, M7f, M9 |
|
||||
| 3 | M7e Feature analytics | `M7e.md` | M6, M12d | M7a to M7d, M7f, M9 |
|
||||
| 3 | M7f Feature settings | `M7f.md` | M6, M12d | M7a to M7e, M9 |
|
||||
| 3 | M9 Splitwise importer | `M9.md` | M2a, M4 | M5, M6, M7a to M7f |
|
||||
| 4 | M8 App integration | `M8.md` | M3, M4, M5, M7a to M7f, M2c-dev | M9 if still running |
|
||||
| 5 | M2c-pi Infra on the Pi | `M2c-pi.md` | M2c-dev, M8; OQ-3 answered | none needed |
|
||||
@@ -40,7 +41,7 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7
|
||||
- **M3 and M4** (D-015): M2a provides `next_occurrence_date`, the `AuditWriter`/`PushQueue` traits with no-op implementations and `tasks::start`; M3 writes the recurring generation function and calls the traits; M4 replaces the implementations, its scheduler calls M3's function, and M4 lands after M3.
|
||||
- **M2a and M2b** create shell files and modules in later milestones' paths (D-004); ownership passes on when they land.
|
||||
- **M7a to M7f** follow the feature convention of SPEC §6.2 (D-017) and take the ledger rows and expense editor fields from M6's `:core:design` (D-019).
|
||||
- **M6** starts only after the §9.7 colour change from U2's proposal (the muted Kalas values, `slate` and `amber` included, D-051) has landed in SPEC and M12c has landed (§11.1).
|
||||
- **M12** (D-054): SPEC v0.8 changes the §10.8 table that the importer test `category_table_matches_the_spec` parses, so the server gate on `main` is red from SPEC v0.8 until M12b lands; land M12b soon after (or land SPEC v0.8 together with it).
|
||||
- **M6** started 2026-10-18 after the §9.7 values landed (SPEC v0.9, D-051) and M12c landed; it builds from `docs/ux/design-spec.md` §2 and §3 and `strings.csv`.
|
||||
- **M12** (D-054): done; the importer test `category_table_matches_the_spec` is green again since M12b (`2e2ebe7`). M12d (D-059) runs beside M6 in disjoint paths; M7a to M7f wait for both.
|
||||
- **Phone:** M2b (one `installDebug`), M5 (only if instrumented tests), M8 and M10 use the phone; only one at a time, named in the notes.
|
||||
- **Outside the repository:** M2c-dev (D-022, done), M2c-pi (the Kvitt Pi, the Home Assistant Pi, Gitea, Drive: allowances requested at the start of that sprint, D-023), M8 (dev server on the Mac), M10 and M11 (production) need the owner's allowance in the notes.
|
||||
|
||||
@@ -19,16 +19,13 @@ async fn e7_returns_user_device_household_and_public_url() {
|
||||
.insert("x-kvitt-app-version", "41".parse().unwrap());
|
||||
let reply = app.send(request).await;
|
||||
assert_eq!(reply.status, 200);
|
||||
// `server_version` is volatile here: the example's users have 11 and 12 (schema 1, after ten seeded
|
||||
// categories); since migration 0002 seeds fourteen, the harness's users have 15 and 16 (SPEC §14.1.1).
|
||||
// The harness seeds the users after the fourteen category seeds, so they have `server_version` 15 and
|
||||
// 16, as the example shows (D-058); the example's value is compared.
|
||||
assert_like_example(
|
||||
&reply.json,
|
||||
"E-7-me/response.json",
|
||||
&["created_at", "updated_at", "last_seen_at", "server_version"],
|
||||
&["created_at", "updated_at", "last_seen_at"],
|
||||
);
|
||||
assert_eq!(reply.json["user"]["server_version"], 15);
|
||||
assert_eq!(reply.json["household"][0]["server_version"], 15);
|
||||
assert_eq!(reply.json["household"][1]["server_version"], 16);
|
||||
assert_eq!(reply.json["device"]["id"], ADMIN_DEVICE_ID);
|
||||
assert_eq!(reply.json["device"]["is_current"], true);
|
||||
let member = app.call(Method::GET, "/me", &member_token(), None).await;
|
||||
|
||||
Reference in New Issue
Block a user