diff --git a/CLAUDE.md b/CLAUDE.md index ecbcd45..200a461 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,7 @@ The owner talks to one **orchestrator** session. The orchestrator plans, briefs, * **Agents' reports are not shown to the owner.** The orchestrator extracts the decision: one line per finding, fixed or not, and relays every owner step in chat. * **Subagents are Opus.** Haiku or Sonnet only for a read-only scan the owner explicitly asks for that way. * **Numbering** (decisions, spec versions, schema versions, contract requests) is handed out by the orchestrator in the brief. Two parallel agents get disjoint ranges; the second to land renumbers to be contiguous on rebase. **Implementation agents never edit `SPEC.md` or `docs/decisions.md`:** they put proposed decision entries and spec text, numbered from their range, in their report, and the orchestrator has them recorded by a docs agent, landed together with the implementation. +* **The UX sprint** (U1, U2) runs in the owner's own Claude session in `~/orca/workspaces/kvitt/UX-sprint` on branch `UX-sprint`, with no orchestrator. Its results reach the main line only through `docs/ux/proposed-decisions.md` (owner answers, proposed decisions and spec text, no numbers): the orchestrator numbers them and has a docs agent record them in `SPEC.md` and `docs/decisions.md` (SPEC v0.8 recorded UX-1 to UX-8 as D-047 to D-054). * **Parallel agents** each have their own worktree, build through `tools/gradle-serial.sh` (one Gradle build at a time on this machine), and take turns on the phone: **one agent on the phone at a time**, named in the brief. --- diff --git a/PLAN-M12a.md b/PLAN-M12a.md new file mode 100644 index 0000000..b419a66 --- /dev/null +++ b/PLAN-M12a.md @@ -0,0 +1,72 @@ +# 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 `6b60ef7`. 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. diff --git a/PLAN-M12b.md b/PLAN-M12b.md new file mode 100644 index 0000000..034b249 --- /dev/null +++ b/PLAN-M12b.md @@ -0,0 +1,72 @@ +# 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 6b60ef7` (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 (`6b60ef7`) 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 SQLite 3.50 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 (`6b60ef7`) 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_version`s 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. diff --git a/PLAN-M12c.md b/PLAN-M12c.md new file mode 100644 index 0000000..79cc134 --- /dev/null +++ b/PLAN-M12c.md @@ -0,0 +1,73 @@ +# 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 the Java resource `android/data/src/main/resources/category-keywords.csv`, byte-identical to `contracts/category-keywords.csv`, checked by a test; 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). diff --git a/PLAN-M6.md b/PLAN-M6.md index 897f66b..7702773 100644 --- a/PLAN-M6.md +++ b/PLAN-M6.md @@ -20,7 +20,7 @@ Implement `:core:design` from the approved `docs/ux/design-spec.md`: theme, colo - Typography uses the Material 3 default typeface; no font file is bundled and no licence step is needed (D-020). - Shared strings in `:core:design` include the notification copy of §13.3 and the channel name "Activity"/"Aktivitet" of §14.6, which `:app` uses; their keys never start with a feature name (D-017). - Money: sv `1 234,50 kr` (U+00A0 thousands and before `kr`, comma decimal, two decimals); other locales `SEK 1,234.50` (U+00A0 after `SEK`); never a minus sign; dates in lists `3 okt.` / `3 Oct`, with year when not the current year; detail `lördag 3 oktober 2026` / `Saturday, 3 October 2026`; times `14:02`; relative "last synced" per §9.8 (§9.8). Formatting lives in `:core:design`, never in `:domain` (§9.8). -- Colour token names are the 12 of §9.7 (`ColorToken`); icon tokens the 30 of §9.7 (`CategoryIcons.ALL`) (§9.7, §14.2.4). +- Colour token names are the 14 of §9.7 (`ColorToken`, `slate` and `amber` added by D-054, hex values from U2 per D-051); icon tokens the 33 of §9.7 (`CategoryIcons.ALL`); fourteen default categories (AC-7.2), whose localised default names `:core:design` holds as shared strings (§9.7, §14.2.4, D-054). Direction "Kalas", muted (D-047). M12c must have landed (§11.2). - Initials: first letter of the first two words, uppercase, on a colour derived from the user id (AC-4.5). - Accessibility: 48 dp targets, 4.5:1 contrast, TalkBack labels, reflow at 200 % (§13.1). - No hard-coded colours, sizes or strings in features: tokens come from one design file; shared strings live in `:core:design` with keys from `docs/ux/strings.csv`; lint `MissingTranslation` is an error in this module (CLAUDE.md, §14.7 Strings, D-017). diff --git a/PLAN-M7c.md b/PLAN-M7c.md index 497e33a..9be7e23 100644 --- a/PLAN-M7c.md +++ b/PLAN-M7c.md @@ -6,7 +6,7 @@ Build `:feature:expense`: the full-screen expense editor (S-7) and the expense d ## 2. Read first 1. `docs/ux/design-spec.md`: tokens, component inventory, and the sections for S-7, S-8. 2. `docs/ux/strings.csv` (keys for these screens) and `docs/ux/screens/S-7-*`, `docs/ux/screens/S-8-*`. -3. SPEC §13.2 Navigation model and §13.3 Screens (rows S-7, S-8 and the states paragraph). +3. SPEC §13.2 Navigation model and §13.3 Screens (rows S-7, S-8 and the states paragraph); §9.11 Category suggestion (D-049). M12c must have landed (§11.2). After saving, the snackbar "Utgiften sparades" has no undo (S-7, D-052). 4. SPEC §14.2.4 UI contract: `ExpenseRepository`, `ExpenseDraft`, `ExpenseEditorOptions`, `ExpenseDetailState`, `TimelineLine`, `FieldChange`, `Share`, `SaveResult`, `FieldError`, `SplitMode`, `Account`, `Category`, `User`. 5. SPEC §4 FR-8 Expenses (AC-8.1 to AC-8.11); FR-12 AC-12.4, AC-12.5; §9.1 Splits; §9.6 (limits, amount input parsing); §9.10 (exact-mode edit); §14.5.3 error tokens. 6. SPEC §6.2 (module table), §9.8 Formatting, §9.9 Online-only actions, §11.2 (row M7c), §11.3 (note M7x), §14.7. @@ -28,7 +28,7 @@ Build `:feature:expense`: the full-screen expense editor (S-7) and the expense d - No placeholder text in previews or screenshots; goldens are committed; open every changed PNG before committing (CLAUDE.md). - The add flow needs at most: open app, tap add, type amount, type description, tap save; account, category, payer and split are defaulted (§13.1, AC-8.6). - Editor is a full screen, not a bottom sheet (§13.2). Amount input accepts digits, one decimal separator (`,` or `.`), at most two decimals, no sign, converted to öre by string parsing, never floating point (§9.6). -- Defaults: account = last used on this phone if active else oldest active; category = last used in that account if active else `other`; split mode `default`; payer = current user; date = today in the phone's zone (AC-8.3, AC-8.4, AC-8.6). Defaults come from `ExpenseEditorOptions`. +- Defaults: account = last used on this phone if active else oldest active; category for a new expense = the suggestion of §9.11 (D-049): the editor keeps a "picked by hand" flag (CS-2) and, while it is false, calls `ExpenseRepository.suggestCategory(accountId, description)` on every description and account change (CS-3), showing the result with no marker (CS-7); before any description it is `ExpenseEditorOptions.defaultCategoryFor` (last used in that account if active else `other`); editing never changes the category (CS-1); split mode `default`; payer = current user; date = today in the phone's zone (AC-8.3, AC-8.4, AC-8.6). Defaults come from `ExpenseEditorOptions`. - Split modes `default` ("Account default 60/40"), `ratio`, `exact` (type one share, the other is amount minus it, read-only; a share larger than the amount blocks saving inline), `full` (one bearer); changing account in `default` re-copies the ratio; live preview from `ExpenseRepository.preview` (AC-8.5, AC-8.7, AC-8.8, §13.3 S-7). - Validation errors come back as `SaveResult.Invalid` with §14.5.3 tokens, `required`, or `validation_failed` for a §9.6 limit without a domain token (lengths, dates) (§9.6, §14.2.4); deleting asks for confirmation (AC-8.9). - Detail shows amount, description, category, account, date, payer, shares and mode, notes, "Recurring: