SPEC v0.8: UX-session decisions (D-047 to D-054), category contract v2, M12 plans

Records the owner's UX-sprint decisions UX-1 to UX-8 of docs/ux/proposed-decisions.md:
D-047 direction Kalas muted, D-048 three tabs, D-049 category suggestion (§9.11,
suggestCategory, contracts/category-keywords.csv, suggest_category.json), D-050 S-1
display-name field, D-051 colour token values from Kalas (slate, amber TODO(U2)),
D-052 smaller UX rules, D-053 finer Splitwise mapping, D-054 fourteen default
categories with server schema 2 (migration 0002, §14.1.1). Adds milestone group M12
(PLAN-M12a/b/c, prompts), the CLAUDE.md UX-sprint note, and rewrites HANDOFF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 08:06:45 +02:00
parent aa765bbb48
commit d39eb2c79b
15 changed files with 553 additions and 68 deletions
+9 -1
View File
@@ -50,4 +50,12 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-043 | 2026-10-17 | Two extra DataStore keys: the pairing status in `auth.preferences_pb`, and the installed version code that received a 426 in `prefs.preferences_pb` (syncing stops until the installed version changes). The token is encrypted with Keystore key `kvitt_token_key`, AES-256-GCM (as §8.2). (M5's D-033, renumbered.) | M5 report. | SPEC §8.2, §14.2.2, §14.4.5 |
| D-044 | 2026-10-18 | Nine Swedish Splitwise category names, found unmapped in the importer rehearsal on the real Swedish export, are added to the §10.8 table and to `server/crates/kvitt-import/data/splitwise_categories.csv` with the key of their English equivalent: Städning → `utilities` (Cleaning); Verktyg - Övrigt → `utilities` (Utilities - Other); Tjänster → `home` (Services); Cykel → `transport` (Bicycle); Flyg → `travel` (Plane); Spel → `entertainment` (Games); Kläder → `shopping` (Clothing); Livet - Övrigt → `other` (Life - Other); Utbildning → `other` (Education). The test `category_table_matches_the_spec` keeps the two in step. | The rehearsal of 2026-10-18 left 21 rows of the Vardag file as `other`, counted unmapped, only because these names were missing from D-003's table. | SPEC §10.8; `server/crates/kvitt-import/data/splitwise_categories.csv` |
| D-045 | 2026-10-18 | Import reconciliation. Inside the import transaction, after the rows are written, each user's balance of the whole account (`net(u)`, §9.3) must equal the sum of that member's values in the file's totals rows (`Total balance` / `Totalsumma`), one totals row allowed per currency, a non-SEK one converted with its `--fx` rate. A mismatch or a missing totals row rolls the run back and exits 4; a dry run reports the same. The comparison covers the whole account, so importing into an account that already holds other rows rolls back. When any `--fx` conversion took place, a mismatch is reported in the summary but accepted (per-value rounding can make an exact match impossible). | M9 report (`ab298ab`): v0.6 ignored the totals row, and M11's gate ("imported balances equal Splitwise's totals") is cheapest checked by the importer itself, before anything is committed. | SPEC §10.8, AC-17.5, §11.2 (M11) |
| D-046 | 2026-10-18 | Importer CLI semantics. `--user <Splitwise name>=<user>` takes a user id or an exact display name that exactly one user has; the two arguments must name the two household users. The importer never creates or migrates a database and requires `meta.schema_version` = `1`. A dry run runs the full transaction and rolls it back. Exit codes: 0 ok, 1 database or file error, 2 usage, 3 cannot import (header, invalid rows, non-SEK without `--fx`), 4 reconciliation mismatch. The import key uses an existing account's stored name, so a re-run with another spelling finds its rows. An empty description or one over 1000 code points makes the row invalid; a description over 100 code points is cut with the full text in `notes`; settlement notes are cut to 200 code points; both nets zero means the owner pays in mode `full`. Idempotency uses the existing `import_key` columns; no schema change. | M9 report (`ab298ab`): §10.8 left the user lookup, database handling, dry-run mechanics, exit codes and these edge cases open. | SPEC §10.8, AC-17.5; server/README.md |
| D-046 | 2026-10-18 | Importer CLI semantics. `--user <Splitwise name>=<user>` takes a user id or an exact display name that exactly one user has; the two arguments must name the two household users. The importer never creates or migrates a database and requires `meta.schema_version` = `1` (revised by D-054: the schema version the server's migrations produce, now `2`). A dry run runs the full transaction and rolls it back. Exit codes: 0 ok, 1 database or file error, 2 usage, 3 cannot import (header, invalid rows, non-SEK without `--fx`), 4 reconciliation mismatch. The import key uses an existing account's stored name, so a re-run with another spelling finds its rows. An empty description or one over 1000 code points makes the row invalid; a description over 100 code points is cut with the full text in `notes`; settlement notes are cut to 200 code points; both nets zero means the owner pays in mode `full`. Idempotency uses the existing `import_key` columns; no schema change. | M9 report (`ab298ab`): §10.8 left the user lookup, database handling, dry-run mechanics, exit codes and these edge cases open. | SPEC §10.8, AC-17.5; server/README.md |
| D-047 | 2026-10-18 | Visual direction: U1's direction 3 "Kalas" (cards, very round shapes, a solid category-colour tile on every expense, the seesaw balance) with the muted palette of `docs/ux/directions/3-kalas/` (owner approved 2026-10-18). Typeface per D-020 (Material 3 default); Kalas's preview font Nunito is dropped. U2's `design-spec.md` carries the tokens. | Owner's pick (UX-1); muted because bright colours tire over years of daily use. | SPEC §9.7, §13.4 |
| 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-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 |