- 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
8.3 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
- SPEC §9.11 Category suggestion (CS-1 to CS-7), all of it.
- SPEC §14.5.6 Category suggestion (types and signatures in
:domain,UnicodeText), and the landedandroid/domain/src/main/kotlin/se/hagstedt/kvitt/domain/CategorySuggestion.ktwith its test bindingandroid/domain/src/test/kotlin/se/hagstedt/kvitt/domain/JvmUnicodeText.kt. - SPEC §14.2.4: the enums,
CategoryIcons,ExpenseRepositoryand "Changes in v0.8"; §14.2.2 (DataStore preferences: last used category per account). - 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). - SPEC §11.2 (row M12c), §11.3 (note M12), §14.7 (row M12c).
docs/decisions.mdD-049, D-054.- Files:
contracts/category-keywords.csvandcontracts/test-vectors/suggest_category.json(from M12a),android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Enums.ktand the repository interfaces,android/core/model/src/test/.../ContractTest.kt, the:domainvector tests (how they loadcontracts/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.ktand the fake expense repository,android/README.md. CLAUDE.md(Android gate, layering, no mocking libraries, no silencing).
3. Already established
DefaultCategoryKeyin AC-7.2 order (14),ColorToken+SLATE,AMBER,CategoryIcons.ALL+key,shield,account_balanceat the end (33) (§14.2.4). Wire value = lowercase name.- New default ids
01920000-0000-7000-8000-000000000011to…014(housing, insurance, children, fees), sort orders 25, 45, 85, 95 (AC-7.2, §14.1.1). - Landed in
:domainby M12a (6b60ef7), not to be rewritten:SuggestionHistoryEntry,KeywordMatch,CategoryKeyword,ActiveCategory,UnicodeText,normalizeDescription,suggestCategory(description, history, keywords, activeCategories, fallback, unicode): CategoryId?, passingsuggest_category.json(§14.5.6). :domainmay not usejava.*, so:dataprovides the productionUnicodeText(java.text.NormalizerNFC,Character.getTypefor L and Nd), the same behaviour as the:domaintest 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, elseother;accountIdnull givesother); 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 ine499107, D-055: copied at build time by:data:copyCategoryKeywordsand:core:fakes:copyCategoryKeywords, no committed copy; parsed by:domain'sparseCategoryKeywords, 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
suggestCategoryfollows §9.11 over the fake's own data by calling the same:domainfunction (:core:fakesmay depend on:domain, §6.2). - Default category names (AC-7.2) are shown from string resources. At
e8ee518no module holds default category name strings;:core:designwill hold them (M6, fromdocs/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 fromstrings.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;suggestCategoryonExpenseRepository;ContractTestupdated (33 icons, 14 colours, 14 keys).:data: the productionUnicodeTextwith a test against the:domaintest binding's behaviour (NFC, letters, Nd digits, a supplementary code point); the resource file, a test that it equalscontracts/category-keywords.csvbyte for byte and satisfies the §14.8 rules;suggestCategoryin the expense repository (history query, active categories, fallback per AC-8.6, theUnicodeTextbinding); repository tests over Room: exact match, first token, keyword, fallback, archived category skipped, deleted expenses ignored, pending expenses counted,accountIdnull.:core:fakes: fourteen defaults in the empty scenario with the AC-7.2 ids and values; fakesuggestCategory.- Every existing test that assumed ten defaults or 30 icons updated, each with the reason in the report.
6. Steps
- 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. 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 M12a has landed (
6b60ef7:contracts/category-keywords.csvand:domain'sCategorySuggestion.ktonmain); if not, stop and report. - Run
python3 tools/dependency-report/latest.py; record the lines. No new library is expected. :core:modelchanges andContractTest; build.:domain: nothing to write; confirm thesuggest_categoryvector test passes in the gate.:core:fakes: fourteen defaults, fakesuggestCategory.:data: resource and its test, thensuggestCategoryand its repository tests.- String resources per §3, if they exist.
- 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 onmain. - 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.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).