Files
kvitt/PLAN-M12d.md
T
jacobh2 e6768121d6 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
2026-10-18 08:34:45 +02:00

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

  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).