Files
kvitt/PLAN-M12c.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

74 lines
8.3 KiB
Markdown

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