Files
kvitt/PLAN-M12c.md
T
jacobh2 d39eb2c79b 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
2026-10-18 08:14:12 +02:00

8.2 KiB

PLAN-M12c: Category contract v2, Android

1. Goal

Bring the landed Android contract and data layer to the category contract v2 (D-049, D-054): fourteen default categories, two more colour tokens and three more icons in :core:model; ExpenseRepository.suggestCategory in :data, calling the pure suggestCategory that M12a already landed in :domain (6b60ef7) with a platform UnicodeText binding and the keyword table bundled; fakes with the fourteen defaults. After this lands, M6 and M7c build on it.

2. Read first

  1. SPEC §9.11 Category suggestion (CS-1 to CS-7), all of it.
  2. SPEC §14.5.6 Category suggestion (types and signatures in :domain, UnicodeText), and the landed android/domain/src/main/kotlin/se/hagstedt/kvitt/domain/CategorySuggestion.kt with its test binding android/domain/src/test/kotlin/se/hagstedt/kvitt/domain/JvmUnicodeText.kt.
  3. SPEC §14.2.4: the enums, CategoryIcons, ExpenseRepository and "Changes in v0.8"; §14.2.2 (DataStore preferences: last used category per account).
  4. SPEC AC-7.2, AC-8.6, AC-8.10 (list order used for ties), §9.7, §6.2 (layering, pure modules, fakes), §14.8 (category-keywords.csv).
  5. SPEC §11.2 (row M12c), §11.3 (note M12), §14.7 (row M12c).
  6. docs/decisions.md D-049, D-054.
  7. Files: contracts/category-keywords.csv and contracts/test-vectors/suggest_category.json (from M12a), android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Enums.kt and the repository interfaces, android/core/model/src/test/.../ContractTest.kt, the :domain vector tests (how they load contracts/test-vectors/), android/data/ (the expense repository, DAOs, the DataStore preferences for the last-used category, the test harness), android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/FakeScenarios.kt and the fake expense repository, android/README.md.
  8. CLAUDE.md (Android gate, layering, no mocking libraries, no silencing).

3. Already established

  • DefaultCategoryKey in AC-7.2 order (14), ColorToken + SLATE, AMBER, CategoryIcons.ALL + key, shield, account_balance at the end (33) (§14.2.4). Wire value = lowercase name.
  • New default ids 01920000-0000-7000-8000-000000000011 to …014 (housing, insurance, children, fees), sort orders 25, 45, 85, 95 (AC-7.2, §14.1.1).
  • Landed in :domain by M12a (6b60ef7), not to be rewritten: SuggestionHistoryEntry, KeywordMatch, CategoryKeyword, ActiveCategory, UnicodeText, normalizeDescription, suggestCategory(description, history, keywords, activeCategories, fallback, unicode): CategoryId?, passing suggest_category.json (§14.5.6).
  • :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 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).

4. Owned paths

From SPEC §14.7 (row M12c, a contract-change plan): android/core/model/** (enums, icons, ExpenseRepository), android/data/** (UnicodeText binding, suggestCategory, the bundled keyword resource, tests), android/core/fakes/** (defaults, fake suggestCategory); plus the default-name string files of §3 last item, only if they already exist.

Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.

5. Deliverables

  • :core:model: the enum, token and icon changes; suggestCategory on ExpenseRepository; ContractTest updated (33 icons, 14 colours, 14 keys).
  • :data: the production UnicodeText with a test against the :domain test binding's behaviour (NFC, letters, Nd digits, a supplementary code point); the resource file, a test that it equals contracts/category-keywords.csv byte for byte and satisfies the §14.8 rules; suggestCategory in the expense repository (history query, active categories, fallback per AC-8.6, the UnicodeText binding); repository tests over Room: exact match, first token, keyword, fallback, archived category skipped, deleted expenses ignored, pending expenses counted, accountId null.
  • :core:fakes: fourteen defaults in the empty scenario with the AC-7.2 ids and values; fake suggestCategory.
  • Every existing test that assumed ten defaults or 30 icons updated, each with the reason in the report.

6. Steps

  1. Worktree. From the main checkout /Users/jhag/projects/kvitt: git worktree add .claude/worktrees/agent-m12c -b agent-m12c main, then in .claude/worktrees/agent-m12c: git reset --hard main. Copy android/local.properties from the main checkout (add sdk.dir=/opt/homebrew/share/android-commandlinetools if missing; never overwrite or commit). Run tools/install-git-hooks.sh.
  2. Read §2 in order. Confirm M12a has landed (6b60ef7: contracts/category-keywords.csv and :domain's CategorySuggestion.kt on main); if not, stop and report.
  3. Run python3 tools/dependency-report/latest.py; record the lines. No new library is expected.
  4. :core:model changes and ContractTest; build.
  5. :domain: nothing to write; confirm the suggest_category vector test passes in the gate.
  6. :core:fakes: fourteen defaults, fake suggestCategory.
  7. :data: resource and its test, then suggestCategory and its repository tests.
  8. String resources per §3, if they exist.
  9. Run the gate (§7). Commit on agent-m12c, git rebase main, 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. Green means: BUILD SUCCESSFUL; test counts from fresh XML show 0 failures and include the :domain suggest_category vector test (from M12a), the UnicodeText and resource identity tests and the repository tests.

8. HUMAN steps

From SPEC §11.2, verbatim: "none".

9. Out of scope

  • Any UI: the editor's "picked by hand" flag and the calls on description and account changes (M7c), category tiles and colours (M6), the categories screen (M7f).
  • Colour hex values (U2, M6).
  • A Room migration (none is needed); contracts/** (M12a); the server (M12b).
  • Applying the suggestion to the recurring template editor (CS-1).

10. Report

  • Branch agent-m12c, head commit hash after rebasing on main.
  • Gate: BUILD line, actionable tasks, test counts from fresh XML.
  • Dependency-report lines.
  • Whether default-name string resources existed and what was added (§3).
  • Tests changed because they assumed ten defaults or 30 icons, 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).