Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
8.4 KiB
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
- SPEC §9.11 Category suggestion (CS-1 to CS-7), all of it.
- SPEC §14.5.5 Test-vector format (the
suggest_categorykeys, output and minimum cases) and §14.5.6 Category suggestion. - SPEC §14.8 (the
contracts/category-keywords.csvrules and the contract-file list), §14.1 and §14.1.1 Migration 0002. - SPEC AC-7.2 (fourteen defaults), §9.7 (33 icons, 14 colours), §14.3.3 Category, §14.3.4 E-1.
- SPEC §11.2 (row M12a), §11.3 (note M12), §14.7.
docs/decisions.mdD-049 and D-054.- 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 undercontracts/examples/that hold categories (E-23toE-27,E-31-sync-pull,sync/pull-page.json), and the source tabledocs/ux/category-keywords.csv(148 entries) as it is onmain. Ifdocs/ux/category-keywords.csvis not onmainyet, take it from the branchUX-sprintwithgit show UX-sprint:docs/ux/category-keywords.csv(read only; never edit that branch). 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_versiontaken from the global sequence by the migration (§14.1.1). - New icons
key,shield,account_balance(appended, 33 in all); new colour tokensslate,amber(14 in all); their hex values areTODO(U2)and are not needed by this plan (D-051). contracts/schema/server-v1.sqlstays byte-locked;contracts/schema/migrations/0002_categories_v2.sqlis the SQL block of §14.1.1 byte for byte (§14.1.1, §14.8).contracts/category-keywords.csv: columnskeyword,match,default_key,source, the rules of §14.8, seeded fromdocs/ux/category-keywords.csvunchanged (that draft already satisfies the rules: normalised, unique, sorted, phrase rule, noother).suggest_category.jsoninput 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_versionbecomes 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:DefaultCategoryKeywith the fourteen keys (a semantic change, recorded by D-054).contracts/examples/E-1-health/response.json:schema_version2; other examples only if they enumerate the default categories.tools/contracts-check.pyextended (stdlib only): (a) the §14.8 rules forcategory-keywords.csv; (b)0002_categories_v2.sqlbyte-identical to the second SQL block of SPEC §14.1 (the one under#### 14.1.1); (c) in an in-memory SQLite withPRAGMA foreign_keys = ON: loadserver-v1.sql, insert a user, an account, a custom category and expenses referencing a default and the custom category, run 0002 insideBEGIN/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_checkempty,meta.schema_version=2, the four newserver_versions strictly greater than every earlier one; (d) a Python reference of §9.11 and §14.5.6 (NFC viaunicodedata, general categories viaunicodedata.category) recomputing everysuggest_categorycase, 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
- 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. Runtools/install-git-hooks.sh. - Read §2 in order. Confirm SPEC.md on
mainis v0.8 (header) with §9.11 and §14.1.1; if not, stop and report. - Copy the keyword draft into
contracts/category-keywords.csv; write checker part (a) and run it. - Extract the §14.1.1 block into
contracts/schema/migrations/0002_categories_v2.sql; write checker parts (b) and (c); run. - 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.
- Update
DefaultCategoryKeyincontracts/openapi.yamland the E-1 example; scan the other examples (§2 item 7) and change only those that enumerate the defaults. - 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
slateandamber(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 onmain. - Gate: the checker's summary lines from this session, with the new checks named.
suggest_categorycase 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.mdordocs/decisions.mdby this agent) orTODO(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.