Files
kvitt/PLAN-M12b.md

8.8 KiB

PLAN-M12b: Category contract v2, server

1. Goal

Bring the landed server and importer to the category contract v2 (D-053, D-054): migration 0002 adds the four new default categories to every database and moves the server to schema version 2; the token lists, the default-key enum and the importer's category table follow the spec. Existing databases, the development stack included, upgrade in place without losing a row.

2. Read first

  1. SPEC §14.1 and §14.1.1 Migration 0002 (all of it), §14.8 (contract files, migration texts).
  2. SPEC AC-7.2 (fourteen defaults), §9.7 (33 icons, 14 colour tokens), §14.3.3 Category, §14.3.4 E-1.
  3. SPEC §10.8 Splitwise import (the schema check and the new category table), D-044, D-046, D-053.
  4. SPEC §8.9 (pragmas), §14.4.2 (pull: the new rows reach phones by server_version).
  5. SPEC §11.2 (row M12b), §11.3 (note M12), §14.7 (row M12b).
  6. docs/decisions.md D-053, D-054.
  7. Files: git show aa765bb (M12a's commit message lists what it held back), contracts/schema/migrations/0002_categories_v2.sql and tools/contracts-check.py (from M12a), server/crates/kvitt-server/migrations/0001_init.sql, src/db/mod.rs, src/api/validate.rs, src/wire/category.rs, src/wire/mod.rs, src/openapi.rs, tests/skeleton/migration.rs, tests/skeleton/routes.rs (E-1 schema_version), tests/openapi_contract.rs, the tests in tests/core_api/ and tests/sync_api/ that count or list categories, server/crates/kvitt-import/src/categories.rs, data/splitwise_categories.csv, tests/import.rs (category_table_matches_the_spec), contracts/examples/E-31-sync-pull/response.json.
  8. CLAUDE.md (server gate, no unwrap outside tests, every schema change gets a migration and a migration test).

3. Already established

  • Migration 0002 is the SQL of §14.1.1 byte for byte; server-v1.sql and 0001_init.sql stay unchanged (§14.1.1).
  • It rebuilds categories under PRAGMA defer_foreign_keys = ON inside sqlx's per-migration transaction (foreign keys stay ON), inserts four rows with server_version from sync_sequence, sets meta.schema_version to 2 (§14.1.1). M12a's checker (aa765bb) loads it on a fresh and on an in-use v1 database with foreign keys on, the way sqlx does; the docs agent's own run of an equivalent text with the SQLite the server bundles (3.51.x, 3.51.3 through libsqlite3-sys 0.37.0) also committed with foreign_key_check empty.
  • SCHEMA_VERSION = 2; E-1 reports it; the importer's check reads that constant, so it follows (§10.8, §14.1.1; D-046 revised by D-054).
  • Icons: the 30 of v1 plus key, shield, account_balance; colours: the 12 of v1 plus slate, amber (§9.7). Hex values are irrelevant to the server.
  • DefaultCategoryKey has the fourteen keys in both contracts/openapi.yaml and the generated document. M12a (aa765bb) held back the contract side, so this plan edits contracts/openapi.yaml (that enum only) and adds the category examples M12a held back (see its commit message), then the OpenAPI match test compares the two.
  • Importer table per §10.8 (D-053): Rent/Mortgage/Hyra/Avbetalning/Amortering housing; Insurance/Försäkringar insurance; Childcare/Barnomsorg children; Taxes/Skatter fees; Swedish El, Värme/gas, Vatten, Sophämtning (utilities) and Husdjur (home) added. category_table_matches_the_spec parses SPEC §10.8, so it fails on main from SPEC v0.8 until this plan lands (the orchestrator lands them close together).
  • The E-31 example lists two categories, not the defaults; change it only if it enumerates the defaults.

4. Owned paths

From SPEC §14.7 (row M12b, a contract-change plan): contracts/openapi.yaml (DefaultCategoryKey only) and the category examples under contracts/examples/ that M12a held back (with python3 tools/contracts-check.py still passing); server/crates/kvitt-server/migrations/0002_categories_v2.sql, src/db/mod.rs (SCHEMA_VERSION), src/api/validate.rs (token lists), src/wire/category.rs and src/wire/mod.rs (key enum), and the tests of these (tests/skeleton/**, and the category-counting lines of tests/core_api/** and tests/sync_api/**); server/crates/kvitt-import/data/splitwise_categories.csv, src/categories.rs and its tests.

Edit nothing outside these paths; a needed change elsewhere (including contracts/**) is a contract request to the orchestrator.

5. Deliverables

  • server/crates/kvitt-server/migrations/0002_categories_v2.sql: byte-identical to contracts/schema/migrations/0002_categories_v2.sql.
  • An identity test asserting that equality (next to the existing 0001 identity test).
  • SCHEMA_VERSION = 2 and the E-1 tests updated.
  • A seed test: on a fresh database, the fourteen default rows exactly per AC-7.2 and §14.1.1 (ids, keys, icons, colours, sort orders, null names, server_version 11 to 14 after the ten seeds); on a v1 database (built from 0001_init.sql alone, then data inserted: a custom category, a renamed and an archived default, expenses and a recurring template referencing categories) upgraded through the migrator: every earlier row unchanged, the four new rows with server_versions above the old sequence value, PRAGMA foreign_key_check empty, meta.schema_version 2; a pull from cursor = the old sequence value returns exactly the four new categories.
  • validate.rs: icon list of 33 and colour list of 14 (§9.7), with tests for the new tokens.
  • contracts/openapi.yaml DefaultCategoryKey with the fourteen keys and the held-back category examples; the default-key enum in src/wire/ with the fourteen keys; the OpenAPI match test and python3 tools/contracts-check.py green.
  • splitwise_categories.csv per §10.8; src/categories.rs key list of fourteen; category_table_matches_the_spec green; an importer test that a Försäkringar / Insurance row maps to insurance.
  • Existing tests that assumed ten defaults 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-m12b -b agent-m12b main, then in .claude/worktrees/agent-m12b: git reset --hard main. No git-ignored files are needed. Run tools/install-git-hooks.sh.
  2. Read §2 in order. Confirm M12a has landed (contracts/schema/migrations/0002_categories_v2.sql exists and python3 tools/contracts-check.py passes on main); if not, stop and report.
  3. Copy the migration file; add the identity test; bump SCHEMA_VERSION; run the server gate and list every failing test that counts categories or checks the schema version.
  4. Write the seed and upgrade tests (§5), then fix the lists, the enum and the failing tests until green.
  5. Update the importer table and key list; run category_table_matches_the_spec; add the insurance mapping test.
  6. Run the gate (§7). Commit on agent-m12b, git rebase main, rerun the gate. Never merge, push or remove the worktree.
  7. Do not migrate the running development stack's database yourself: report the command for the orchestrator (kvitt-server migrate runs at start, so a restart with the new image upgrades it; take a backup first with the dev backup script).

7. Gate

From SPEC §11.2, verbatim: "Server gate; python3 tools/contracts-check.py passes". Command, from server/: cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test; and python3 tools/contracts-check.py from the repository root (this plan edits contracts/). Green means: all three exit 0 in this session; the test result: lines show 0 failed and include the identity, seed, upgrade, OpenAPI match and importer parity tests.

8. HUMAN steps

From SPEC §11.2, verbatim: "none".

9. Out of scope

  • contracts/** beyond the OpenAPI enum and the held-back examples, Android (M12c), server-v1.sql and 0001_init.sql (byte-locked).
  • Any change to sync, push or the audit writer; the four new rows get no audit entries and no pushes (they are seed data like the v1 seeds).
  • Running the importer against the real exports, or migrating the development or production database.

10. Report

  • Branch agent-m12b, head commit hash after rebasing on main.
  • Gate output: fmt and clippy exit status, every test result: line.
  • Tests changed because they assumed ten defaults or schema 1, each with its reason.
  • SQLite version of the bundled libsqlite3-sys and confirmation that the migration ran under sqlx's transaction with foreign keys on.
  • The upgrade steps for the development stack, worded for the orchestrator.
  • 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. This plan implements server schema version 2 (D-054); any further schema change would be 3.