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