diff --git a/PLAN-M12b.md b/PLAN-M12b.md index 034b249..1c4ad68 100644 --- a/PLAN-M12b.md +++ b/PLAN-M12b.md @@ -15,7 +15,7 @@ Bring the landed server and importer to the category contract v2 (D-053, D-054): ## 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. +- 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 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 (`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. diff --git a/PLAN-M12c.md b/PLAN-M12c.md index 79cc134..f239ff5 100644 --- a/PLAN-M12c.md +++ b/PLAN-M12c.md @@ -19,7 +19,7 @@ Bring the landed Android contract and data layer to the category contract v2 (D- - 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 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`). diff --git a/PLAN-M12d.md b/PLAN-M12d.md new file mode 100644 index 0000000..f2d413b --- /dev/null +++ b/PLAN-M12d.md @@ -0,0 +1,65 @@ +# PLAN-M12d: UI contract additions (UX-9) + +## 1. Goal +Add the two UI-contract members that U2 found the design needs (UX-9, D-059) to `:core:model`, implement them in `:data` from Room and in `:core:fakes` from each scenario's own data: `ExpenseEditorOptions.frequentCategoryIds` (S-7's and S-19's `CategoryQuickRow`) and `CategoryRepository.deletable(id)` (S-15's category editor). No server, sync or Room schema change. Runs in parallel with M6; M7a to M7f start after both M6 and this plan have landed, and M7c and M7f consume the two members. + +## 2. Read first +1. SPEC §14.2.4: `ExpenseEditorOptions`, `ExpenseRepository`, `CategoryRepository`, and "Changes in v0.9" (the readings of both members, the UI text choice, the fallbacks), all of it. +2. SPEC AC-7.2 (`sort_order`), AC-7.5, AC-7.6 (deletability), AC-8.6, §9.11 CS-1 (the suggestion is not this plan's), §6.2 (layering, fakes), §14.2.1 (mirrored tables), §14.8 (the change rule). +3. SPEC §11.2 (row M12d), §11.3 (note M12d), §14.7 (row D-059). +4. `docs/decisions.md` D-018 (empty scenario), D-054, D-059. +5. `docs/ux/proposed-decisions.md` UX-9 and `docs/ux/design-spec.md` §3.7 (`CategoryQuickRow`), §4 S-15, §6 open points 1 and 2 (for context; the spec text wins). +6. Files: `android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Expenses.kt` and `Settings.kt`, `android/core/model/src/test/.../ContractTest.kt`; `android/data/src/main/kotlin/se/hagstedt/kvitt/data/repo/ExpenseRepositoryImpl.kt`, `SettingsRepositories.kt`, the DAOs and the `:data` test harness; `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/FakeExpenseRepository.kt`, `FakeSettingsRepositories.kt`, `FakeRepositories.kt`, `FakeScenarios.kt`; `android/README.md`. +7. `CLAUDE.md` (Android gate, layering, no mocking libraries, no silencing). + +## 3. Already established +- `ExpenseEditorOptions` gains `val frequentCategoryIds: Map>` as its last parameter; `CategoryRepository` gains `fun deletable(id: CategoryId): Flow` (§14.2.4, D-059). Nothing else in `:core:model` changes. +- `frequentCategoryIds`: one entry per active account; the active (non-archived, non-deleted) categories ordered by the number of non-deleted expenses in that account that use them, descending; ties by `sort_order` ascending; at most 5; categories with no use in the account are left out (the list may be shorter or empty). Counted over every expense in Room (both users, pending, imported and recurring included); not a per-phone history. The `editorOptions()` flow re-emits when expenses or categories change (§14.2.4). +- `deletable(id)`: false for a default category (`defaultKey != null`) or a category used by a non-deleted expense or a non-deleted recurring template; true otherwise. Computed from Room; the server stays the authority (`delete` can still return `Failed("category_in_use")` or `Failed("category_is_default")`) (AC-7.6, §14.2.4). An unknown or deleted id: emit false. +- The UI rules (filling the row to five, the fifth place for the selected category, the three delete-blocked texts) are M7c's and M7f's, not this plan's. +- Fakes: every scenario computes both members from its own data, with the same rules (the empty scenario of D-018 has no expenses, so every list is empty and every default category is not deletable). +- No Room migration (Room schema stays 1); no new DAO table; queries over existing tables only. + +## 4. Owned paths +From SPEC §14.7 (row D-059, a contract-change plan): `android/core/model/**` (`ExpenseEditorOptions`, `CategoryRepository`, `ContractTest`), `android/data/**` (both members and their tests), `android/core/fakes/**` (both members, outside the `fakes//` folders that M7x own). + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. M6 runs at the same time in `android/core/design/**`; do not touch it. + +## 5. Deliverables +- `:core:model`: the two members; `ContractTest` updated. +- `:data`: `frequentCategoryIds` in the expense repository's `editorOptions()`; `deletable` in the category repository; repository tests over Room: ordering by count, tie by `sort_order`, cap at 5, unused categories left out, archived and deleted categories left out, deleted expenses not counted, pending and both users' expenses counted, one entry per active account and none for an archived one, re-emission after a new expense; `deletable` false for a default, for one used by an expense, for one used only by a recurring template, true for an unused custom one and after the last using expense is deleted, false for an unknown id. +- `:core:fakes`: both members in every fake repository, computed from the scenario data, with a test over the empty scenario and one scenario with expenses. +- Every existing caller or test that constructs `ExpenseEditorOptions` updated, each listed in the report. + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m12d -b agent-m12d main`, then in `.claude/worktrees/agent-m12d`: `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 SPEC v0.9 is on `main` (§14.2.4 "Changes in v0.9"); 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. `:core:fakes`: both members and their test. +6. `:data`: queries, both members, repository tests. +7. Run the gate (§7). Commit on `agent-m12d`, `git rebase main` (M6 may have landed meanwhile; paths are disjoint), 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` (Android agents share one Gradle build at a time; `gradle-serial.sh` queues behind M6). +Green means: `BUILD SUCCESSFUL`; test counts from fresh XML show 0 failures and include the new repository and fake tests. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "none". + +## 9. Out of scope +- Any UI: `CategoryQuickRow` (M6), the S-7 row filling (M7c), the S-15 delete texts (M7f). +- The category suggestion (§9.11, landed with M12c); the server; `contracts/**`; a Room migration. +- Fake scenarios for feature states (M7x, under `fakes//`). + +## 10. Report +- Branch `agent-m12d`, head commit hash after rebasing on `main`. +- Gate: BUILD line, actionable tasks, test counts from fresh XML. +- Dependency-report lines. +- Callers and tests changed for the new `ExpenseEditorOptions` parameter, 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 7702773..49b1b07 100644 --- a/PLAN-M6.md +++ b/PLAN-M6.md @@ -27,6 +27,8 @@ Implement `:core:design` from the approved `docs/ux/design-spec.md`: theme, colo - M2b created the module shell (build file, manifest, empty string files); M6 owns it and fills it in (D-004). - Screenshot goldens are committed; open every changed PNG before committing (CLAUDE.md). - `:core:design` depends only on `:core:model` (§6.2). +- Design-spec open points (`docs/ux/design-spec.md` §6, SPEC v0.9): (1) **System font on One UI**: `FontFamily.Default` may resolve to a One UI font instead of Roboto; check on the owner's S26 that amounts stay tabular (tabular figures) and weights 500, 600 and 700 are distinct, and report it (D-020 stands either way). (2) **Material 3 Expressive API status**: check every Expressive API of design-spec §3.1 against the Material 3 release pinned in `android/gradle/libs.versions.toml`; any that is not usable (not in the pinned release, or experimental without a stable path) takes its listed stable fallback without a design change; list each with its outcome in the report. (3) S-19 has no category suggestion (CS-1); `CategoryQuickRow` must work without one. +- The §9.7 hex values are in SPEC v0.9 (from design-spec §2.3; D-051 fulfilled); `design-spec.md` §2 and §3 and `strings.csv` are the sources (§13.4). M6 started 2026-10-18. `CategoryQuickRow` takes its list from the caller: `ExpenseEditorOptions.frequentCategoryIds` (D-059) is added by M12d in parallel; the component must not depend on it existing. - Avatars (v0.5, §14.2.4 readings): `:data` always loads pictures at size 512 (E-11); the 128 variant is unused. If the avatar component needs the small size, ask the orchestrator for a size parameter (a contract request on `:core:model` and `:data`). ## 4. Owned paths diff --git a/PLAN-M7c.md b/PLAN-M7c.md index 9be7e23..c6b2d76 100644 --- a/PLAN-M7c.md +++ b/PLAN-M7c.md @@ -31,6 +31,8 @@ Build `:feature:expense`: the full-screen expense editor (S-7) and the expense d - 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). +- Category quick row (D-059, SPEC §14.2.4 "Changes in v0.9"): `ExpenseEditorOptions.frequentCategoryIds[accountId]`, read once on open and on an account change, filled up to five from the remaining active categories in `sort_order`; the selected category (suggested, defaulted or picked in "Alla") takes the fifth place when it is not among them. M12d must have landed (§11.2). When the suggestion returns null (`other` archived and no active last-used category), the editor shows no category until the user picks one, and saving is blocked as for a missing category (AC-8.6). +- Design-spec open points (`docs/ux/design-spec.md` §6) carried here: tabular figures for amounts as M6 delivers them (M6 checks them on the S26); Material 3 Expressive components only as M6 provides them, with M6's stable fallbacks; the recurring editor S-19 (M7f) has no category suggestion, so nothing in this feature is shared with it beyond the `:core:design` editor fields. - Detail shows amount, description, category, account, date, payer, shares and mode, notes, "Recurring: