Files
kvitt/PLAN-M12a.md

73 lines
8.4 KiB
Markdown

# PLAN-M12a: Category contract v2, contracts
On record only: M12a ran in parallel with the SPEC v0.8 docs change, from the UX-session documents, and landed on `main` as `aa765bb`. What landed wins where this plan differs: the suggestion records and the `UnicodeText` interface live in `:domain` (M12a also wrote `:domain`'s `suggestCategory`), the vectors include non-NFC strings, and the OpenAPI `DefaultCategoryKey` extension and the new category examples were held back for M12b (SPEC §11.2, §14.5.6).
## 1. Goal
Write the contract files of the category contract v2 (D-049, D-054) so that M12b (server) and M12c (Android) can work in parallel against them: the keyword table, the locked text of server migration 0002, the `suggest_category` test vectors, the fourteen-key enum in the OpenAPI document, the examples that change, and the checker extended to verify all of it, including a stdlib-only Python reference of §9.11.
## 2. Read first
1. SPEC §9.11 Category suggestion (CS-1 to CS-7), all of it.
2. SPEC §14.5.5 Test-vector format (the `suggest_category` keys, output and minimum cases) and §14.5.6 Category suggestion.
3. SPEC §14.8 (the `contracts/category-keywords.csv` rules and the contract-file list), §14.1 and §14.1.1 Migration 0002.
4. SPEC AC-7.2 (fourteen defaults), §9.7 (33 icons, 14 colours), §14.3.3 Category, §14.3.4 E-1.
5. SPEC §11.2 (row M12a), §11.3 (note M12), §14.7.
6. `docs/decisions.md` D-049 and D-054.
7. Files: `tools/contracts-check.py` (whole file: how vectors, the reference functions and the §14.1 byte check are organised), `contracts/openapi.yaml` (`DefaultCategoryKey`), `contracts/examples/E-1-health/response.json`, the examples under `contracts/examples/` that hold categories (`E-23` to `E-27`, `E-31-sync-pull`, `sync/pull-page.json`), and the source table `docs/ux/category-keywords.csv` (148 entries) as it is on `main`. If `docs/ux/category-keywords.csv` is not on `main` yet, take it from the branch `UX-sprint` with `git show UX-sprint:docs/ux/category-keywords.csv` (read only; never edit that branch).
8. `CLAUDE.md`.
## 3. Already established
- Fourteen default keys in AC-7.2 order: `groceries`, `restaurants`, `housing`, `home`, `utilities`, `insurance`, `transport`, `travel`, `entertainment`, `health`, `children`, `shopping`, `fees`, `other` (D-054).
- New ids `01920000-0000-7000-8000-000000000011` (`housing`), `…012` (`insurance`), `…013` (`children`), `…014` (`fees`); `created_at` = `updated_at` = `2026-10-18T00:00:00.000Z`; `server_version` taken from the global sequence by the migration (§14.1.1).
- New icons `key`, `shield`, `account_balance` (appended, 33 in all); new colour tokens `slate`, `amber` (14 in all); their hex values are `TODO(U2)` and are not needed by this plan (D-051).
- `contracts/schema/server-v1.sql` stays byte-locked; `contracts/schema/migrations/0002_categories_v2.sql` is the SQL block of §14.1.1 byte for byte (§14.1.1, §14.8).
- `contracts/category-keywords.csv`: columns `keyword,match,default_key,source`, the rules of §14.8, seeded from `docs/ux/category-keywords.csv` unchanged (that draft already satisfies the rules: normalised, unique, sorted, phrase rule, no `other`).
- `suggest_category.json` input keys: `description`, `history` (`description`, `category_id`, `expense_date`, `created_at`), `keywords` (`keyword`, `match`, `default_key`), `active_categories` (`id`, `default_key`), `fallback`; output `{"ok": id}` or `{"ok": null}`; every string NFC (§14.5.5).
- E-1's `schema_version` becomes 2 (§14.3.4).
- The Room schema is unchanged (§14.1.1).
## 4. Owned paths
From SPEC §14.7: `contracts/**`, `tools/contracts-check.py` (owner M1; after landing "contract-change plans only (§14.8)"); M12a is that contract-change plan (§14.7 row M12a, D-054). Concretely: `contracts/category-keywords.csv`, `contracts/schema/migrations/0002_categories_v2.sql`, `contracts/test-vectors/suggest_category.json`, `contracts/openapi.yaml` (the `DefaultCategoryKey` enum only), `contracts/examples/**` (only where §2 item 7 shows a change is needed), `tools/contracts-check.py`.
Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. Never edit `docs/ux/**`.
## 5. Deliverables
- `contracts/category-keywords.csv`: the 148 entries of the draft, byte-identical to it unless a §14.8 rule fails (report any change).
- `contracts/schema/migrations/0002_categories_v2.sql`: the §14.1.1 SQL block, byte for byte.
- `contracts/test-vectors/suggest_category.json`: format 1, `"function": "suggest_category"`, at least the §14.5.5 minimum cases.
- `contracts/openapi.yaml`: `DefaultCategoryKey` with the fourteen keys (a semantic change, recorded by D-054).
- `contracts/examples/E-1-health/response.json`: `schema_version` 2; other examples only if they enumerate the default categories.
- `tools/contracts-check.py` extended (stdlib only): (a) the §14.8 rules for `category-keywords.csv`; (b) `0002_categories_v2.sql` byte-identical to the second SQL block of SPEC §14.1 (the one under `#### 14.1.1`); (c) in an in-memory SQLite with `PRAGMA foreign_keys = ON`: load `server-v1.sql`, insert a user, an account, a custom category and expenses referencing a default and the custom category, run 0002 inside `BEGIN`/`COMMIT`; assert fourteen default rows with the AC-7.2 ids, keys, icons, colours and sort orders, the custom category and every expense unchanged, `PRAGMA foreign_key_check` empty, `meta.schema_version` = `2`, the four new `server_version`s strictly greater than every earlier one; (d) a Python reference of §9.11 and §14.5.6 (NFC via `unicodedata`, general categories via `unicodedata.category`) recomputing every `suggest_category` case, plus a check that every string in the file is NFC; (e) the vector file is listed with the other Kotlin-only files.
## 6. Steps
1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m12a -b agent-m12a main`, then in `.claude/worktrees/agent-m12a`: `git reset --hard main`. No git-ignored files are needed. Run `tools/install-git-hooks.sh`.
2. Read §2 in order. Confirm SPEC.md on `main` is v0.8 (header) with §9.11 and §14.1.1; if not, stop and report.
3. Copy the keyword draft into `contracts/category-keywords.csv`; write checker part (a) and run it.
4. Extract the §14.1.1 block into `contracts/schema/migrations/0002_categories_v2.sql`; write checker parts (b) and (c); run.
5. Write the Python reference of §9.11 (part d) first, then the vector cases one group at a time, running the checker after each group. Hand-check every expected value against §9.11 before trusting the reference.
6. Update `DefaultCategoryKey` in `contracts/openapi.yaml` and the E-1 example; scan the other examples (§2 item 7) and change only those that enumerate the defaults.
7. Run the gate (§7). Commit on `agent-m12a`, `git rebase main`, run the gate again. Never merge, push or remove the worktree.
## 7. Gate
From SPEC §11.2, verbatim: "`python3 tools/contracts-check.py` passes".
Command: `python3 tools/contracts-check.py` from the repository root. Green means exit code 0 and its summary line reporting zero failures, with the new checks (a) to (e) listed in its output.
## 8. HUMAN steps
From SPEC §11.2, verbatim: "none".
## 9. Out of scope
- Any Rust or Kotlin code: the server migration file, `SCHEMA_VERSION`, token lists and importer table (M12b); `:core:model`, `:domain`, `:data`, fakes (M12c).
- The colour hex values of `slate` and `amber` (U2, then a docs agent).
- `contracts/schema/server-v1.sql` (byte-locked).
- `SPEC.md`, `docs/`, `CLAUDE.md`; a §9.11 ambiguity is a gap for the report, not something to resolve in the vectors.
## 10. Report
- Branch `agent-m12a`, head commit hash after rebasing on `main`.
- Gate: the checker's summary lines from this session, with the new checks named.
- `suggest_category` case count and the list of case names; whether the keyword file is byte-identical to the draft.
- Examples changed, and why each other category example needed no change.
- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences.
- Open questions and spec gaps with section numbers (any §9.11 case the text does not decide).
## 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. This plan's own changes are already recorded as D-049 and D-054 in SPEC v0.8; the server schema version it describes is 2.