- 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
7.0 KiB
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
- 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. - 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). - SPEC §11.2 (row M12d), §11.3 (note M12d), §14.7 (row D-059).
docs/decisions.mdD-018 (empty scenario), D-054, D-059.docs/ux/proposed-decisions.mdUX-9 anddocs/ux/design-spec.md§3.7 (CategoryQuickRow), §4 S-15, §6 open points 1 and 2 (for context; the spec text wins).- Files:
android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Expenses.ktandSettings.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:datatest harness;android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/FakeExpenseRepository.kt,FakeSettingsRepositories.kt,FakeRepositories.kt,FakeScenarios.kt;android/README.md. CLAUDE.md(Android gate, layering, no mocking libraries, no silencing).
3. Already established
ExpenseEditorOptionsgainsval frequentCategoryIds: Map<AccountId, List<CategoryId>>as its last parameter;CategoryRepositorygainsfun deletable(id: CategoryId): Flow<Boolean>(§14.2.4, D-059). Nothing else in:core:modelchanges.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 bysort_orderascending; 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. TheeditorOptions()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 (deletecan still returnFailed("category_in_use")orFailed("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;ContractTestupdated.:data:frequentCategoryIdsin the expense repository'seditorOptions();deletablein the category repository; repository tests over Room: ordering by count, tie bysort_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;deletablefalse 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
ExpenseEditorOptionsupdated, each listed in the report.
6. Steps
- 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. Copyandroid/local.propertiesfrom the main checkout (addsdk.dir=/opt/homebrew/share/android-commandlinetoolsif missing; never overwrite or commit). Runtools/install-git-hooks.sh. - Read §2 in order. Confirm SPEC v0.9 is on
main(§14.2.4 "Changes in v0.9"); if not, stop and report. - Run
python3 tools/dependency-report/latest.py; record the lines. No new library is expected. :core:modelchanges andContractTest; build.:core:fakes: both members and their test.:data: queries, both members, repository tests.- 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 onmain. - Gate: BUILD line, actionable tasks, test counts from fresh XML.
- Dependency-report lines.
- Callers and tests changed for the new
ExpenseEditorOptionsparameter, each with its reason. - Proposed decision entries (never written to
SPEC.mdordocs/decisions.mdby this agent) orTODO(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).