From 5e1be383648231bbada3a98417ef234496f3afc0 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Sat, 17 Oct 2026 21:05:34 +0200 Subject: [PATCH] Plans and start prompts for every milestone; SPEC v0.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLAN-.md for U1, U2, M1, M2a, M2b, M2c-dev, M2c-pi, M3, M4, M5, M6, M7a-M7f, M8, M9, M10, M11, each with the eleven fixed sections, and a start prompt per plan in prompts/ plus prompts/README.md (dependency order and parallel waves). SPEC v0.2: owner answers D-001 (development-first, M2c split into M2c-dev and M2c-pi, Gitea registry), D-002 (local backups deleted once on Drive), D-003 (Splitwise CSV format, English and Swedish variants, category mapping), OQ-1, OQ-2, OQ-4 answered; M1's readings of ambiguous contract points stated explicitly in §14.3 to §14.5. HANDOFF §2 lists the plans. Co-Authored-By: Claude Opus 5.5 --- PLAN-M1.md | 73 ++++++++++++++++++++++++++++++ PLAN-M10.md | 81 +++++++++++++++++++++++++++++++++ PLAN-M11.md | 72 ++++++++++++++++++++++++++++++ PLAN-M2a.md | 104 ++++++++++++++++++++++++++++++++++++++++++ PLAN-M2b.md | 97 ++++++++++++++++++++++++++++++++++++++++ PLAN-M2c-dev.md | 82 ++++++++++++++++++++++++++++++++++ PLAN-M2c-pi.md | 102 ++++++++++++++++++++++++++++++++++++++++++ PLAN-M3.md | 93 ++++++++++++++++++++++++++++++++++++++ PLAN-M4.md | 82 ++++++++++++++++++++++++++++++++++ PLAN-M5.md | 87 ++++++++++++++++++++++++++++++++++++ PLAN-M6.md | 83 ++++++++++++++++++++++++++++++++++ PLAN-M7a.md | 84 ++++++++++++++++++++++++++++++++++ PLAN-M7b.md | 88 ++++++++++++++++++++++++++++++++++++ PLAN-M7c.md | 82 ++++++++++++++++++++++++++++++++++ PLAN-M7d.md | 82 ++++++++++++++++++++++++++++++++++ PLAN-M7e.md | 81 +++++++++++++++++++++++++++++++++ PLAN-M7f.md | 88 ++++++++++++++++++++++++++++++++++++ PLAN-M8.md | 87 ++++++++++++++++++++++++++++++++++++ PLAN-M9.md | 81 +++++++++++++++++++++++++++++++++ PLAN-U1.md | 82 ++++++++++++++++++++++++++++++++++ PLAN-U2.md | 79 ++++++++++++++++++++++++++++++++ SPEC.md | 109 ++++++++++++++++++++++++--------------------- docs/HANDOFF.md | 23 ++++++++-- docs/decisions.md | 3 ++ prompts/M1.md | 24 ++++++++++ prompts/M10.md | 24 ++++++++++ prompts/M11.md | 24 ++++++++++ prompts/M2a.md | 24 ++++++++++ prompts/M2b.md | 24 ++++++++++ prompts/M2c-dev.md | 24 ++++++++++ prompts/M2c-pi.md | 24 ++++++++++ prompts/M3.md | 24 ++++++++++ prompts/M4.md | 24 ++++++++++ prompts/M5.md | 24 ++++++++++ prompts/M6.md | 24 ++++++++++ prompts/M7a.md | 23 ++++++++++ prompts/M7b.md | 23 ++++++++++ prompts/M7c.md | 23 ++++++++++ prompts/M7d.md | 23 ++++++++++ prompts/M7e.md | 23 ++++++++++ prompts/M7f.md | 23 ++++++++++ prompts/M8.md | 24 ++++++++++ prompts/M9.md | 24 ++++++++++ prompts/README.md | 41 +++++++++++++++++ prompts/U1.md | 17 +++++++ prompts/U2.md | 19 ++++++++ 46 files changed, 2398 insertions(+), 54 deletions(-) create mode 100644 PLAN-M1.md create mode 100644 PLAN-M10.md create mode 100644 PLAN-M11.md create mode 100644 PLAN-M2a.md create mode 100644 PLAN-M2b.md create mode 100644 PLAN-M2c-dev.md create mode 100644 PLAN-M2c-pi.md create mode 100644 PLAN-M3.md create mode 100644 PLAN-M4.md create mode 100644 PLAN-M5.md create mode 100644 PLAN-M6.md create mode 100644 PLAN-M7a.md create mode 100644 PLAN-M7b.md create mode 100644 PLAN-M7c.md create mode 100644 PLAN-M7d.md create mode 100644 PLAN-M7e.md create mode 100644 PLAN-M7f.md create mode 100644 PLAN-M8.md create mode 100644 PLAN-M9.md create mode 100644 PLAN-U1.md create mode 100644 PLAN-U2.md create mode 100644 prompts/M1.md create mode 100644 prompts/M10.md create mode 100644 prompts/M11.md create mode 100644 prompts/M2a.md create mode 100644 prompts/M2b.md create mode 100644 prompts/M2c-dev.md create mode 100644 prompts/M2c-pi.md create mode 100644 prompts/M3.md create mode 100644 prompts/M4.md create mode 100644 prompts/M5.md create mode 100644 prompts/M6.md create mode 100644 prompts/M7a.md create mode 100644 prompts/M7b.md create mode 100644 prompts/M7c.md create mode 100644 prompts/M7d.md create mode 100644 prompts/M7e.md create mode 100644 prompts/M7f.md create mode 100644 prompts/M8.md create mode 100644 prompts/M9.md create mode 100644 prompts/README.md create mode 100644 prompts/U1.md create mode 100644 prompts/U2.md diff --git a/PLAN-M1.md b/PLAN-M1.md new file mode 100644 index 0000000..abe426d --- /dev/null +++ b/PLAN-M1.md @@ -0,0 +1,73 @@ +# PLAN-M1: Contracts and vectors + +On record only: M1 was implemented in parallel by an agent working directly from SPEC §14 and §9 and landed on `main` as `c6df2ab`; its readings of ambiguous points are stated in SPEC v0.2. This plan describes the same scope; if the two disagree, SPEC wins. + +## 1. Goal +Write the cross-team contract files from SPEC §14: the OpenAPI document, the SQLite schema v1, the shared test vectors, the JSON examples, and a stdlib-only Python checker. After M1 lands, server and Android agents work in parallel against these files without talking to each other. + +## 2. Read first +1. SPEC §14 Contracts: §14.1 SQLite schema v1, §14.3 OpenAPI 3.1 outline (14.3.1 to 14.3.5), §14.4 Sync protocol v1 (examples), §14.5 Domain rules as pure functions (14.5.1 to 14.5.5), §14.6 Push payload v1, §14.7 Module layout and ownership map, §14.8 Contract files and the change rule. +2. SPEC §9.1 Splits, §9.3 Balances, §9.4 Settlement allocation proposal, §9.6 Limits and validation. +3. SPEC §7 API outline (endpoint ids E-1 to E-33). +4. SPEC §11.2 (row M1) and §11.3 (note M1). +5. `CLAUDE.md`. + +## 3. Already established +- The OpenAPI file is written by hand from §14.3, OpenAPI 3.1.0, `info.title` `Kvitt`, `info.version` `1`, one server `https://icehack.asuscomm.com:8443`, security scheme `device` = HTTP bearer; the outline is binding and the YAML adds or drops nothing (§14.3, §11.3). +- `contracts/schema/server-v1.sql` is the §14.1 DDL byte for byte; M2a's `0001_init.sql` must equal it (§14.1). +- Vector files, one per function: `compute_split.json`, `compute_balances.json`, `propose_allocation.json`, `validate_settlement.json`, `over_settled_accounts.json`, `spend_series.json`, `resolve_range.json`; format of §14.5.5; minimum cases listed in §14.5.5. +- Examples: one per request and response body of §14.3.4 in `contracts/examples/-/{request,response}.json`, one per error code in `contracts/examples/errors/.json`, the sync examples of §14.4 in `contracts/examples/sync/`, push examples in `contracts/examples/push/` (§14.3.5, §14.7). +- Error codes are exactly the 25 of §14.3.2; error tokens exactly those of §14.5.3. +- Ratio rule rounds the non-payer down; payer absorbs the remainder (§9.1). Proposal algorithm §9.4 is normative. +- A contract change after landing needs a spec bump and a decisions entry (§14.8). + +## 4. Owned paths +From SPEC §14.7, verbatim: `contracts/**`, `tools/contracts-check.py` (owner M1; after the owner lands: contract-change plans only (§14.8)). + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +## 5. Deliverables +- `contracts/openapi.yaml`: OpenAPI 3.1.0 document of §14.3 (every endpoint E-1 to E-33, wire objects, error responses). +- `contracts/schema/server-v1.sql`: the §14.1 DDL, byte for byte. +- `contracts/test-vectors/compute_split.json`, `compute_balances.json`, `propose_allocation.json`, `validate_settlement.json`, `over_settled_accounts.json`, `spend_series.json`, `resolve_range.json`: vectors per §14.5.5 with at least its minimum cases. +- `contracts/examples/-/{request,response}.json`: one per request and response body of §14.3.4. +- `contracts/examples/errors/.json`: one per error code of §14.3.2. +- `contracts/examples/sync/*.json`: pull page, push request, push response with accepted and rejected results (§14.4). +- `contracts/examples/push/*.json`: one FCM message per push type of §14.6. +- `tools/contracts-check.py`: stdlib-only checker (JSON parses, vector format, Python reference of §9.1, §9.3, §9.4, example key sets against §14.3 shapes). + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m1 -b agent-m1 main`, then in `.claude/worktrees/agent-m1`: `git reset --hard main`. No git-ignored files are needed; copy none. Run `tools/install-git-hooks.sh`. +2. Read §2 in order. +3. Copy the §14.1 DDL into `contracts/schema/server-v1.sql` exactly (no trailing changes); verify with a byte comparison against the block extracted from SPEC.md. +4. Write `tools/contracts-check.py` first (Python 3 stdlib only): JSON parse of every file under `contracts/`; vector envelope (`format` 1, `function`, `cases` with `name`, `input`, `expected` holding `ok` or `error`); reference implementations of `compute_split`, `compute_balances`, `propose_allocation`, `validate_settlement`, `over_settled_accounts` per §9.1, §9.3, §9.4 and §14.5.2 recomputing every expected value; key-set validation of each example against the wire shapes of §14.3.3 and the endpoint bodies of §14.3.4. +5. Write the vector files with the §14.5.5 minimum cases (and the §9.1 example table rows); run the checker after each file. For `spend_series` and `resolve_range` (Kotlin only) the checker validates format and that dates are well formed; state in the report whether it also recomputes them. +6. Write the examples, using UUIDv7-shaped ids and the fixed timestamp form; run the checker. +7. Write `contracts/openapi.yaml` from §14.3; cross-check every endpoint, status code and error list against §14.3.4 and the always-possible codes of §14.3.2. Python stdlib has no YAML parser: state in the report how the YAML was checked. +8. Run the gate (§7). Commit on `agent-m1`, `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: all JSON parses, every vector file matches §14.5.5, a stdlib-only Python reference of §9.1, §9.3, §9.4 reproduces every expected value, every example validates against the shapes of §14.3 by key set". +Command: `python3 tools/contracts-check.py` from the repository root. Green means exit code 0 and its summary line reporting zero failures. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "none". + +## 9. Out of scope +- Any Rust or Kotlin code (M2a, M2b), including the utoipa annotations (M2a) and DTOs (M5). +- The server migration file `server/crates/kvitt-server/migrations/0001_init.sql` (M2a). +- The Room exported schema `android/data/schemas/**` (M5). +- Changing §14 of the spec; anything that would is a gap for the report. +- `docs/`, `SPEC.md`, `CLAUDE.md`, other `tools/` files. + +## 10. Report +- Branch `agent-m1`, head commit hash after rebasing on `main`. +- Gate: the checker's output summary lines (files, cases, examples, failures) from this session. +- Count of vector cases per function; list of example directories. +- How the YAML was checked; whether `spend_series`/`resolve_range` are recomputed. +- Versions chosen: Python version used (no third-party packages). +- Numbering used (decision ids, spec version) or `TODO(spec)` occurrences. +- Open questions and spec gaps (any place §14 is ambiguous or self-contradicting), 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. diff --git a/PLAN-M10.md b/PLAN-M10.md new file mode 100644 index 0000000..b8f6ea9 --- /dev/null +++ b/PLAN-M10.md @@ -0,0 +1,81 @@ +# PLAN-M10: First release + +## 1. Goal +Ship the first signed release to both phones from the production server on the Pi: release keystore, signed APK published, both phones paired, push working both ways, nightly backup on Drive, a restore drill, and the memory check. Record the evidence in `docs/validation/first-release.md` and `docs/validation/restore-drill.md`. + +## 2. Read first +1. SPEC §10.6 APK hosting and release, §10.9 Backups and restore, §10.10 Operations, §10.4 Server configuration and CLI (`bootstrap`, `rotate-epoch`), §10.7 (step 7, the FCM key on the Pi). +2. SPEC §4 FR-1, FR-2 (pairing both phones), FR-13 Push notifications, FR-16 In-app update, FR-18 Backups and restore. +3. SPEC §8.5 Transport (release network security config), §14.4.8 Epoch recovery. +4. SPEC §11.2 (row M10), §11.3 (note M10), §14.7. +5. `infra/README.md`, `infra/RESTORE.md`, `infra/publish-apk.sh`, `infra/deploy.sh` (from M2c-pi); `android/app/build.gradle.kts` (signing config, from M8). +6. `CLAUDE.md` (THE PHONE, THE OWNER: confirm before anything destructive; EXECUTION PROTOCOLS: Reports are deliverables). + +## 3. Already established +- `version_code` = `git rev-list --count HEAD` of the released commit; `version_name` = `YYYY.MM.DD-`; passed as `kvitt.versionCode`/`kvitt.versionName` by the release script (§10.6). +- Release keystore at `android/keystore/release.jks` (git-ignored), created once by the owner; passwords and alias in `android/local.properties` keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias`, `kvitt.release.keyPassword`; the owner keeps a copy in a password manager; losing it means reinstalling (§10.6). +- `infra/publish-apk.sh` builds `:app:assembleRelease` through `tools/gradle-serial.sh`, writes `apk/kvitt-.apk` and an atomic `apk/latest.json`, keeps the newest 5 (§10.6). +- The landing page links "Download Kvitt" to the latest APK; the first install asks to allow "Install unknown apps" (AC-2.5, AC-16.3). +- Pairing: `kvitt-server bootstrap` on the Pi prints the admin code (FR-1); the admin creates the partner invite in Settings and shares it (AC-2.1); after pairing the app syncs and asks for notifications (AC-2.9). +- Push both ways: a change by one user notifies the other user's devices (AC-13.1). +- Backup nightly 03:30 to Drive folder `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV`; local files deleted once confirmed on Drive (D-002, §10.9). +- Restore drill follows §10.9 steps 1 to 7 and is documented in `docs/validation/restore-drill.md`; both phones must sync correctly afterwards (AC-18.2). +- Memory: `docker stats --no-stream` with both phones syncing; server RSS under 100 MB (§10.10, §11.2). +- Production runs on the Pi; development data on the Mac is not migrated (D-001). + +## 4. Owned paths +From SPEC §14.7, verbatim: `infra/**` row, after-landing column "M10 updates runbooks" (that is `infra/README.md`, `infra/RESTORE.md`), and `docs/validation/` | "the milestone that names the file", here `docs/validation/first-release.md` and `docs/validation/restore-drill.md`. + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +## 5. Deliverables +- `docs/validation/first-release.md`: release version, publish result, pairing of both phones, push both ways, update check, backup on Drive, memory check result, outstanding items; a "Results (owner's phone)" section for anything still to come; aggregates only. +- `docs/validation/restore-drill.md`: each §10.9 restore step as run, timings, the epoch rotation, both phones' re-push and balance check, issues found. +- `infra/README.md`, `infra/RESTORE.md`: corrections found during the release and the drill. +- On the Pi (not in git): published `apk/kvitt-.apk` and `apk/latest.json`; a production household with both users. + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m10 -b agent-m10 main`, then in `.claude/worktrees/agent-m10`: `git reset --hard main`. Copy `android/local.properties`, `android/app/google-services.json` and `infra/secrets/` from the main checkout; the keystore stays in the main checkout's `android/keystore/` and is referenced by absolute path from `local.properties` (never copied into git). Run `tools/install-git-hooks.sh`. +2. Read §2 in order. Confirm M2c-pi, M3, M4 and M8 have landed and the Pi stack is healthy (`infra/deploy.sh` output, `/health`); if not, stop and report. +3. Keystore (H-1). After the owner created it, add the four `kvitt.release.*` keys to the main checkout's `android/local.properties` (never overwrite other lines). +4. Deploy the current `main` image to the Pi with `infra/deploy.sh `; publish the APK with `infra/publish-apk.sh`; check `GET /apk/latest` returns the new `version_code`. +5. Bootstrap production: `docker compose exec server kvitt-server bootstrap` on the Pi (over SSH); give the code to the owner through the orchestrator. +6. Owner's phone (H-2 to H-4): install from the landing page, pair, allow notifications. Confirm with `adb shell pm list packages | grep se.hagstedt.kvitt` that the release id is installed and does not clash with the debug build. +7. Partner (H-5): the owner creates the partner invite in Settings and shares it; the partner installs, pairs, completes the profile step. +8. Push both ways (H-6): one test expense from each phone (deleted afterwards); each phone confirms the notification. +9. Backup: trigger `docker compose exec backup /backup.sh`; confirm on Drive with `rclone lsf`; confirm the next nightly run the following day if the session allows, else leave it in "Results (owner's phone)". +10. Restore drill (confirm with the owner before step 1 of §10.9 because it stops production): follow `infra/RESTORE.md` exactly with the latest backup; both phones re-push after the new epoch; the agent compares balances from the server with what the phones hold (pulled over USB from the owner's phone; the partner phone by yes/no). Fix the runbook where it was wrong. +11. Memory check: `docker stats --no-stream` on the Pi while both phones sync; record the server's memory. +12. Write the two validation files. Commit on `agent-m10`, `git rebase main`. Never merge, push or remove the worktree. + +## 7. Gate +From SPEC §11.2, verbatim: "`docs/validation/first-release.md` and `docs/validation/restore-drill.md` complete; server RSS under 100 MB". +Green means: both files exist with every section filled (no `TODO` other than a "Results (owner's phone)" item named as outstanding); the `docker stats` line recorded in `first-release.md` shows the server below 100 MB with both phones syncing. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "Create the release keystore; install the APK on both phones from the landing page; allow "Install unknown apps"; partner pairs with the invite; confirm notifications arrive". +- H-1, step 3, on the owner's Mac in the main checkout: `keytool -genkeypair -v -keystore android/keystore/release.jks -alias ` with key algorithm, size and validity as the start prompt states (`TODO(spec)`: §10.6 does not fix them); choose the passwords, store them in the password manager, and tell the agent the alias and where the passwords are entered (the agent never echoes them). +- H-2, step 6, on the owner's phone: open the invite link or `https://icehack.asuscomm.com:8443/i/`, tap "Download Kvitt", open the download. +- H-3, step 6: when Android asks, allow "Install unknown apps" for the browser, install, open Kvitt, pair with the code, allow notifications. +- H-4, step 6 and later updates: allow "Install unknown apps" for Kvitt itself when the in-app update first asks (AC-16.3). +- H-5, step 7, on the partner's phone: the same as H-2 and H-3 with the partner invite link, then the profile step. +- H-6, step 8: each phone answers yes/no: a notification about the other's test expense arrived. + +## 9. Out of scope +- New features or fixes in app or server code (findings go in the report; fixes are new plans). +- The Splitwise import (M11). +- Gitea Actions changes and Pi provisioning beyond runbook corrections (M2c-pi). +- Deleting anything on Drive. + +## 10. Report +- Branch `agent-m10`, head commit hash after rebasing on `main`. +- Released `version_code` and `version_name`; image tag deployed. +- Gate: the two files' section list and the `docker stats` server line. +- Restore drill result in one line; runbook corrections. +- Versions: none expected beyond what landed; state any tool used (keytool, rclone) and its version. +- Numbering used or `TODO(spec)` occurrences. +- HUMAN steps done and outstanding, worded for relay. +- 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. diff --git a/PLAN-M11.md b/PLAN-M11.md new file mode 100644 index 0000000..83dd44c --- /dev/null +++ b/PLAN-M11.md @@ -0,0 +1,72 @@ +# PLAN-M11: Import history + +## 1. Goal +Run the Splitwise importer once against the production database with the owner's two real exports, so six years of history appear on both phones. Prove it by comparing the imported per-account balances with Splitwise's own total rows and record the comparison, aggregates only, in `docs/validation/splitwise-import.md`. + +## 2. Read first +1. SPEC §10.8 Splitwise import, §4 FR-17 Splitwise import. +2. SPEC §12 OQ-2 (answered) and `docs/decisions.md` D-003. +3. SPEC §10.9 Backups and restore (backup run), §10.4 (running commands on the Pi), §10.10 Operations. +4. SPEC §9.3 Balances. +5. SPEC §11.2 (row M11), §14.7. +6. `PLAN-M9.md` §3 (mapping notes: header lines and member names), `infra/README.md`, `infra/RESTORE.md`. +7. `CLAUDE.md` (ANTI-PATTERNS: never commit personal data or the Splitwise CSV; THE OWNER: confirm before anything destructive; EXECUTION PROTOCOLS: Reports are deliverables). + +## 3. Already established +- Files: `export/export_hemma.csv` (English variant, group "hemma") into account "Hemma"; `export/export_general.csv` (Swedish variant, general group, about 1750 rows) into account "Vardag"; both in the main checkout, git-ignored, all SEK (D-003). +- Command per file: `kvitt-import --db /data/kvitt.sqlite --file --account "" --user "=" --user "=" [--dry-run]`, run on the Pi with the server stopped, or on the Mac against a copy of the database (§10.8). Names exactly as in the file headers (PLAN-M9 mapping notes). +- Re-running creates nothing new (import key); imported rows get audit entries with `source` `import` and no pushes (AC-17.2, AC-17.6). +- Unknown categories map to `other` and are counted as a warning (§10.8). +- Splitwise's trailing `Total balance` / `Totalsumma` row is ignored by the importer and is the reference for the comparison (§10.8, §11.2). +- Reports carry aggregates only; no descriptions, no individual amounts, no names beyond the account names (CLAUDE.md). + +## 4. Owned paths +From SPEC §14.7, verbatim: `docs/validation/` | "the milestone that names the file", here `docs/validation/splitwise-import.md`. + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +Open points the orchestrator must settle in the start prompt's notes: (a) how the CSV files reach the importer on the Pi (the `server` image's entrypoint is `kvitt-server`; the CSVs must be mounted or copied, then removed), or whether the import runs on the Mac against a copy of the database that is then restored on the Pi; (b) whether to take a backup immediately before the import (recommended by analogy with §10.10, not stated for imports); (c) the owner's and partner's production user ids (from `GET /me` or the database). + +## 5. Deliverables +- `docs/validation/splitwise-import.md`: per file: rows read, expenses, settlements, skipped duplicates, unmapped category names with counts; per account: the imported balance (net from the owner's side) and Splitwise's total-row balance, equal or not; the re-run result (nothing new); the phones' confirmation; a "Results (owner's phone)" section if anything is still to come. +- Production database on the Pi with the imported history (not in git). + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m11 -b agent-m11 main`, then in `.claude/worktrees/agent-m11`: `git reset --hard main`. Copy `infra/secrets/` from the main checkout if SSH to the Pi needs it. Do not copy `export/` into the worktree; read the files from `/Users/jhag/projects/kvitt/export/`. Run `tools/install-git-hooks.sh`. +2. Read §2 in order. Confirm M9 and M10 have landed and production is healthy; if not, stop and report. +3. Dry run per file against a copy of the production database (on the Mac or the Pi per open point a); record the aggregates; stop and ask if any row is invalid, any currency is not SEK, or unmapped categories are more than the owner expects. +4. Confirm with the owner (through the orchestrator) before writing to production. Backup per open point (b): `docker compose exec backup /backup.sh`, confirmed on Drive. +5. `docker compose stop server`; run the import for "Hemma" then "Vardag" per open point (a); `docker compose start server`; check `/health`. Remove any CSV copied to the Pi. +6. Re-run both imports with `--dry-run`: zero new rows expected. +7. Compute each account's balance from the database with the §9.3 definition (or via the app's numbers pulled over USB), and compare with the total row of each file (parsed by a script that prints only the two member totals); record equal or not. +8. The phones sync; the owner confirms the balances (§8). +9. Write `docs/validation/splitwise-import.md`. Commit on `agent-m11` (only that file; `git status` shows no CSV), `git rebase main`. Never merge, push or remove the worktree. + +## 7. Gate +From SPEC §11.2, verbatim: "Imported balances equal Splitwise's "Total balance" rows per group". +Green means: for "Hemma" and "Vardag" the imported balance equals the file's total row (`Total balance` in the English file, `Totalsumma` in the Swedish file) to the öre, recorded in `docs/validation/splitwise-import.md`; the re-run dry run reports zero new rows. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "Provide the CSVs; confirm the balances on the phone match Splitwise". +- Provide the CSVs: done for both files (`export/`, D-003). If the owner exports newer files before the run, they replace these in `export/` before step 3. +- Before step 5: the owner says yes to writing into production (CLAUDE.md, destructive-data rule). +- After step 8, on each phone: open Kvitt Home and Splitwise side by side; answer yes/no: "Hemma" and "Vardag" show the same balance and direction as Splitwise. + +## 9. Out of scope +- Changing the importer (M9; findings go in the report as new work). +- Importing other groups or later Splitwise data. +- Editing imported rows by hand; deleting anything. +- Committing, copying into the repository, or quoting the CSVs. + +## 10. Report +- Branch `agent-m11`, head commit hash after rebasing on `main`. +- Gate: the per-account comparison lines (equal or not) and the re-run result. +- Dry-run aggregates per file; unmapped category names with counts. +- Backup stamp taken before the import. +- How open points (a) to (c) were resolved. +- Numbering used or `TODO(spec)` occurrences. +- HUMAN steps done and outstanding, worded for relay. +- 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. diff --git a/PLAN-M2a.md b/PLAN-M2a.md new file mode 100644 index 0000000..196b772 --- /dev/null +++ b/PLAN-M2a.md @@ -0,0 +1,104 @@ +# PLAN-M2a: Server skeleton and `kvitt-domain` + +## 1. Goal +Create the Rust Cargo workspace with its three crates, a running `kvitt-server` with config, database pool, schema migration, error type, auth extractor, every route of §7 registered (501 until implemented), `GET /health` and `GET /setup/status`, and a utoipa document equal to `contracts/openapi.yaml`. Implement `kvitt-domain` so that it passes every shared test vector, so M3, M4 and M9 can fill in their own files in parallel. + +## 2. Read first +1. SPEC §6.3 Server (stack and crate table) and §6.1 System view. +2. SPEC §7 API outline. +3. SPEC §14.3 OpenAPI 3.1 outline (14.3.1 Common rules, 14.3.2 Error shape and error codes, 14.3.3 Wire objects, 14.3.4 Endpoints, 14.3.5 Examples as contract). +4. SPEC §14.1 SQLite schema v1 and its server-side rules. +5. SPEC §14.5 Domain rules as pure functions (14.5.1 Types, 14.5.2 Functions, 14.5.3 Error tokens, 14.5.5 Test-vector format). +6. SPEC §9.1 Splits, §9.3 Balances, §9.4 Settlement allocation proposal, §9.6 Limits and validation. +7. SPEC §8.2 Device tokens, §8.4 Authorization rules, §8.6 Rate limiting and input limits, §8.7 Privacy and logging, §8.9 Server hardening. +8. SPEC §10.3 Images, registry and build (the `server` image), §10.4 Server configuration and CLI. +9. SPEC §4 FR-20 Health and AC-1.5 (setup status). +10. SPEC §11.2 (row M2a), §11.3 (notes M1, M2a), §14.7 Module layout and ownership map, §14.8 Contract files and the change rule. +11. Files: `contracts/openapi.yaml`, `contracts/schema/server-v1.sql`, `contracts/test-vectors/*.json`, `contracts/examples/**`, `CLAUDE.md` (BUILD AND VERIFY, ARCHITECTURE RULES, ANTI-PATTERNS). + +## 3. Already established +- Rust stable, edition 2024; axum on tokio multi-thread runtime with 2 worker threads; SQLite via sqlx, WAL, migrations embedded with `sqlx::migrate!`; serde; utoipa; tracing JSON lines to stdout; chrono + chrono-tz; uuid v7; image crate; reqwest with rustls; jsonwebtoken (§6.3). M2a needs only the subset it uses; later plans add the rest. +- Crates: `kvitt-domain` (library, no I/O, depends only on `serde`, `serde_json` for tests, `uuid`), `kvitt-server` (binary plus `lib.rs`), `kvitt-import` (binary, depends on `kvitt-domain` and `kvitt-server` as a library) (§6.3, CLAUDE.md Server layering). +- `migrations/0001_init.sql` is byte-identical to `contracts/schema/server-v1.sql`; a test asserts it (§14.1). +- Every connection sets `foreign_keys = ON`, `journal_mode = WAL`, `synchronous = NORMAL`, `busy_timeout = 5000` (§8.9). Every write transaction is `BEGIN IMMEDIATE`; `server_version` comes from `UPDATE sync_sequence SET value = value + 1 WHERE id = 1 RETURNING value` (§14.1). +- Error type covers exactly the 25 codes of §14.3.2 with their HTTP statuses and the JSON shape `{code, message, field}`; `not_implemented` (501) exists only during M2a to M4 (§14.3.2). +- Auth: bearer `kvt_` token, SHA-256 hex lookup, 401 `unauthorized` / `device_revoked`, `last_seen_at` and `app_version_code` updated at most once per hour per device; admin-only routes 403 `forbidden` for members (§8.2, §8.4). +- Every route of §7 is registered up front with a handler returning 501 `not_implemented`, and the utoipa `ApiDoc` lists them all, so M3 and M4 only fill in handler bodies in their own files (§11.2, §11.3). +- `GET /health` returns `{status, server_version, schema_version, db_ok}`, 503 `degraded` when `SELECT 1` fails, unauthenticated, under 100 ms on the Pi (FR-20, §14.3.4 E-1). `GET /setup/status` returns `{"initialized": bool}` (AC-1.5). +- Environment variables are exactly those of §10.4 with their defaults; `Europe/Stockholm` is a code constant. Subcommands are exactly `serve`, `bootstrap`, `pair-device`, `rotate-epoch`, `healthcheck`, `migrate` (§10.4); M2a implements `serve`, `migrate`, `healthcheck`. +- `server/Dockerfile`: stage 1 `rust:-bookworm` builds both binaries release `--locked` for `aarch64-unknown-linux-gnu` (no `target-cpu=native`); stage 2 `gcr.io/distroless/cc-debian12:nonroot` arm64, binaries in `/usr/local/bin/`, user 10001, entrypoint `kvitt-server`, command `serve` (§10.3). +- Every response carries `X-Kvitt-Server-Version`; authenticated endpoints require `X-Kvitt-Protocol: 1` else 426 `protocol_unsupported`; unknown request fields ignored; missing/wrong fields 400 `invalid_request`; path ids that are not UUIDs 404 `not_found`; optional response fields present as `null` (§14.3.1). +- Logs never contain tokens, invite codes, names, descriptions, notes, amounts or picture bytes; they log route templates, not raw paths (§8.7). +- No `unwrap()`/`expect()` outside `main` start-up, no `unsafe` (CLAUDE.md). +- M1 has landed (commit `c6df2ab`): `contracts/openapi.yaml` exists with 33 operations and 60 schemas, and the utoipa document must match it; differences may only be representational. M1's readings are now explicit in SPEC v0.2 (§14.3.1, §14.3.2 `not_implemented` on no operation, §14.3.4 E-1/E-3/E-12/E-17/E-31/E-33, §14.4.3 result key sets, §14.5.1, §14.5.2, §14.5.5); do not re-decide them. +- `contracts/openapi.yaml` may be changed by M2a only for representational differences that do not change meaning (key order, `$ref` naming), recorded in the report; anything semantic is a contract change (§11.3, §14.8). + +## 4. Owned paths +From SPEC §14.7, verbatim: +- `server/Cargo.toml`, `server/rust-toolchain.toml`, `server/Dockerfile`, `server/crates/kvitt-domain/**` (owner M2a; after landing: "contract-change plans only for `kvitt-domain`; M3, M4 and M9 may add a dependency line to a crate's `Cargo.toml` (second to land resolves `Cargo.lock` on rebase)"). +- `kvitt-server`: `migrations/`, `src/{main,lib,config,error,router,openapi,auth,time}.rs`, `src/db/**`, `src/wire/**`, `src/api/{mod,health,setup}.rs`, `src/cli/{mod,healthcheck}.rs` (owner M2a; after landing: "contract-change plans; M3/M4 may add one line per new module to `mod.rs` files they need"). +- Exception granted by §11.3: representational (non-semantic) edits to `contracts/openapi.yaml`. + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +Open points the orchestrator must settle in the start prompt's notes (stop and ask if they are not settled): (a) the 501 stub handlers for E-3 to E-30, E-33 and E-31/E-32 belong in files §14.7 gives to M3 (`src/api/{invites,landing,me,pictures,devices,accounts,categories,recurring,proposal,apk}.rs`) and M4 (`src/api/sync.rs`), the subcommand stubs in `src/cli/{bootstrap,pair_device,rotate_epoch}.rs`, and the third crate in `server/crates/kvitt-import/**` (M9): whether M2a creates these files as stubs that ownership then passes on; (b) `server/Cargo.lock`, `server/crates/kvitt-server/Cargo.toml` and `server/crates/kvitt-import/Cargo.toml` are not in the ownership table; (c) names for M2a's own integration tests under `kvitt-server/tests/` (M3 has `core_*`, M4 `sync_*`, `scheduler_*`, `push_*`); (d) where the protocol-header check, body-size limits (§8.6) and the rate-limit layer that M3 writes in `src/ratelimit.rs` attach to `router.rs`; (e) how M4's scheduler and push sender get started by `serve` (`main.rs`/`lib.rs`) when M4 may only add `mod.rs` lines; (f) whether the OpenAPI equality test compares parsed values or text. + +## 5. Deliverables +- `server/Cargo.toml`: workspace with members `crates/kvitt-domain`, `crates/kvitt-server`, `crates/kvitt-import`; shared lints (deny `unsafe_code`, clippy `unwrap_used`/`expect_used` outside tests). +- `server/Cargo.lock`: committed. +- `server/rust-toolchain.toml`: the Rust stable version chosen (see step 3). +- `server/Dockerfile`: the two-stage arm64 image of §10.3. +- `server/crates/kvitt-domain/Cargo.toml`, `src/lib.rs`, `src/split.rs`, `src/balance.rs`, `src/allocation.rs`, `src/settlement.rs`: types and functions of §14.5.1 and §14.5.2. +- `server/crates/kvitt-domain/tests/vectors.rs`: runs every case of the five Rust vector files in `contracts/test-vectors/`. +- `server/crates/kvitt-server/Cargo.toml`, `src/main.rs` (CLI parsing, start-up), `src/lib.rs` (public modules for `kvitt-import`), `src/config.rs` (§10.4 variables), `src/error.rs` (§14.3.2), `src/router.rs` (every route of §7, layers of §14.3.1), `src/openapi.rs` (utoipa `ApiDoc`), `src/auth.rs` (device and admin extractors, §8.2, §8.4), `src/time.rs` (fixed RFC 3339 form, `Europe/Stockholm` constant, injectable clock). +- `src/db/mod.rs`, `src/db/pool.rs` (pool with the §8.9 pragmas, `BEGIN IMMEDIATE` helper), `src/db/sequence.rs` (next `server_version`). +- `migrations/0001_init.sql`: byte copy of `contracts/schema/server-v1.sql`. +- `src/wire/{mod,user,device,invite,account,category,expense,settlement,recurring,audit,sync}.rs`: serde wire objects of §14.3.3 and §14.4 with utoipa schemas, arrays sorted as §14.3.3 says. +- `src/api/mod.rs`, `src/api/health.rs` (E-1), `src/api/setup.rs` (E-2). +- `src/cli/mod.rs` (subcommand dispatch, `serve`, `migrate`), `src/cli/healthcheck.rs`. +- 501 stubs for the remaining routes and subcommands, placed as the orchestrator decides (open point a). +- Integration tests in `kvitt-server/tests/` (names per open point c): OpenAPI document equals `contracts/openapi.yaml`; `0001_init.sql` equals `contracts/schema/server-v1.sql` byte for byte; every route returns 501 `not_implemented` with the error shape; E-1 and E-2 responses; auth extractor cases (missing, unknown, revoked token; member on admin route); protocol header 426; error JSON for every code serialises like `contracts/examples/errors/.json`. +- `server/crates/kvitt-import/` minimal crate that builds (if open point a says M2a creates it). + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m2a -b agent-m2a main`, then in `.claude/worktrees/agent-m2a`: `git reset --hard main`. No git-ignored files are needed; copy none. Run `tools/install-git-hooks.sh`. +2. Read §2 in order. Confirm `contracts/` exists on `main` (M1 landed); if not, stop and report. +3. Versions: "Rust stable" (§6.3) and `rust:-bookworm` (§10.3). Look up the current stable Rust release on the official Rust site (rust-lang.org / the Rust release blog) and the matching tag on the official Docker Hub `rust` image page; pin the same version in `rust-toolchain.toml` and the Dockerfile. Look up current stable versions of each crate you add on crates.io / docs.rs and the digest of `gcr.io/distroless/cc-debian12:nonroot`. Confirm current axum, sqlx and utoipa usage from their official docs (CLAUDE.md "Fast-moving APIs"). Record every version in the report. +4. Write `kvitt-domain` test-first: `tests/vectors.rs` loads `../../../contracts/test-vectors/{compute_split,compute_balances,propose_allocation,validate_settlement,over_settled_accounts}.json` and checks every case; then implement §14.5.2 with the error order of the enums. Run `cargo test -p kvitt-domain`. +5. Write `config.rs`, `time.rs`, `error.rs`, `db/**`, `migrations/0001_init.sql` (copy the bytes with `cp contracts/schema/server-v1.sql server/crates/kvitt-server/migrations/0001_init.sql`), `wire/**`. +6. Write `auth.rs` and `router.rs` with every route of §7, the §14.3.1 headers and limits, and stub handlers per open point (a). +7. Write `openapi.rs` with utoipa annotations on every handler and wire type; make the generated document equal `contracts/openapi.yaml` (comparison per open point f). Only representational changes to the YAML are allowed; list each in the report. +8. Implement E-1, E-2, `serve` (graceful SIGTERM drain up to 10 s, §10.4), `migrate`, `healthcheck`. +9. Write the integration tests of §5 with an in-memory or temp-file SQLite and an injected clock. +10. Write `server/Dockerfile`. Build it once locally only if no Gradle build is running (CLAUDE.md); record the image size. Do not push it anywhere. +11. Run the gate (§7) in `server/`. Commit on `agent-m2a`, `git rebase main`, rerun the gate. Never merge, push or remove the worktree. + +## 7. Gate +From SPEC §11.2, verbatim: "Server gate (includes the OpenAPI match test, the migration identity test, the vector tests)". +Command, from `server/`: `cargo fmt --check && cargo clippy -- -D warnings && cargo test`. +Green means: all three exit 0 in this session; the `test result:` lines show 0 failed and include the OpenAPI match test, the migration identity test and the vector tests (each named in the report). + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "none". + +## 9. Out of scope +- Handler bodies of E-3 to E-33 other than 501 stubs (M3: E-3 to E-30, E-33; M4: E-31, E-32). +- Invite landing page, pictures, rate-limit logic, `bootstrap`, `pair-device` (M3). +- Sync, audit, push, scheduler, `rotate-epoch`, `sync_mutations` pruning (M4). +- The Splitwise importer logic (M9). +- `infra/` compose files and image publishing (M2c-dev, M2c-pi). +- Any Android work (M2b and later). +- Semantic changes to any contract file. + +## 10. Report +- Branch `agent-m2a`, head commit hash after rebasing on `main`. +- Gate output: fmt and clippy exit status, every `test result:` line, and the names of the OpenAPI match, migration identity and vector tests. +- Versions chosen (Rust, Docker base images with digests, every crate) with the official source. +- Representational changes made to `contracts/openapi.yaml`, one line each. +- How each open point (a) to (f) was resolved, as given in the start prompt. +- Numbering used (decision ids, spec version) or `TODO(spec)` occurrences. +- Contract requests, open questions, spec gaps (with section numbers). +- Docker image build result (size) if built. + +## 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. diff --git a/PLAN-M2b.md b/PLAN-M2b.md new file mode 100644 index 0000000..27862e8 --- /dev/null +++ b/PLAN-M2b.md @@ -0,0 +1,97 @@ +# PLAN-M2b: Android skeleton and `:domain` + +## 1. Goal +Create the Gradle project in `android/` with every module of §6.2, the version catalog, the architecture check, the `:core:model` UI contract and domain types, a `:domain` that passes every shared test vector including analytics, empty fakes, and an `:app` that launches to an empty themed screen. After it lands, M5 (data), M6 (design) and M7a to M7f (features) build against the same interfaces in parallel. + +## 2. Read first +1. SPEC §6.2 Android app (stack table, module graph, the paragraph on the architecture check). +2. SPEC §14.2.4 UI contract: types and repository interfaces in `:core:model`. +3. SPEC §14.5 Domain rules as pure functions (14.5.1 Types, 14.5.2 Functions, 14.5.3 Error tokens, 14.5.4 Analytics, 14.5.5 Test-vector format). +4. SPEC §9.1 Splits, §9.3 Balances, §9.4 Settlement allocation proposal, §9.6 Limits and validation. +5. SPEC §4 FR-14 Analytics (AC-14.2, AC-14.7, AC-14.10) and FR-19 Localisation and formatting. +6. SPEC §14.2 Room entities (only to know what `:data` will hold; M5 writes it). +7. SPEC §11.2 (row M2b), §11.3 (notes M2b, M7x, M8), §14.7 Module layout and ownership map, §14.8. +8. Files: `contracts/test-vectors/*.json`, `android/gradle.properties`, `android/gradle/gradle-daemon-jvm.properties`, `tools/gradle-serial.sh`, `tools/dependency-report/latest.py`, `tools/git-hooks/pre-commit`, `CLAUDE.md` (BUILD AND VERIFY, ARCHITECTURE RULES, ANTI-PATTERNS, THE PHONE). + +## 3. Already established +- Kotlin 2.x latest stable; Compose with Material 3; `minSdk` 36; `targetSdk` and `compileSdk` the latest stable API; Room with KSP; Hilt; Ktor client (OkHttp engine) with kotlinx.serialization; WorkManager; kotlinx-datetime; Coil; Vico or Canvas charts (decided by M6); FCM only in `:adapter:fcm`; version catalog `android/gradle/libs.versions.toml`; Gradle daemon on JDK 25 (pinned, existing files); bytecode target Java 17 (§6.2). +- Screenshot tests use Roborazzi or AGP Compose Preview Screenshot Testing, whichever is stable at implementation time, recorded in decisions (§6.2). +- Modules and allowed edges exactly as the §6.2 table; features never depend on `:data` or `:domain`; `:core:fakes` only for tests, screenshot tests and debug previews (§6.2, CLAUDE.md Layering). +- The architecture check is a Gradle task in `check`, owned by M2b; it fails on any other dependency edge and on `java.time`, `java.io` or `android.*` imports in `:core:model` and `:domain` (§6.2). +- `:core:model` package `se.hagstedt.kvitt.model` holds the §14.2.4 types and interfaces verbatim; domain input/output types in `se.hagstedt.kvitt.model.money` (§14.5.1); `:domain` package `se.hagstedt.kvitt.domain` with the §14.5.2 functions and §14.5.4 analytics (§14.5). +- Wire value of every enum = lowercase name; icon tokens are Strings from `CategoryIcons.ALL`, the 30 tokens of §9.7 in that order (§14.2.4). +- `:domain` passes every vector file in `contracts/test-vectors/`, including `spend_series.json` and `resolve_range.json` (Kotlin only) (§14.5, §11.2). +- Strings live in `values/strings.xml` (English fallback) and `values-sv/strings.xml`; a lint or test in the gate fails on a key missing from either file (AC-19.1). +- Application ids: release `se.hagstedt.kvitt`, debug `se.hagstedt.kvitt.debug`, instrumented tests `se.hagstedt.kvitt..test` (CLAUDE.md THE PHONE). +- No mocking libraries, no `!!`, no `@Suppress`, no lint-baseline additions (CLAUDE.md). +- M1 has landed (`c6df2ab`); its readings are explicit in SPEC v0.2: domain functions receive only non-deleted rows; analytics vector keys per §14.5.5; `bp_out_of_range` means above 10000; `resolveRange` edge cases per §14.5.4 (how `CUSTOM` with a null date signals `validation_failed` is for M2b to fix and report). +- `:core:model/**` and `:domain/**` become contract files once landed (§14.8). + +## 4. Owned paths +From SPEC §14.7, verbatim: +- `android/settings.gradle.kts`, `android/build.gradle.kts`, `android/gradle.properties`, `android/gradle/**`, `android/build-logic/**` (owner M2b; after landing: "later plans may add catalog entries and module includes (rebase resolves)"). +- `android/core/model/**`, `android/domain/**` (owner M2b; after landing: "contract-change plans only"). +- `android/core/fakes/**` (owner M2b; after landing: "M7x add files only under `fakes//`"). +- `android/app/**` up to and including "the M2b shell" (the row `android/adapter/fcm/**`, `android/app/**` (after the M2b shell) | M8). + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +Open points the orchestrator must settle in the start prompt's notes (stop and ask if they are not settled): (a) "every module of §6.2" means build files in `android/core/design/`, `android/data/`, `android/adapter/fcm/` and `android/feature/*/`, which §14.7 gives to M6, M5, M8 and M7x: whether M2b creates them as empty module shells that ownership then passes on; (b) where the theme of the "empty themed screen" lives, given `:core:design` belongs to M6; (c) which module holds the "string resource files for both languages" M2b creates; (d) which screenshot-test tool, and whether M2b wires it into `check` (needs a decision id); (e) whether feature modules use Hilt (`hiltViewModel`) or receive repositories as parameters, and how cross-feature navigation is expressed, since the feature convention plugin in `build-logic` fixes it for M7a to M7f; (f) the package of the §14.5.4 analytics types (`:core:model` money package or `:domain`); (g) what an "empty scenario" is for repositories whose state has required fields (for example `HomeState.me`). + +## 5. Deliverables +- `android/settings.gradle.kts`, `android/build.gradle.kts`: the project with every module of §6.2. +- `android/gradle/libs.versions.toml`: every library and plugin of §6.2 with the versions chosen. +- `android/gradle/wrapper/` (`gradle-wrapper.properties`, `gradle-wrapper.jar`) and `android/gradlew`, `android/gradlew.bat`. +- `android/gradle.properties`, `android/gradle/gradle-daemon-jvm.properties`: the existing JDK 25 pin and flags kept, merged with what the project needs. +- `android/build-logic/**`: convention plugins (pure JVM module, Android library, feature module, application) and the architecture check task wired into `check`. +- `android/core/model/**`: §14.2.4 and §14.5.1 types and interfaces, verbatim, plus unit tests for enum wire values and `CategoryIcons.ALL`. +- `android/domain/**`: §14.5.2 functions and §14.5.4 analytics; a vector test runner reading `../contracts/test-vectors/*.json` for all seven files. +- `android/core/fakes/**`: one fake per repository interface of §14.2.4 driven by a scenario type, with an empty scenario each; fakes compute derived values through `:domain`. +- `android/app/**`: `Application` class, `MainActivity` showing an empty themed screen, debug `applicationIdSuffix` `.debug`, manifest. +- Module shells for `:core:design`, `:data`, `:adapter:fcm`, `:feature:{onboarding,home,account,expense,settle,analytics,settings}` (per open point a). +- `values/strings.xml` and `values-sv/strings.xml` with the app name (per open point c), and the missing-translation check (Android lint `MissingTranslation` as an error, or a test) in `check`. +- Screenshot-test tooling wired into `check` (per open point d) with one screenshot of the empty screen in light and dark. +- `docs/decisions.md` entry for the screenshot tool only if the start prompt grants a decision id and the path (the file is not owned by M2b; otherwise put the text in the report as a contract request). + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m2b -b agent-m2b main`, then in `.claude/worktrees/agent-m2b`: `git reset --hard main`. Copy `android/local.properties` from the main checkout if it exists (`cp /Users/jhag/projects/kvitt/android/local.properties android/`); if it does not, create it with the single line `sdk.dir=/opt/homebrew/share/android-commandlinetools`. Never overwrite an existing one, never commit it. Run `tools/install-git-hooks.sh`. +2. Read §2 in order. Confirm `contracts/test-vectors/` exists on `main`; if not, stop and report. +3. Versions ("latest stable" in §6.2): resolve from official sources and record each with its source: Kotlin (kotlinlang.org releases), Android Gradle Plugin and the latest stable `compileSdk`/`targetSdk` API level (developer.android.com release notes), Gradle wrapper (gradle.org releases; must support JDK 25 daemon and the AGP chosen), Compose BOM, Material 3 (developer.android.com/jetpack/androidx/releases), KSP matching the Kotlin version, Room, Hilt, Ktor, kotlinx.serialization, kotlinx.coroutines, kotlinx-datetime, WorkManager, DataStore, Coil, Roborazzi or the AGP screenshot plugin. Check whether the AGP Compose Preview Screenshot Testing plugin is stable; report the finding for open point (d). +4. Generate the Gradle wrapper with the chosen version; keep `android/gradle.properties` and `android/gradle/gradle-daemon-jvm.properties` content (JDK 25 pin, auto-download off). +5. Write `build-logic` convention plugins and the architecture check (allowed edges per the §6.2 table; forbidden imports `java.time`, `java.io`, `android.*` in `:core:model` and `:domain`); add a failing-case test of the check if `build-logic` supports it. +6. Write `:core:model` exactly from §14.2.4 and §14.5.1 (no added fields). Write `:domain` test-first against all seven vector files, then implement §14.5.2 and §14.5.4. +7. Write `:core:fakes` (one fake per interface, scenario-driven, empty scenario each) and the module shells. +8. Write `:app`: themed empty screen, string resources in both languages, missing-translation check, screenshot tooling with one golden in light and dark; open the PNGs before committing (CLAUDE.md Screenshots). +9. Run `python3 tools/dependency-report/latest.py` and record its lines. +10. Run the gate (§7). If the owner's phone is attached and the start prompt names this plan as the one on the phone: `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH tools/gradle-serial.sh installDebug`, after `/opt/homebrew/share/android-commandlinetools/platform-tools/adb shell pm list packages | grep se.hagstedt.kvitt` to confirm the ids. +11. Commit on `agent-m2b`, `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` in this session; the architecture check, `:core:model` and `:domain` unit tests (all seven vector files), lint with the missing-translation rule, and the screenshot test all ran (task names listed in the report); test counts from fresh XML under `build/test-results/`. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "Plug the phone in once to confirm `installDebug` works". +Where in the flow: after the gate is green (step 10). The orchestrator relays the step; the agent runs the package check and `installDebug` itself and reports the result. If the phone is not attached, report the step as outstanding. + +## 9. Out of scope +- Room, DAOs, Ktor client, sync engine, token store, workers, real repositories, APK updater (M5). +- Theme tokens, typography, shared components, charts, `MoneyFormatter`, `DateFormatter` (M6). +- Any feature screen (M7a to M7f) and fake scenarios beyond the empty one (M7x add theirs). +- Navigation graph, Hilt bindings to `:data`, deep links, FCM, notifications, network security config, release signing (M8). +- Server work (M2a and later), `infra/` (M2c-dev). +- Adding fields to §14.2.4 types (contract change). + +## 10. Report +- Branch `agent-m2b`, head commit hash after rebasing on `main`. +- Gate output: BUILD line, actionable tasks line, the names of the architecture-check, lint and screenshot tasks that ran, unit test counts from fresh XML (per module). +- Versions chosen (Kotlin, AGP, Gradle, compile/target SDK, every catalog entry) with the official source; dependency-report lines. +- Screenshot tool chosen and why (and the decisions text if no decision id was granted). +- How each open point (a) to (g) was resolved, as given in the start prompt. +- `installDebug` result on the phone, or "outstanding". +- Numbering used (decision ids, spec version) 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. diff --git a/PLAN-M2c-dev.md b/PLAN-M2c-dev.md new file mode 100644 index 0000000..e364a29 --- /dev/null +++ b/PLAN-M2c-dev.md @@ -0,0 +1,82 @@ +# PLAN-M2c-dev: Infra for development (D-001) + +## 1. Goal +Give development a local server on the owner's M4 Mac: a Docker compose stack with the `server` (built from `server/Dockerfile`), optional Caddy, and an on-demand backup job, reachable by debug builds on the owner's phone over the home Wi-Fi. Set up the Firebase project and apps now, because push is needed during development. + +## 2. Read first +1. SPEC §10 Deployment & ops: the intro paragraph "Development first (D-001)", §10.2 Compose stack (service shapes, limits, logging), §10.3 Images, registry and build (the `server` and `backup` images), §10.4 Server configuration and CLI, §10.7 Firebase setup, §10.9 Backups and restore (backup steps 1 to 5). +2. SPEC §4 AC-2.7 and AC-2.8 (debug builds accept `http://` to private LAN addresses), FR-20 Health. +3. SPEC §8.7 Privacy and logging, §8.9 Server hardening (container user, read-only root, pragmas). +4. SPEC §11.2 (row M2c-dev), §11.3 (note M2c-dev / M2c-pi), §12 (OQ-1 answered, OQ-3 open for M2c-pi only), §14.7. +5. `docs/decisions.md` D-001, D-002. +6. Files: `server/Dockerfile` (from M2a, if landed), `.gitignore`, `CLAUDE.md` (BUILD AND VERIFY: Cargo and Docker builds; EXECUTION PROTOCOLS: Stay inside the repository, Stop and ask; THE OWNER). + +## 3. Already established +- During development the server runs on the Mac, via `infra/dev/compose.yaml` or plain `cargo run`; debug builds pair against it (D-001, §10 intro). +- The dev stack has `server`, an optional `caddy` with a self-signed certificate or no TLS for local use, and `backup` runnable on demand without the Drive upload (D-001, §10 intro, §11.2). +- `backup.sh` steps: `.backup` snapshot, `PRAGMA integrity_check` must print `ok`, gzip and pictures tar with a UTC stamp, rclone copy only, `last-success` marker; when `RCLONE_FOLDER_ID` is empty step 4 is skipped with a log line and local files are kept (§10.9, D-001). On the Pi, local files are deleted as soon as confirmed on Drive (D-002; built by M2c-pi). +- `backup` image: `alpine` with `sqlite`, `gzip`, `tar`, `rclone`, `infra/backup/backup.sh` run by `crond` (§10.3). +- Server config is only the env variables of §10.4; `KVITT_FCM_CREDENTIALS` default `/secrets/fcm-service-account.json`, push disabled with a warning when missing; `KVITT_FCM_PROJECT_ID` `kvitt-1337` (§10.4). +- Firebase: project `kvitt-1337`; Android apps `se.hagstedt.kvitt` ("Kvitt") and `se.hagstedt.kvitt.debug` ("Kvitt debug"); one merged `android/app/google-services.json` (git-ignored); service account `kvitt-fcm` with `roles/firebasecloudmessaging.admin`, key at `infra/secrets/fcm-service-account.json`; stop if an organisation policy blocks key creation; free tier, no billing (§10.7). +- The registry is Gitea at `http://gitea.lan`; not used by this plan (D-001, OQ-1). +- Secrets live in `infra/secrets/` (git-ignored) and are never committed (§10 intro, `.gitignore`). +- Never build Docker images while a Gradle build runs (CLAUDE.md). + +## 4. Owned paths +From SPEC §14.7, verbatim: `infra/**` (owner "M2c-dev, then M2c-pi (D-001)"; after landing: "M10 updates runbooks"). +Also, from §11.2 (row M2c-dev): the git-ignored `android/app/google-services.json` and `infra/secrets/` contents, written in the main checkout `/Users/jhag/projects/kvitt` (where every worktree copies them from, CLAUDE.md), only with the start prompt's allowance. + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +## 5. Deliverables +- `infra/dev/compose.yaml`: `server` (image built from `server/Dockerfile`, data in a named volume, FCM key mounted read-only from `infra/secrets/`, `KVITT_PUBLIC_URL` set to the Mac's LAN URL, user 10001, read-only root, no capabilities, `no-new-privileges`), optional `caddy`, `backup` (on demand), logging and `TZ=Europe/Stockholm` as §10.2. +- `infra/dev/Caddyfile`: only if the stack includes Caddy (self-signed via `tls internal`, or plain HTTP). +- `infra/dev/README.md`: start, stop, logs, `bootstrap` and `pair-device` commands, finding the Mac's LAN address, pairing a debug build over the home Wi-Fi, running a backup, resetting the dev data. +- `infra/backup/Dockerfile`, `infra/backup/backup.sh`: the backup image and script of §10.9 with the empty-`RCLONE_FOLDER_ID` local mode. +- `infra/env.example`: the `.env` keys of §10.2 with dev-friendly comments (no secret values). +- Firebase project `kvitt-1337` with both Android apps (cloud state, no repository file). +- `infra/secrets/fcm-service-account.json` (git-ignored, main checkout). +- `android/app/google-services.json` with both clients (git-ignored, main checkout). + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m2c-dev -b agent-m2c-dev main`, then in `.claude/worktrees/agent-m2c-dev`: `git reset --hard main`. Copy from the main checkout whatever already exists of `infra/secrets/` (`mkdir -p infra/secrets && cp -R /Users/jhag/projects/kvitt/infra/secrets/. infra/secrets/` if present). Run `tools/install-git-hooks.sh`. +2. Read §2 in order. Check the tools: `command -v docker gcloud firebase shellcheck rclone`, `docker buildx version`, `docker compose version`. If `docker`, `gcloud` or `firebase` is missing, stop and ask the owner (do not install). +3. Versions: resolve from official sources and record: the `alpine` image tag and digest (Docker Hub official image), `rclone` package version in that Alpine release, the `caddy` image version and digest if Caddy is used (Docker Hub official image). Pin by version and digest. +4. Write `infra/backup/backup.sh` and `infra/backup/Dockerfile`; run `shellcheck` on the script. +5. Write `infra/dev/compose.yaml` (and `infra/dev/Caddyfile` if used), `infra/env.example`, `infra/dev/README.md`. Record which TLS option (self-signed or none) was chosen and why. +6. Run `docker compose -f infra/dev/compose.yaml config`. +7. Firebase (needs HUMAN step H-1, §8). After the owner signed in, run §10.7 steps 2 to 6 exactly: `gcloud config set project kvitt-1337`; `gcloud services enable fcm.googleapis.com firebase.googleapis.com`; `firebase projects:addfirebase kvitt-1337` (skip if already a Firebase project); `firebase apps:create ANDROID "Kvitt" --package-name se.hagstedt.kvitt`; `firebase apps:create ANDROID "Kvitt debug" --package-name se.hagstedt.kvitt.debug`; `firebase apps:sdkconfig ANDROID ` for each, merged into one `android/app/google-services.json` in the main checkout; `gcloud iam service-accounts create kvitt-fcm --display-name "Kvitt FCM sender"`; grant `roles/firebasecloudmessaging.admin` on the project; `gcloud iam service-accounts keys create /Users/jhag/projects/kvitt/infra/secrets/fcm-service-account.json --iam-account kvitt-fcm@kvitt-1337.iam.gserviceaccount.com`. If an organisation policy blocks key creation, stop and report; change no policy. Copying the key to the Pi (§10.7 step 7) belongs to M2c-pi. +8. Health check (needs M2a on `main`; if not landed, report this part as outstanding): build the server image locally (no Gradle build running), `docker compose -f infra/dev/compose.yaml up -d`, `curl -fsS http://localhost:/health` on the Mac; then from the owner's phone on the home Wi-Fi (see §8 H-3). +9. Backup check: run the backup job once on demand (`docker compose -f infra/dev/compose.yaml run --rm backup /backup.sh` or as written in the README), then `sqlite3 "PRAGMA integrity_check"` prints `ok`. +10. Run `shellcheck infra/**/*.sh` (in `zsh` or `bash` with `shopt -s globstar`, so `**` recurses) if shellcheck is installed. +11. Run the gate (§7). Commit on `agent-m2c-dev` (never a secret), `git rebase main`, rerun the gate. Never merge, push or remove the worktree. Leave the dev stack running or stopped as the start prompt says, and say which in the report. + +## 7. Gate +From SPEC §11.2, verbatim: "`docker compose -f infra/dev/compose.yaml config` succeeds; `shellcheck infra/**/*.sh` clean if shellcheck is installed; on the Mac (with the M2a image): `GET /health` answers 200 from the Mac and from the owner's phone on the home Wi-Fi; one local backup run produces a snapshot that passes `PRAGMA integrity_check`". +Commands: `docker compose -f infra/dev/compose.yaml config`; `shellcheck infra/**/*.sh` (globstar on); `curl -fsS http://localhost:/health`; the phone check of H-3; `sqlite3 "PRAGMA integrity_check"`. +Green means: `config` exits 0; shellcheck prints nothing and exits 0 (or "not installed" stated); `/health` returns 200 with `"status":"ok"` from the Mac and the phone; integrity check prints `ok`. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "`gcloud auth login`, `firebase login`". No open question blocks this plan (OQ-1 answered; OQ-3 only blocks M2c-pi). +- H-1, before step 7, on the owner's Mac: `gcloud auth login` (sign in with the owner's Google account in the browser that opens), then `firebase login` (same account). Expected result: both commands end with a "logged in as" line. +- H-2 (only if step 2 finds a tool missing; not a §11.2 step): the owner decides whether and how to install it. +- H-3, in step 8 (needed by the gate; not listed in §11.2): if the agent cannot query the server from the phone over USB itself, the owner opens `http://:/health` in Chrome on the phone on the home Wi-Fi and answers yes/no: the page shows `"status":"ok"`. If macOS asks whether Docker may accept incoming connections, the owner taps Allow. + +## 9. Out of scope +- Everything that concerns the Pi (M2c-pi): `infra/compose.yaml`, `infra/Caddyfile`, cert-sync image, `infra/build-images.sh`, `infra/deploy.sh`, `infra/publish-apk.sh`, `infra/README.md`, `infra/RESTORE.md`, the Gitea registry and Gitea Actions, router forwarding, the Drive upload and `rclone authorize`, copying secrets to the Pi. +- Server code (M2a, M3, M4); Android code and Gradle files (M2b and later). +- Changing org policies or billing in Google Cloud. + +## 10. Report +- Branch `agent-m2c-dev`, head commit hash after rebasing on `main`. +- Gate output lines: `config` exit status, shellcheck result, `/health` responses (Mac, phone), integrity check output. +- Versions chosen (images with digests, rclone) with sources; TLS option chosen. +- Firebase: app ids created (ids are not secrets), service account email, whether `google-services.json` holds both clients; any policy block. +- Where the secrets were written (paths only, never contents). +- Whether the dev stack is left running, and its URL. +- Numbering used or `TODO(spec)` occurrences. +- HUMAN steps done and outstanding, worded for relay. +- 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. diff --git a/PLAN-M2c-pi.md b/PLAN-M2c-pi.md new file mode 100644 index 0000000..641ca1a --- /dev/null +++ b/PLAN-M2c-pi.md @@ -0,0 +1,102 @@ +# PLAN-M2c-pi: Infra on the Pi (D-001) + +Runs after M8 lands and before M10 (D-001, §11.1). + +## 1. Goal +Deploy Kvitt's production stack on the Raspberry Pi 3: Caddy with the Let's Encrypt certificate synced from the Home Assistant Pi, the server, cert-sync and nightly Drive backups, with images in the Gitea registry at `http://gitea.lan`. Write the runbooks so the owner can provision, deploy, publish the APK and restore without an agent. + +## 2. Read first +1. SPEC §10 Deployment & ops, all of it: intro "Development first (D-001)", §10.1 Hosts and network, §10.2 Compose stack, §10.3 Images, registry and build, §10.4 Server configuration and CLI, §10.5 TLS (10.5.1 Certificate sync, 10.5.2 Caddyfile, 10.5.3 Fallback), §10.6 APK hosting and release, §10.7 Firebase setup (step 7), §10.9 Backups and restore, §10.10 Operations. +2. SPEC §8.5 Transport, §8.9 Server hardening, §8.7 Privacy and logging. +3. SPEC §4 FR-18 Backups and restore, FR-20 Health (AC-20.2). +4. SPEC §11.2 (rows M2c-dev, M2c-pi, M10), §11.3 (note M2c-dev / M2c-pi), §12 (OQ-1, OQ-3, OQ-4), §14.7. +5. `docs/decisions.md` D-001, D-002. +6. Files: everything under `infra/` from M2c-dev (`infra/dev/**`, `infra/backup/**`, `infra/env.example`), `server/Dockerfile`, `.gitignore`, `CLAUDE.md` (EXECUTION PROTOCOLS: Stay inside the repository, Stop and ask; THE OWNER). + +## 3. Already established +- Hosts: Kvitt Pi (Raspberry Pi 3, 1 GB, 64-bit Raspberry Pi OS Lite, Docker Engine with compose plugin, static LAN address); Home Assistant Pi owns 80/443 and the certificate; ASUS router forwards external TCP 8443 to Kvitt Pi 8443; the Pi never builds anything (§10.1). +- Registry: Gitea at `http://gitea.lan`, plain HTTP, home Wi-Fi only; the Pi's Docker daemon trusts it via `insecure-registries`; images will be built by Gitea Actions from the owner's example workflow; until then `infra/build-images.sh` on the Mac pushes to `gitea.lan` (D-001, §10.1, §10.3). Gitea credentials, if needed, come from the owner and live in `infra/secrets/` (§10.1). +- Compose stack exactly as §10.2: services `caddy`, `server`, `cert-sync`, `backup`, their images, volumes, memory limits, healthchecks, `depends_on`, `restart: unless-stopped`, json-file logging 10m × 3, `TZ=Europe/Stockholm`, network `kvitt`; `.env` keys exactly `KVITT_REGISTRY`, `KVITT_TAG`, `HA_HOST`, `HA_PORT`, `HA_USER`, `HA_CERT_DIR`, `RCLONE_REMOTE` (default `gdrive:`), `RCLONE_FOLDER_ID`. +- Files deployed to `/opt/kvitt/compose.yaml`, `/opt/kvitt/.env`, secrets in `/opt/kvitt/secrets/` (§10.2, §10 intro). +- Images: `server` per §10.3 (M2a's Dockerfile); `cert-sync` = `alpine` + `openssh-client`, `curl`, `infra/cert-sync/sync.sh` under `crond`; `backup` from M2c-dev (§10.3). +- Scripts: `infra/build-images.sh [tag]` (buildx `linux/arm64 --push`, tag default short SHA, also `latest`, refuses a dirty tree); `infra/deploy.sh ` (copy compose and Caddyfile over SSH, set `KVITT_TAG`, pull, up, wait healthy, print `/health`; triggers a backup and waits for `last-success` before a deploy with a migration); `infra/publish-apk.sh` (assembleRelease via `tools/gradle-serial.sh`, SHA-256 and size, copy to `apk/kvitt-.apk`, atomic `apk/latest.json`, keep newest 5) (§10.3, §10.6, §10.10). +- cert-sync: at start and daily 04:00; SSH with `StrictHostKeyChecking=yes`; `restrict,command="tar -C -cf - fullchain.pem privkey.pem"` where the HA SSH setup allows it, and the plan records which variant the add-on supports; installs only valid, newer, changed PEMs with mode 0640, reloads Caddy through its admin API; failures keep the old certificate (§10.5.1). +- Caddyfile exactly §10.5.2; Caddy must not log `Authorization` (verify); fallback §10.5.3 documented in `infra/README.md`, not deployed (§10.5). +- Backups: daily 03:30; Drive root folder id `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV` as `RCLONE_FOLDER_ID` / `--drive-root-folder-id`; `rclone copy --immutable`, never `sync`, never delete on Drive; local files deleted as soon as both are confirmed on Drive with `rclone lsf` (D-002, §10.9, OQ-4). rclone remote `[gdrive]`, `type = drive`, `scope = drive`, token from `rclone authorize "drive"` (§10.9). +- FCM key on the Pi at `/opt/kvitt/secrets/fcm-service-account.json`, mode 0440, group 10001 (§10.7 step 7). +- Restore steps are §10.9 1 to 7, documented in `infra/RESTORE.md` (§10.9). + +## 4. Owned paths +From SPEC §14.7, verbatim: `infra/**` (owner "M2c-dev, then M2c-pi (D-001)"; after landing: "M10 updates runbooks"). + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +Open points the orchestrator must settle in the start prompt's notes: (a) agent access to the Kvitt Pi over SSH from the Mac (key-based login) and permission to act outside the repository (the Pi, the HA Pi, Gitea, Google Drive) per CLAUDE.md "Stay inside the repository"; (b) where the Gitea Actions workflow file lives (Gitea reads `.gitea/workflows/` at a repository root, outside `infra/**`) and whether the repository is mirrored to Gitea (a push needs the owner's explicit ask); (c) file modes and owners on the Pi for `ha_ssh_key`, `ha_known_hosts` and `rclone.conf`, and the uid the `cert-sync` and `backup` containers run as (only the FCM key's mode is in §10.7). + +## 5. Deliverables +- `infra/compose.yaml`: the §10.2 stack. +- `infra/Caddyfile`: §10.5.2 verbatim. +- `infra/env.example`: updated for the Pi (`KVITT_REGISTRY` on `gitea.lan`, `RCLONE_FOLDER_ID` `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV`; no secrets). +- `infra/cert-sync/Dockerfile`, `infra/cert-sync/sync.sh`: §10.5.1. +- `infra/backup/backup.sh`: D-002 deletion after Drive confirmation (kept local mode from M2c-dev). +- `infra/build-images.sh`, `infra/deploy.sh`, `infra/publish-apk.sh`: §10.3, §10.6, §10.10. +- Gitea Actions workflow from the owner's example (path per open point b). +- `infra/README.md`: Pi provisioning, Docker install, `gitea.lan` registry trust, router forwarding, HA SSH add-on key setup and which `authorized_keys` variant it supports, the §10.5.3 fallback, deploy and publish, operations of §10.10. +- `infra/RESTORE.md`: §10.9 restore steps 1 to 7, with exact commands. +- On the Pi (not in git): `/opt/kvitt/compose.yaml`, `/opt/kvitt/.env`, `/opt/kvitt/secrets/{fcm-service-account.json, ha_ssh_key, ha_known_hosts, rclone.conf}`. +- In `infra/secrets/` on the Mac (git-ignored, main checkout): `ha_ssh_key`, `ha_ssh_key.pub`, `ha_known_hosts`, `rclone.conf`, Gitea credentials if any. + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m2c-pi -b agent-m2c-pi main`, then in `.claude/worktrees/agent-m2c-pi`: `git reset --hard main`. Copy `infra/secrets/` from the main checkout (`mkdir -p infra/secrets && cp -R /Users/jhag/projects/kvitt/infra/secrets/. infra/secrets/`) and `android/local.properties` (needed by `publish-apk.sh` only). Run `tools/install-git-hooks.sh`. +2. Read §2 in order. Confirm OQ-3 is answered in the start prompt (or SPEC §12); without it only steps 3 to 6 can run; stop before step 7 and report. +3. Versions: resolve from official sources and record: `caddy` image version and digest (Docker Hub official image), `alpine` tag and digest, `openssh-client` and `curl` package versions in that Alpine release, Docker Engine install method for Raspberry Pi OS (docs.docker.com, Debian/Raspberry Pi OS page). +4. Write `infra/compose.yaml`, `infra/Caddyfile`, `infra/cert-sync/**`, the D-002 change in `infra/backup/backup.sh`, the three scripts, `infra/README.md`, `infra/RESTORE.md`. Generate the dedicated cert-sync key in the main checkout: `ssh-keygen -t ed25519 -N '' -C kvitt-cert-sync -f /Users/jhag/projects/kvitt/infra/secrets/ha_ssh_key`. +5. Run `docker compose -f infra/compose.yaml config` (with a `.env` made from `infra/env.example` in a temp location, never committed) and `shellcheck infra/**/*.sh` (globstar on). +6. Gitea Actions: write the workflow from the owner's example (H-2); until it exists, build with `infra/build-images.sh` on the Mac (no Gradle build running) and push to `gitea.lan`. +7. Pi provisioning over SSH (needs OQ-3, H-1, H-3, H-4): create `/opt/kvitt/` and `/opt/kvitt/secrets/`; copy the secrets with the modes of open point (c) and §10.7 step 7; install `insecure-registries` for `gitea.lan` per H-4. +8. cert-sync: `ssh-keyscan -p > /Users/jhag/projects/kvitt/infra/secrets/ha_known_hosts`; the owner installs the public key (H-6); test the tar-stream variant first, fall back to plain copy if the add-on refuses `authorized_keys` options, and record the variant. +9. rclone: after H-7, write `/Users/jhag/projects/kvitt/infra/secrets/rclone.conf` with `[gdrive]`, `type = drive`, `scope = drive`, the token; copy to the Pi. +10. Deploy: `infra/deploy.sh `; on the Pi `docker compose ps` shows all four services healthy. +11. Router: the owner sets the forward (H-5); then check `https://icehack.asuscomm.com:8443/health` from the phone on mobile data (H-8). +12. Backup: `docker compose exec backup /backup.sh` on the Pi; `rclone lsf` shows the new files in the Drive folder; local files are gone afterwards (D-002). +13. Verify Caddy logs contain no `Authorization` header (one authenticated request, then grep `docker compose logs caddy`). +14. Run the gate (§7). Commit on `agent-m2c-pi` (never a secret), `git rebase main`, rerun the gate. Never merge, push or remove the worktree. + +## 7. Gate +From SPEC §11.2, verbatim: "`docker compose -f infra/compose.yaml config` succeeds; `shellcheck infra/**/*.sh` clean if shellcheck is installed; on the Pi: cert-sync fetched a valid certificate, `https://icehack.asuscomm.com:8443/health` answers from the phone on mobile data; one backup file appears in the Drive folder". +Green means: `config` exits 0; shellcheck clean (or "not installed" stated); `docker compose exec cert-sync` shows `/certs/fullchain.pem` with a not-after in the future and `openssl s_client -connect icehack.asuscomm.com:8443` (from the Mac) presents that certificate; the phone on mobile data gets `"status":"ok"` (H-8); `rclone lsf` lists the new `kvitt-*.sqlite.gz` and `pictures-*.tar.gz` in folder `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV`. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "Answer OQ-3; provide the example Gitea Actions workflow (and Gitea credentials if the registry needs them); install Docker on the Pi if missing; set the router forward 8443 → Kvitt Pi; add the cert-sync public key to the Home Assistant SSH add-on; `rclone authorize "drive"`". +Each with where it is needed, the exact command, and what blocks it (OQ-1 and OQ-4 are answered; OQ-3 is the only open question): +- H-1, before step 7: answer OQ-3 (Kvitt Pi LAN address and SSH user; HA Pi LAN address, SSH port and user; certificate directory; which SSH add-on). Blocks H-3, H-5, H-6 and steps 7 to 13. +- H-2, step 6: provide the example Gitea Actions workflow, and Gitea credentials if push or pull needs them. Not blocked by an open question; blocks only the Gitea Actions part (manual `build-images.sh` works meanwhile, unless credentials are required). +- H-3, step 7, on the Kvitt Pi (only if `docker version` fails there), using the method from docs.docker.com for Raspberry Pi OS, for example: `curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh && sudo usermod -aG docker `, then log out and in. Blocked by OQ-3. +- H-4, step 7, on the Kvitt Pi (sudo; derived from §10.3, not a separate §11.2 step): add `{"insecure-registries": ["gitea.lan"]}` to `/etc/docker/daemon.json` and run `sudo systemctl restart docker`. Blocked by OQ-3. +- H-5, step 11, in the ASUS router web interface: WAN, Virtual Server / Port Forwarding: add a TCP rule, external port 8443, internal IP = the Kvitt Pi's LAN address, internal port 8443; Apply. Blocked by OQ-3. +- H-6, step 8, in Home Assistant: Settings, Add-ons, , Configuration: add the one line from `infra/secrets/ha_ssh_key.pub` (with the `restrict,command=…` prefix the agent gives) to `authorized_keys`; Save; restart the add-on. Blocked by OQ-3. +- H-7, step 9, on the owner's Mac: `rclone authorize "drive"`, sign in with the owner's Google account in the browser, and leave the terminal output for the agent (the agent reads the token from the terminal session or the owner pastes it into a file the agent names inside `infra/secrets/`). Not blocked (OQ-4 answered). Needs `rclone` installed on the Mac. +- H-8, step 11 (needed by the gate): on the phone with Wi-Fi off, open `https://icehack.asuscomm.com:8443/health` in Chrome; answer yes/no: the page shows `"status":"ok"` without a certificate warning. Blocked by H-5. +- Prerequisite not listed in §11.2 (open point a): key-based SSH login from the Mac to the Kvitt Pi, for example `ssh-copy-id @` on the owner's Mac. Blocked by OQ-3. + +## 9. Out of scope +- The development stack `infra/dev/**` (M2c-dev; do not break it). +- Firebase project and app creation (done in M2c-dev); only copying the key to the Pi is here. +- Release keystore, signed release, publishing the first real APK, pairing both phones, restore drill, memory check (M10). +- Server or Android code changes; the Splitwise import (M11). +- Changing the Home Assistant Pi beyond adding the one `authorized_keys` line; touching certbot there. + +## 10. Report +- Branch `agent-m2c-pi`, head commit hash after rebasing on `main`. +- Gate output lines: `config` exit status, shellcheck result, certificate not-after and issuer, phone health result (H-8), `rclone lsf` lines (file names only). +- Versions chosen (images with digests, packages, Docker install method) with sources. +- The `authorized_keys` variant the HA add-on supports. +- Image tag deployed; `docker compose ps` summary. +- Paths of secrets written (never contents), modes set on the Pi. +- How open points (a) to (c) were resolved. +- Numbering used or `TODO(spec)` occurrences. +- HUMAN steps done and outstanding, worded for relay. +- 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. diff --git a/PLAN-M3.md b/PLAN-M3.md new file mode 100644 index 0000000..a75d9d0 --- /dev/null +++ b/PLAN-M3.md @@ -0,0 +1,93 @@ +# PLAN-M3: Server core API + +## 1. Goal +Implement the REST surface of the server other than sync: invites and pairing, profile and pictures, devices, accounts, categories, recurring templates, the settlement proposal, APK serving, the invite landing page, rate limiting, and the `bootstrap` and `pair-device` subcommands. Every endpoint is covered by integration tests for each error code it can return. + +## 2. Read first +1. SPEC §7 API outline. +2. SPEC §14.3 OpenAPI 3.1 outline: 14.3.1 Common rules, 14.3.2 Error shape and error codes, 14.3.3 Wire objects, 14.3.4 Endpoints (E-2 to E-30, E-33), 14.3.5 Examples as contract. +3. SPEC §4 FR-1 Server bootstrap, FR-2 Invites and pairing, FR-3 Devices and revocation, FR-4 Profiles, FR-5 Household gate, FR-6 Expense accounts, FR-7 Categories, FR-9 Recurring expenses (AC-9.1, AC-9.7, AC-9.8), FR-11 AC-11.8, FR-16 AC-16.1. +4. SPEC §8.2 Device tokens, §8.3 Invite codes, §8.4 Authorization rules, §8.5 Transport (`X-Forwarded-For`), §8.6 Rate limiting and input limits, §8.7 Privacy and logging. +5. SPEC §9.4 Settlement allocation proposal, §9.5 Recurring generation (resume rule, `next_occurrence_date`, due-period generation on create), §9.6 Limits and validation, §9.7 Icon and colour tokens, §9.10 Other edge cases. +6. SPEC §10.4 Server configuration and CLI (`bootstrap`, `pair-device`), §10.6 APK hosting and release (`latest.json`, file name pattern). +7. SPEC §14.1 SQLite schema v1 and server-side rules; §14.5.2 Functions (`propose_allocation`). +8. SPEC §11.2 (row M3), §11.3, §14.7, §14.8. +9. Files: `contracts/openapi.yaml`, `contracts/examples/**`, the M2a code under `server/` (router, error, auth, wire, db), `CLAUDE.md` (BUILD AND VERIFY, ARCHITECTURE RULES, ANTI-PATTERNS). + +## 3. Already established +- Routes, the utoipa `ApiDoc`, error type, auth extractors, wire objects, DB pool and sequence exist from M2a; M3 fills in handler bodies in its own files (§11.3). +- `bootstrap`: no users → cancel any pending `admin_setup`, create a new one, print code, link `/i/`, expiry, exit 0; users exist → `already initialized` on stderr, exit 2 (AC-1.1 to AC-1.3, §10.4). `pair-device --user `: device invite, exit 3 for unknown user (AC-1.6, §10.4). +- Invite codes: 12 Crockford base32 characters, displayed `XXXX-XXXX-XXXX`, normalised (uppercase, strip `-` and spaces, `O`→`0`, `I`/`L`→`1`), stored as SHA-256 hex, single-use, 72 h expiry, cancellable; failures are `invite_invalid` without saying why (§8.3, AC-2.3). +- Tokens: `kvt_` + base64url(32 CSPRNG bytes) without padding, 47 characters, stored as SHA-256 hex; never logged (§8.2). +- Partner invite refused with `household_full` at two users; a new partner invite cancels a pending one (AC-2.1). Accept returns token, user, device, household (AC-2.4, E-3). +- Landing page `GET /i/{code}`: Swedish if `Accept-Language` prefers `sv`, else English; app name, "Download Kvitt" to the latest APK, "Open in Kvitt" to `kvitt://invite?server=&code=`, code in groups of 4, one sentence of instructions; no validity hint, no personal data; any 1 to 32 character code gets 200, else 404 HTML (AC-2.5, E-4). +- Rate limits: unauthenticated 60/min per IP, authenticated 600/min per device, 429 `rate_limited` with `Retry-After`; more than 10 failed accepts per IP in 60 minutes → `rate_limited` for the rest of the window; in memory; `X-Forwarded-For` trusted only from the Caddy peer (`KVITT_TRUSTED_PROXY`) (§8.6, AC-2.12, §8.5, §10.4). +- Pictures: JPEG/PNG/WebP up to 5 MiB and 8000 × 8000, EXIF orientation applied, centre-crop square, JPEG q85 at 512 and 128, metadata stripped, `picture_version` incremented, files `pictures//-512.jpg` and `-128.jpg`, older versions deleted after success; E-11 headers `ETag`, `Cache-Control: private, max-age=31536000, immutable`, `size` 128|512 default 512 (AC-4.4, §10.4, E-9 to E-11). +- Household gate: account create, template create and any expense or settlement mutation → `household_incomplete` below two users (AC-5.2). +- Accounts: names unique among non-deleted, case-insensitive after trim (Unicode lowercase) via `name_key`; delete only when unreferenced (`account_not_empty`), soft delete; ratio change affects only later expenses (FR-6). +- Categories: icon and colour validated against §9.7 lists; new `sort_order` = max + 10; `name: null` resets a default name, `category_is_default` on a non-default; delete blocked by use (`category_in_use`) or default key (`category_is_default`); no uniqueness (FR-7, E-23 to E-27). +- Recurring templates: E-28 generates an already-due period before responding; E-29 `split_mode` and `splits` together; resume does not backfill; `next_occurrence_date` per §9.5 (§14.3.4, §9.5). +- Every REST write that changes a synced row takes a new `server_version` in a `BEGIN IMMEDIATE` transaction and returns the row in wire form (§7, §14.1). +- E-33 uses `kvitt-domain::propose_allocation` over non-deleted rows, `exclude_settlement_id` honoured (AC-11.8, E-33). +- APK: `GET /apk/latest` reads `apk/latest.json` on each request; `GET /apk/files/{file_name}` only for `^kvitt-[0-9]+\.apk$` that exists, `application/vnd.android.package-archive`, `Content-Length` (§10.6, E-5, E-6). +- `last_seen_at`/`app_version_code` update at most hourly (§8.2); devices never deleted (AC-3.5). + +## 4. Owned paths +From SPEC §14.7, verbatim: `kvitt-server`: `src/api/{invites,landing,me,pictures,devices,accounts,categories,recurring,proposal,apk}.rs`, `src/ratelimit.rs`, `src/cli/{bootstrap,pair_device}.rs`, `tests/core_*.rs` (owner M3; after landing: "later server plans"). +Also from §14.7: M3 "may add a dependency line to a crate's `Cargo.toml` (second to land resolves `Cargo.lock` on rebase)" and "may add one line per new module to `mod.rs` files they need". + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +Open points the orchestrator must settle in the start prompt's notes: (a) E-28's due-period generation, E-29's resume rule and `next_occurrence_date` use §9.5 logic, the audit writer and the push queue, which §14.7 places in M4's `src/scheduler/**`, `src/audit/**` and `src/push/**` while M3 and M4 run in parallel: which plan writes the shared generation function and in which order they land; (b) E-2 `GET /setup/status` is listed for M3 in §11.2 but implemented by M2a (`src/api/setup.rs`); M3 only tests it; (c) how the rate-limit layer and body limits attach to M2a's `router.rs`; (d) the landing page text (one sentence of instructions, English and Swedish) is not in §13 or `docs/ux/strings.csv`: who writes the Swedish copy. + +## 5. Deliverables +- `src/api/invites.rs`: E-3, E-12, E-13, E-14. +- `src/api/landing.rs`: E-4 HTML in sv and en. +- `src/api/me.rs`: E-7, E-8. +- `src/api/pictures.rs`: E-9, E-10, E-11. +- `src/api/devices.rs`: E-15, E-16, E-17. +- `src/api/accounts.rs`: E-18 to E-22. +- `src/api/categories.rs`: E-23 to E-27. +- `src/api/recurring.rs`: E-28 to E-30. +- `src/api/proposal.rs`: E-33. +- `src/api/apk.rs`: E-5, E-6. +- `src/ratelimit.rs`: §8.6 limits and AC-2.12. +- `src/cli/bootstrap.rs`, `src/cli/pair_device.rs`: FR-1 subcommands. +- `tests/core_*.rs`: one or more integration test files per endpoint group covering success and every error code listed for each endpoint in §14.3.4, responses compared with `contracts/examples/`. + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m3 -b agent-m3 main`, then in `.claude/worktrees/agent-m3`: `git reset --hard main`. No git-ignored files are needed; copy none. Run `tools/install-git-hooks.sh`. +2. Read §2 in order. Confirm M2a has landed (`server/crates/kvitt-server/src/router.rs` exists on `main`); if not, stop and report. +3. Versions: any crate added (for example image decoding, HTML templating, a rate limiter) at its latest stable version from crates.io / docs.rs; confirm current API usage from official docs; record each. +4. Build the test harness first in `tests/core_common.rs` or equivalent (temp SQLite, injected clock, seeded two-user household helper, request helper that sends `X-Kvitt-Protocol: 1`). +5. Implement in this order, each with its tests before moving on: `bootstrap` and `pair-device`; E-3 accept (all three kinds) and E-12 to E-14; E-7, E-8; E-15 to E-17; E-18 to E-22; E-23 to E-27; E-28 to E-30 (per open point a); E-33; E-9 to E-11; E-5, E-6; E-4; rate limiting (per open point c). +6. For each endpoint, test every error code §14.3.4 lists for it, plus `household_incomplete` where AC-5.2 applies and `forbidden` for member on admin routes. +7. Compare response bodies with `contracts/examples/-/response.json` by JSON value (ids and timestamps normalised as the examples allow). +8. Check logs from one test run contain no token, code, name, description, note or amount (§8.7). +9. Run the gate (§7). Commit on `agent-m3`, `git rebase main` (resolve `Cargo.lock` if M4 landed first), rerun the gate. Never merge, push or remove the worktree. + +## 7. Gate +From SPEC §11.2, verbatim: "Server gate; integration tests per endpoint and per error code listed for it". +Command, from `server/`: `cargo fmt --check && cargo clippy -- -D warnings && cargo test`. +Green means: all three exit 0 in this session; `test result:` lines show 0 failed; the report contains a table endpoint × error code → test name with no empty cell for E-2 to E-30 and E-33. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "none". + +## 9. Out of scope +- E-31, E-32 sync, audit writer and diff, push sender, scheduler, `rotate-epoch`, `sync_mutations` pruning (M4), except what open point (a) assigns. +- Router, error type, auth extractor, wire types, migrations (M2a; contract requests only). +- The importer (M9); `infra/` (M2c-dev, M2c-pi); any Android work. +- Semantic changes to `contracts/**`. + +## 10. Report +- Branch `agent-m3`, head commit hash after rebasing on `main`. +- Gate output: fmt and clippy exit status, every `test result:` line. +- The endpoint × error code → test table. +- Versions of crates added, with sources. +- How open points (a) to (d) were resolved. +- Numbering used (decision ids, spec version) or `TODO(spec)` occurrences. +- Contract requests (anything needed in M2a's files), 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. diff --git a/PLAN-M4.md b/PLAN-M4.md new file mode 100644 index 0000000..588fca4 --- /dev/null +++ b/PLAN-M4.md @@ -0,0 +1,82 @@ +# PLAN-M4: Server sync, audit, push, scheduler + +## 1. Goal +Implement the sync protocol (pull and push), the append-only audit log with its field diff, `sync_mutations` idempotency and pruning, the FCM HTTP v1 sender, the recurring-expense scheduler and `rotate-epoch`. Every rule of §14.4 and every case of §9.5 has an integration test. + +## 2. Read first +1. SPEC §14.4 Sync protocol v1: 14.4.1 Principles, 14.4.2 Pull, 14.4.3 Push, 14.4.4 Server processing of one mutation, 14.4.6 Rejection reasons, 14.4.7 Audit diff, 14.4.8 Epoch recovery and re-pairing, 14.4.9 Limits and timing (14.4.5 for the phone's expectations). +2. SPEC §14.6 Push payload v1. +3. SPEC §9.5 Recurring generation, §9.1 Splits (server check), §9.4 settlement validity, §9.6 Limits and validation, §9.10 Other edge cases. +4. SPEC §4 FR-9 Recurring expenses, FR-12 Audit log and timeline, FR-13 Push notifications, FR-15 Offline and sync. +5. SPEC §14.1 SQLite schema v1 and server-side rules; §14.3.3 Wire objects; §14.3.4 E-31, E-32. +6. SPEC §10.4 (`rotate-epoch`, `KVITT_FCM_CREDENTIALS`, `KVITT_FCM_PROJECT_ID`), §10.9 restore step 5, §6.3 (FCM auth row, Scheduler row), §8.7 Privacy and logging. +7. SPEC §11.2 (row M4), §11.3, §14.7, §14.8. +8. Files: `contracts/examples/sync/*.json`, `contracts/examples/push/*.json`, `contracts/test-vectors/*.json`, the M2a code under `server/`, `CLAUDE.md`. + +## 3. Already established +- Pull: 409 `cursor_ahead` when `since` > current sequence; lowest `server_version > since` across the seven tables via `UNION ALL`, tombstones included, parent with children counts as one row; `limit` default 500, max 1000; `next_since` = highest version in page or `since`; `has_more`; every array present and sorted by `server_version`; `epoch`, `server_time` (§14.4.2, §9.6). +- Push: 1 to 100 mutations (`batch_too_large`), body ≤ 1 MiB; whole request in one `BEGIN IMMEDIATE`, one `SAVEPOINT` per mutation; steps 1 to 12 of §14.4.4 in order (replay, household, shape, clock skew > now + 10 min, load, tombstone, delete always wins, validate, insert, stale, update with diff or no-op, record in `sync_mutations`); pushes enqueued after commit; results in request order (§14.4.3, §14.4.4). +- Server recomputes shares with `kvitt-domain` and rejects mismatches `validation_failed`; mode `default` bp only checked to sum to 10000 (§9.1). +- Audit diff fields, order and value types per §14.4.7; `create` lists every field with `old` null, `update` only differing fields, `delete` only `deleted_at`; `client_at` = pushed `updated_at` (delete: `deleted_at`); scheduler and importer use server now for both; entries never updated or deleted (§14.4.7, AC-12.1). +- `sync_mutations` rows older than 90 days are pruned daily (§14.1, §14.4.9). +- Push: data-only FCM v1 message per target device, `android.priority` high, `ttl` 86400s, all values strings, types exactly the eight of §14.6, recipients per §14.6 (other user's non-revoked devices with a token; recurring to both users); more than 3 notifiable changes in one `POST /sync` → one `sync_batch`; importer sends none; no personal content (§14.6, FR-13). +- Sender: background task, bounded queue of 100 dropping the oldest with a warning; 3 attempts at 1 s, 5 s, 25 s on 429, 5xx, network errors; `UNREGISTERED` or 404 clears the device's `fcm_token`; disabled with a start-up warning when the credentials file is missing; OAuth2 via service-account JWT (RS256) at `https://oauth2.googleapis.com/token`, scope `https://www.googleapis.com/auth/firebase.messaging`, cached until 5 minutes before expiry (§14.6, §6.3). +- Scheduler: tokio task inside `kvitt-server`, runs at start and daily 06:00 Europe/Stockholm (DST-aware); candidate periods, clamping, one transaction per candidate, mode `default` resolves the account's current ratio at generation, audit `create` with `source` `recurring`, `last_generated_period` bump, push `recurring_expense_created`; unique index makes it idempotent; pause/resume and edit rules (§9.5, AC-9.2 to AC-9.6). +- `rotate-epoch` sets `meta.sync_epoch` to a new `lower(hex(randomblob(16)))`, exit 0 (§10.4, §14.1). +- Environment variables only those of §10.4; the FCM endpoints are code constants, overridable only through code (constructor parameters) for the stub-server tests. + +## 4. Owned paths +From SPEC §14.7, verbatim: `kvitt-server`: `src/api/sync.rs`, `src/sync/**`, `src/audit/**`, `src/push/**`, `src/scheduler/**`, `src/cli/rotate_epoch.rs`, `tests/sync_*.rs`, `tests/scheduler_*.rs`, `tests/push_*.rs` (owner M4; after landing: "later server plans"). +Also from §14.7: M4 "may add a dependency line to a crate's `Cargo.toml` (second to land resolves `Cargo.lock` on rebase)" and "may add one line per new module to `mod.rs` files they need". + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +Open points the orchestrator must settle in the start prompt's notes: (a) how `serve` (M2a's `main.rs`/`lib.rs`) starts the scheduler and push sender when M4 may only add `mod.rs` lines; (b) the shared recurring-generation function that M3's E-28/E-29 also need (see PLAN-M3 open point a); (c) the time of the daily `sync_mutations` pruning (the spec says only "daily"). + +## 5. Deliverables +- `src/api/sync.rs`: E-31 and E-32 handlers. +- `src/sync/**`: pull query and paging, push processing of §14.4.4. +- `src/audit/**`: audit writer and diff of §14.4.7, used by sync, scheduler and (later) importer. +- `src/push/**`: queue, FCM v1 sender, OAuth token cache, recipient selection, `sync_batch` aggregation. +- `src/scheduler/**`: recurring generation of §9.5, daily 06:00 Europe/Stockholm tick, `sync_mutations` pruning. +- `src/cli/rotate_epoch.rs`: the subcommand. +- `tests/sync_*.rs`: every rule of §14.4.2 and §14.4.4 steps 1 to 12, each rejection reason of §14.4.6, the §9.10 conflict cases, replay idempotency, batch limits, cursor_ahead, epoch change visible in pull, a 14-day/200-change offline scenario with a scripted clock (server side of AC-15.6). +- `tests/scheduler_*.rs`: every case of §9.5 (catch-up, clamping 29/30/31, February 2027, DST days, pause/resume without backfill, edit affects later only, deleted generated expense not recreated, archived account continues, idempotency, `next_occurrence_date`). +- `tests/push_*.rs`: payloads for every type against `contracts/examples/push/`, recipients, `sync_batch` threshold, retries and delays, token clearing on `UNREGISTERED`/404, queue overflow, disabled without credentials, all against a local stub HTTP server. + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m4 -b agent-m4 main`, then in `.claude/worktrees/agent-m4`: `git reset --hard main`. No git-ignored files are needed (the FCM key is not used; tests use a stub). Run `tools/install-git-hooks.sh`. +2. Read §2 in order. Confirm M2a has landed; if not, stop and report. +3. Versions: crates added (reqwest with rustls, jsonwebtoken, chrono-tz, a stub HTTP server for tests if not axum itself) at latest stable from crates.io / docs.rs; confirm the FCM HTTP v1 request and error format and the OAuth2 service-account flow from Google's official Firebase documentation; record each. +4. Audit writer and diff first, with unit tests for each §14.4.7 rule. +5. Push processing (§14.4.4) step by step, a test per step and per rejection reason; then pull (§14.4.2) with paging tests. +6. Push sender with the stub server tests. +7. Scheduler with an injectable clock; tests for every §9.5 case. +8. `rotate-epoch`; pruning. +9. Wiring into `serve` per open point (a). +10. Run the gate (§7). Commit on `agent-m4`, `git rebase main` (resolve `Cargo.lock` if M3 landed first), rerun the gate. Never merge, push or remove the worktree. + +## 7. Gate +From SPEC §11.2, verbatim: "Server gate; integration tests for every sync rule of §14.4 and every case of §9.5; FCM sender tested against a local stub HTTP server". +Command, from `server/`: `cargo fmt --check && cargo clippy -- -D warnings && cargo test`. +Green means: all three exit 0 in this session; `test result:` lines show 0 failed; the report maps each §14.4.4 step, §14.4.6 reason and §9.5 case to a test name with no gaps. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "none". + +## 9. Out of scope +- REST endpoints E-2 to E-30 and E-33, landing page, pictures, rate limiting, `bootstrap`, `pair-device` (M3). +- The importer (M9); real FCM sends to devices (M8, M10); `infra/`; Android. +- Changing the sync protocol or push payload (contract change). + +## 10. Report +- Branch `agent-m4`, head commit hash after rebasing on `main`. +- Gate output: fmt and clippy exit status, every `test result:` line. +- The rule → test mapping (§14.4.2, §14.4.4 steps, §14.4.6 reasons, §9.5 cases, §14.6 sender rules). +- Versions of crates added, with sources; FCM documentation pages consulted. +- How open points (a) to (c) were resolved. +- Numbering used 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. diff --git a/PLAN-M5.md b/PLAN-M5.md new file mode 100644 index 0000000..a24304d --- /dev/null +++ b/PLAN-M5.md @@ -0,0 +1,87 @@ +# PLAN-M5: Android data layer + +## 1. Goal +Implement `:data`: the Room database v1 with exported schema, DAOs, the Ktor API client and DTOs, the encrypted token store, the sync engine and its WorkManager workers, every real repository behind the `:core:model` interfaces, and the APK updater. The phone then works fully offline and syncs per §14.4, proven by tests with Ktor `MockEngine` and the shared JSON examples. + +## 2. Read first +1. SPEC §14.2 Room entities (14.2.1 Mirrored tables, 14.2.2 Local-only tables, 14.2.3 Migrations, 14.2.4 UI contract). +2. SPEC §14.4 Sync protocol v1, phone side: 14.4.1, 14.4.2 (phone steps), 14.4.3 (request shape), 14.4.5 Phone push procedure, 14.4.6, 14.4.8 Epoch recovery and re-pairing, 14.4.9 Limits and timing. +3. SPEC §14.3 OpenAPI outline (14.3.1 headers, 14.3.2 error codes, 14.3.3 wire objects, 14.3.4 endpoints the app calls, 14.3.5). +4. SPEC §8.2 Device tokens (phone storage), §8.5 Transport, §8.7 Privacy and logging. +5. SPEC §4 FR-2 AC-2.4, AC-2.7 to AC-2.9; FR-3 AC-3.3; FR-4 AC-4.2, AC-4.3, AC-4.6; FR-6 to FR-9 (online-only writes); FR-8 AC-8.6 (defaults), AC-8.10; FR-10 AC-10.4; FR-11 AC-11.3 to AC-11.6; FR-12 AC-12.4, AC-12.5; FR-14 AC-14.1; FR-15 Offline and sync; FR-16 In-app update. +6. SPEC §9.1 to §9.4, §9.6, §9.9 Online-only actions, §9.10. +7. SPEC §6.2 (Local DB, DI, HTTP, Background, Images, Secrets, Tests rows; module table). +8. SPEC §11.2 (row M5), §11.3, §14.7, §14.8. +9. Files: `contracts/examples/**`, `android/core/model/**`, `android/domain/**`, `android/core/fakes/**`, `android/build-logic/**`, `android/gradle/libs.versions.toml`, `CLAUDE.md` (BUILD AND VERIFY, ARCHITECTURE RULES, ANTI-PATTERNS, THE PHONE). + +## 3. Already established +- `KvittDatabase` in package `se.hagstedt.kvitt.data.db`, file `kvitt.db`, version 1, `exportSchema = true` to `android/data/schemas/` (committed; a contract file once landed); no foreign keys; names identical to the server's; camelCase properties with `@ColumnInfo(name = "snake_case")`; server-only columns not mirrored; timestamps `String`, dates `String`, money `Long`, bp `Int`, booleans `Boolean`; `local_updated_at` on every write (§14.2). +- Entity classes, tables, columns, keys and indices exactly as §14.2.1 and §14.2.2; per-phone preferences in DataStore `prefs.preferences_pb` (§14.2.2). +- Local write semantics: `pending = true`, new UUIDv7 `pending_mutation_id`, `updated_at` = now in the fixed form (and `created_at` on create), local delete sets `deleted_at` = `updated_at` = now; pending count = pending expenses + pending settlements (§14.2.1). +- Pull per page in one Room transaction: epoch check, apply arrays in the fixed order, skip rows with local `server_version` ≥ incoming and pending expenses/settlements, upsert and replace children, audit insert-or-ignore, cursor and epoch update; repeat while `has_more`; then `last_success_at` (§14.4.2). +- Push procedure steps 1 to 4 of §14.4.5, including the re-edit-during-request rule, equal-content `rejected` treated as accepted, `clock_skew` handling, 401 handling (`device_revoked` → AC-3.3 wipe; `unauthorized` → `SessionState.Unrecognised`) (§14.4.5, §14.4.8). +- Sync triggers: app start, every foreground, 2 s after the last local write (debounced), every 15 minutes in the background (network constraint), every FCM data message (expedited); one cycle at a time (Mutex; unique work `kvitt-sync`, `KEEP` periodic, `APPEND_OR_REPLACE` one-off); backoff exponential from 30 s (AC-15.3, §14.4.1, §14.4.9). +- Headers on every call: `X-Kvitt-Protocol: 1`, `X-Kvitt-App-Version`, `Accept-Language`, `Authorization: Bearer` on authenticated calls; 426 → stop syncing and surface update (§14.3.1). +- Token: Android Keystore AES-256-GCM key alias `kvitt_token_key`, `setUserAuthenticationRequired(false)`, non-exportable; ciphertext and IV in DataStore `auth.preferences_pb`; base URL, user id, device id in plain text beside it; never logged or put in a URL (§8.2). +- Online-only actions return `OnlineResult.Offline` without a validated network or within 30 s of a network error; a REST network failure changes nothing locally (§9.9). +- Balances, previews, proposals and analytics are computed in `:data` with `:domain` and include pending changes; features never call `:domain` (§6.2, AC-10.4, AC-14.1). +- Editor defaults from DataStore: last used account if active else oldest active; last used category in that account if active else `other` (AC-8.6). +- APK updater: release builds call `GET /apk/latest` on cold start and at most every 6 hours while resumed; download to cache, verify size and SHA-256, install through `PackageInstaller`; failure deletes the file; debug builds never check (FR-16). +- Pictures through Coil with an auth header, cached by user id and `picture_version` (AC-4.6, §6.2). +- Tests use Ktor `MockEngine` and typed fakes; no mocking libraries (§6.2, CLAUDE.md). + +## 4. Owned paths +From SPEC §14.7, verbatim: `android/data/**` (owner M5; after landing: "later data plans"). +Also from §14.7 (row `android/settings.gradle.kts`, `android/build.gradle.kts`, `android/gradle.properties`, `android/gradle/**`, `android/build-logic/**`): "later plans may add catalog entries and module includes (rebase resolves)". + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +## 5. Deliverables +- `android/data/build.gradle.kts`: dependencies `:core:model`, `:domain`, Room, KSP, Ktor, kotlinx.serialization, WorkManager, DataStore, Hilt, Coil. +- `android/data/src/main/kotlin/se/hagstedt/kvitt/data/db/**`: `KvittDatabase`, every entity of §14.2.1 and §14.2.2, DAOs. +- `android/data/schemas/**`: the exported Room schema for version 1. +- `.../data/api/**`: Ktor client with the §14.3.1 headers, DTOs for every wire object and endpoint body, error mapping to §14.3.2 codes. +- `.../data/auth/**`: Keystore token store, session state. +- `.../data/sync/**`: sync engine (push, pull, epoch recovery, rejection records), debounce, WorkManager workers. +- `.../data/repo/**`: real implementations of every repository interface of §14.2.4. +- `.../data/update/**`: APK updater. +- `.../data/connectivity/**`: `ConnectivityRepository` per §9.9. +- `.../data/di/**`: Hilt modules that `:app` (M8) can install. +- `android/data/src/test/**`: DTO round-trip tests over every file in `contracts/examples/`; sync engine tests with `MockEngine` for each §14.4 rule including AC-15.6 (14 days offline, 200 changes, scripted clock); repository tests (balances with pending rows, editor defaults, online-only Offline results). +- `android/data/src/androidTest/**` (if Room needs a device for the schema test; only on the phone the start prompt names). + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m5 -b agent-m5 main`, then in `.claude/worktrees/agent-m5`: `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 M2b has landed (`android/core/model/` on `main`); if not, stop and report. +3. Run `python3 tools/dependency-report/latest.py`; record the lines. Confirm current Room, KSP, Ktor, WorkManager, DataStore and Android Keystore APIs from official documentation (developer.android.com, ktor.io); any new catalog entry at its latest stable version; record versions. +4. Entities and DAOs; export the schema; check it against §14.2.1 column by column. +5. DTOs and the round-trip test over every `contracts/examples/**` file (decode, re-encode, compare JSON values) before writing the client. +6. Ktor client and error mapping; token store. +7. Sync engine test-first with `MockEngine`: each pull step, each push result case, epoch recovery, `Unrecognised` and revoked paths, clock skew, no-progress stop, the AC-15.6 scenario. +8. Repositories, one by one, with tests; workers and triggers; APK updater (verification logic unit-tested; `PackageInstaller` call isolated behind an interface). +9. Run the gate (§7). Commit on `agent-m5`, `git rebase main`, rerun the gate. Never merge, push or remove the worktree. + +## 7. Gate +From SPEC §11.2, verbatim: "Android gate; DTO round-trip tests over every file in `contracts/examples/`; sync engine tests with Ktor `MockEngine` covering §14.4 including the 14-day offline case (AC-15.6)". +Command, from `android/`: `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check`. +Green means: `BUILD SUCCESSFUL` in this session; fresh XML shows the round-trip test covering every example file (count equals the number of files under `contracts/examples/`) and the sync tests including AC-15.6, 0 failures. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "none". + +## 9. Out of scope +- Any UI, ViewModel or string (M7a to M7f; M6 for `:core:design`). +- `:app` wiring, navigation, Hilt component installation, FCM service and token registration, notification building, deep links (M8). +- Changing `:core:model` types or interfaces (contract change via the orchestrator). +- Server code; `infra/`. + +## 10. Report +- Branch `agent-m5`, head commit hash after rebasing on `main`. +- Gate output: BUILD line, actionable tasks line, test counts from fresh XML (round-trip count vs. example file count, sync tests, repository tests). +- Versions added or changed, with sources; dependency-report lines. +- Mapping of §14.4.2 and §14.4.5 rules to test names. +- Numbering used or `TODO(spec)` occurrences. +- Contract requests (any missing field in §14.2.4), 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. diff --git a/PLAN-M6.md b/PLAN-M6.md new file mode 100644 index 0000000..b009fd7 --- /dev/null +++ b/PLAN-M6.md @@ -0,0 +1,83 @@ +# PLAN-M6: Design system + +## 1. Goal +Implement `:core:design` from the approved `docs/ux/design-spec.md`: theme, colour and category tokens, typography, shapes, spacing, shared components, the three analytics charts, and `MoneyFormatter`/`DateFormatter` per §9.8. Every component has screenshot tests in light and dark, Swedish and English, so the feature agents compose screens without inventing visuals. + +## 2. Read first +1. `docs/ux/design-spec.md` (tokens, component inventory; the screen sections only to know which components are needed). +2. `docs/ux/strings.csv` (shared strings) and `docs/ux/screens/` (reference renders). +3. SPEC §6.2 Android app (rows UI, Charts, Tests; `:core:design` row of the module table). +4. SPEC §9.7 Icon and colour tokens (with the hex values as updated by the orchestrator's spec change from U2's proposal), §9.8 Formatting, §9.2 and §9.3 (wording the components render). +5. SPEC §13.1 Context (accessibility, font scale, tabular figures), §13.2 Navigation model. +6. SPEC §4 AC-4.5 (initials avatar), FR-14 AC-14.6, AC-14.10 (charts), FR-19 Localisation and formatting. +7. SPEC §11.2 (row M6), §11.3, §14.7 (including the Strings paragraph). +8. Files: `android/core/model/**`, `android/build-logic/**`, `android/gradle/libs.versions.toml`, `docs/decisions.md`, `CLAUDE.md` (BUILD AND VERIFY: Screenshots; ARCHITECTURE RULES; ANTI-PATTERNS). + +## 3. Already established +- Compose, Material 3, Material 3 Expressive components where stable; follows system light/dark (§6.2, §13.1). +- Charts: Vico; if Vico's current release cannot draw a stacked bar chart with Material 3 theming, hand-roll the three charts with Compose `Canvas` in `:core:design` (§6.2). +- Shared components named in §6.2: money text, avatar, category chip, balance header, charts; plus `MoneyFormatter`, `DateFormatter` (§6.2). The full inventory is the design spec's. +- 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). +- 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` (CLAUDE.md, §14.7 Strings). +- Screenshot goldens are committed; open every changed PNG before committing (CLAUDE.md). +- `:core:design` depends only on `:core:model` (§6.2). + +## 4. Owned paths +From SPEC §14.7, verbatim: `android/core/design/**` (owner M6; after landing: "M7x may not edit; requests go through the orchestrator"). +Also from §14.7: "later plans may add catalog entries and module includes (rebase resolves)" for `android/gradle/**` and `android/settings.gradle.kts`. + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +Open points the orchestrator must settle in the start prompt's notes: (a) shared UI that several features need but §6.2 does not list: the ledger rows used by S-5, S-12 and S-16 (M7b, M7e), and the expense form fields used by S-7 and S-19 (M7c, M7f), which cannot be shared between feature modules; whether M6 provides them; (b) whether the font of the chosen direction is cleared by the owner for bundling (CLAUDE.md "Licences first"); (c) that the §9.7 hex spec change has landed. + +## 5. Deliverables +- `android/core/design/build.gradle.kts`: dependencies (`:core:model`, Compose, Material 3, Vico if used, Coil for avatars). +- `.../design/theme/**`: `KvittTheme`, colour schemes light and dark, category colour tokens mapped from `ColorToken`, typography (with tabular figures for amounts), shapes, spacing scale, elevation, motion tokens from the design spec. +- `.../design/component/**`: every component of the design spec's inventory (at least money text, avatar with initials, category chip and icon, balance header in three states, sync line, update banner, empty and error blocks, "Needs connection" hint, and what open point (a) assigns). +- `.../design/chart/**`: stacked bars over time, category breakdown bars, account breakdown bars. +- `.../design/format/MoneyFormatter.kt`, `DateFormatter.kt`: §9.8, with unit tests for every example in §9.8. +- `android/core/design/src/main/res/values/strings.xml`, `values-sv/strings.xml`: shared strings from `docs/ux/strings.csv`. +- `android/core/design/src/main/res/font/**`: only if the chosen font is cleared (open point b). +- Screenshot tests for every component in light and dark, Swedish and English, plus 200 % font scale for the balance header and money text; committed goldens. +- `docs/decisions.md` entry text for the chart choice (Vico or Canvas) in the report (the file is not owned by M6). + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m6 -b agent-m6 main`, then in `.claude/worktrees/agent-m6`: `git reset --hard main`. Copy `android/local.properties` from the main checkout (add `sdk.dir` if missing; never overwrite or commit). Run `tools/install-git-hooks.sh`. +2. Read §2 in order. Confirm U2's design spec is on `main` and approved (start prompt), M2b has landed, and the §9.7 hex change is in SPEC; otherwise stop and report. +3. Run `python3 tools/dependency-report/latest.py`; record the lines. Versions: Vico latest stable (official Vico documentation and releases); check whether it draws a stacked bar chart with Material 3 theming; record the finding and the decision (Vico or Canvas). Confirm Material 3 Expressive component stability from developer.android.com. Record versions. +4. Formatters test-first from the §9.8 examples (both languages, current-year and other-year dates, relative times). +5. Theme and tokens from the design spec; contrast check of every text/surface pair the spec lists (unit test computing ratios). +6. Components one by one, each with previews (real Swedish and English data from the design spec, no placeholder text) and screenshot tests in light/dark × sv/en. +7. Charts with screenshot tests (empty, one group, many groups, week granularity). +8. Record goldens; open every PNG; post the paths for the owner's look (§8). +9. Run the gate (§7). Commit on `agent-m6`, `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` in this session with the screenshot verification task for `:core:design` run and passing; formatter tests in fresh XML, 0 failures; the report lists one screenshot per component × light/dark × sv/en. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "Look at the component screenshots the agent opens and posts (yes/no)". +Where in the flow: after step 8, before committing the final goldens; the orchestrator shows the owner the PNG paths the agent lists and relays yes/no. A "no" with a comment goes back to the agent before the gate run. + +## 9. Out of scope +- Feature screens and their ViewModels (M7a to M7f); feature strings. +- Navigation, `:app` (M8); `:data` (M5). +- Changing token names, enums or `:core:model` (contract change). +- Changing the design spec (U2's file); disagreements go in the report. + +## 10. Report +- Branch `agent-m6`, head commit hash after rebasing on `main`. +- Gate output: BUILD line, actionable tasks line, screenshot task result, test counts from fresh XML. +- Versions chosen with sources; dependency-report lines; chart decision (Vico or Canvas) with the reason, as decisions text. +- Component list with screenshot paths (for the owner's look) and the owner's yes/no. +- How open points (a) to (c) were resolved. +- Numbering used 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. diff --git a/PLAN-M7a.md b/PLAN-M7a.md new file mode 100644 index 0000000..b25c11f --- /dev/null +++ b/PLAN-M7a.md @@ -0,0 +1,84 @@ +# PLAN-M7a: Feature onboarding + +## 1. Goal +Build `:feature:onboarding`: pairing (S-1), the partner profile step (S-2), waiting for the partner (S-3) and device removed (S-18), against the `:core:model` interfaces and `:core:fakes`. Every state of every screen is screenshot-tested in Swedish and English, light and dark. + +## 2. Read first +1. `docs/ux/design-spec.md`: tokens, component inventory, and the sections for S-1, S-2, S-3, S-18. +2. `docs/ux/strings.csv` (keys for these screens) and `docs/ux/screens/S-1-*`, `S-2-*`, `S-3-*`, `S-18-*`. +3. SPEC §13.2 Navigation model and §13.3 Screens (rows S-1, S-2, S-3, S-18 and the states paragraph). +4. SPEC §14.2.4 UI contract: `SessionState`, `SessionRepository`, `ProfileRepository`, `InviteRepository`, `ConnectivityRepository`, `OnlineResult`, `User`. +5. SPEC §4 FR-2 Invites and pairing (AC-2.6, AC-2.7, AC-2.8, AC-2.9, AC-2.10), FR-3 AC-3.3, FR-4 AC-4.3, AC-4.5, FR-5 AC-5.1; §8.3 Invite codes (display and normalisation); §14.4.8 (the "Pair this phone again" case). +6. SPEC §6.2 (module table), §9.9 Online-only actions, §11.2 (row M7a), §11.3 (note M7x), §14.7. +7. Files: `android/core/model/**`, `android/core/design/**` (read only), `android/core/fakes/**`, `android/build-logic/**`, `CLAUDE.md` (BUILD AND VERIFY: Screenshots; ARCHITECTURE RULES; ANTI-PATTERNS). + +## 3. Already established +- Features depend only on `:core:model`, `:core:design` and `:core:fakes`; `:core:fakes` only in test, screenshot-test and debug-preview configurations, never in a release build. Features never depend on `:data` or `:domain` (§6.2, §11.3, CLAUDE.md Layering). +- Screens consume the `*State` classes through the repository interfaces of §14.2.4; no field may be added except through a contract change (§14.2.4, §14.8). +- Fake scenarios go only in `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes//` (§11.3, §14.7). +- Strings live in the module's `src/main/res/values/strings.xml` and `values-sv/strings.xml` with keys from `docs/ux/strings.csv`; no string outside that file without a decisions entry (§13.4, §14.7, AC-19.1). +- No hard-coded colours, sizes or strings; tokens and shared components come from `:core:design`, which features may not edit (CLAUDE.md, §14.7). Money and dates go through `MoneyFormatter` and `DateFormatter` (§9.8). +- Online-only actions are disabled with "Needs connection" when offline (§9.9); a REST failure shows "Could not reach the server". +- Navigation graph, deep links and Hilt bindings are in `:app` (M8); a feature exposes its screens with navigation callbacks in the form M2b's feature convention fixed (§11.3). +- Layout and visuals follow `docs/ux/design-spec.md`; TalkBack labels as it specifies; 48 dp targets, 200 % font scale reflow (§13.1). +- No placeholder text in previews or screenshots; goldens are committed; open every changed PNG before committing (CLAUDE.md). +- Pairing accepts a server URL (prefilled `https://icehack.asuscomm.com:8443` in release builds) and a 12-character code shown in groups of 4, normalised per §8.3 before sending; release builds accept only `https://`, debug builds also `http://` for `localhost`, `10.0.2.2` and private LAN addresses (AC-2.7, AC-2.8, §8.3). +- Deep links `kvitt://invite?server=…&code=…` and `https://icehack.asuscomm.com:8443/i/` open S-1 with both fields filled; parsing the intent is `:app`'s, the screen takes the values (AC-2.6). +- A `partner` pairing continues to S-2 (display name prefilled, optional picture from the photo picker, "You are joining ") before Home (AC-2.10, AC-4.3). +- S-3: until the partner joins, the admin sees "Waiting for your partner" with "Invite partner" (to S-21) and Settings (AC-5.1, §13.3). +- S-18 after `device_revoked`: explanation that access was removed and local data deleted; "Pair again" goes to S-1 (AC-3.3, §13.3). +- `SessionState.Unrecognised` shows S-1 with "Pair this phone again" (§14.4.8). + +## 4. Owned paths +From SPEC §14.7, verbatim: `android/feature/onboarding/**` (owner M7a), and from the `android/core/fakes/**` row: "M7x add files only under `fakes//`", here `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/onboarding/` (§11.3). Catalog entries and module includes may be added (§14.7, `android/gradle/**` row). + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +## 5. Deliverables +- `android/feature/onboarding/build.gradle.kts`: dependencies `:core:model`, `:core:design`; `:core:fakes` for tests and debug only. +- `android/feature/onboarding/src/main/kotlin/se/hagstedt/kvitt/feature/onboarding/**`: S-1, S-2, S-3, S-18 screens and their state holders. +- `android/feature/onboarding/src/main/res/values/strings.xml`, `values-sv/strings.xml`: keys from `docs/ux/strings.csv`. +- `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/onboarding/**`: a scenario per state below. +- Screenshot tests and committed goldens for every screen × state × light/dark × sv/en. +- Unit tests for state holders (code normalisation display, URL rules, error mapping). + +Screens and states. Required states for every screen below (SPEC §13.3): **loaded**, **empty**, **offline** (with pending changes), **loading** (first sync only), **error** (a failed online action), and for lists **long content** (long Swedish names, 7-digit amounts), each where it applies. A state that `docs/ux/design-spec.md` marks not applicable for a screen is skipped and named in the report. Each state is screenshot-tested in light and dark, Swedish and English. Screen-specific states from §13.3 "Must show" are added per screen: +- **S-1 Pairing**: loaded (empty fields; release prefill), prefilled from a deep link, pairing in progress, error `invite_invalid`, error network ("Could not reach the server"), error `rate_limited`, "Pair this phone again" (Unrecognised), invalid URL (release `http://`), offline, long content n/a unless the design spec says otherwise. +- **S-2 Partner profile step**: loaded with prefilled name and initials avatar, with picture chosen, upload in progress, error (failed upload), offline ("Needs connection" for the picture), long Swedish name. +- **S-3 Waiting for partner**: with a pending invite's expiry, without a pending invite, offline, error. +- **S-18 Device removed**: loaded (single state), plus any state the design spec adds. + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m7a -b agent-m7a main`, then in `.claude/worktrees/agent-m7a`: `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 M6 has landed (`android/core/design/` has components on `main`); if not, stop and report. +3. Run `python3 tools/dependency-report/latest.py`; record the lines. No new library is expected; any one added is at its latest stable version from official docs; record it. +4. Write the fake scenarios under `fakes/onboarding/` for every state listed in §5. +5. Write the state holders and screens, one screen at a time, with strings from `docs/ux/strings.csv` in both languages. +6. Write the screenshot tests; record goldens; open every PNG and compare with `docs/ux/screens/`. +7. Run the gate (§7). Commit on `agent-m7a`, `git rebase main`, rerun the gate. Never merge, push or remove the worktree. + +## 7. Gate +From SPEC §11.2, verbatim: "Android gate with screenshot tests of every state, sv and en, light and dark". +Command, from `android/`: `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check`. +Green means: `BUILD SUCCESSFUL` in this session; screenshot verification passes for `:feature:onboarding`; the report lists, per screen, every state × light/dark × sv/en golden; unit tests 0 failures in fresh XML. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "none". + +## 9. Out of scope +- Other screens: S-4, S-5, S-6, S-16, S-17 (M7b); S-7, S-8 (M7c); S-9, S-10 (M7d); S-11, S-12 (M7e); S-13 to S-15, S-19 to S-22 (M7f). +- Intent parsing, navigation graph, Hilt, the notification-permission prompt after pairing (AC-2.9), the revoke wipe (M8); the real `SessionRepository` (M5). +- Editing `:core:design` (requests go to the orchestrator) or `:core:model`. +- Fake scenarios outside `fakes/onboarding/`. + +## 10. Report +- Branch `agent-m7a`, head commit hash after rebasing on `main`. +- Gate output: BUILD line, actionable tasks line, screenshot task result, test counts from fresh XML. +- Screens × states table with golden file names; states skipped as not applicable. +- Versions added (if any) with sources; dependency-report lines. +- Strings used that are missing from `strings.csv` (should be none) and design-spec mismatches. +- Numbering used or `TODO(spec)` occurrences. +- Contract requests (`:core:design`, `:core:model`), 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. diff --git a/PLAN-M7b.md b/PLAN-M7b.md new file mode 100644 index 0000000..434e7fe --- /dev/null +++ b/PLAN-M7b.md @@ -0,0 +1,88 @@ +# PLAN-M7b: Feature home and account + +## 1. Goal +Build `:feature:home` (Home S-4, sync status S-16, update banner and dialog S-17) and `:feature:account` (account detail S-5, account editor S-6) against the `:core:model` interfaces and `:core:fakes`. Every state of every screen is screenshot-tested in Swedish and English, light and dark. + +## 2. Read first +1. `docs/ux/design-spec.md`: tokens, component inventory, and the sections for S-4, S-5, S-6, S-16, S-17. +2. `docs/ux/strings.csv` (keys for these screens) and `docs/ux/screens/S-4-*`, `docs/ux/screens/S-5-*`, `docs/ux/screens/S-6-*`, `docs/ux/screens/S-16-*`, `docs/ux/screens/S-17-*`. +3. SPEC §13.2 Navigation model and §13.3 Screens (rows S-4, S-5, S-6, S-16, S-17 and the states paragraph). +4. SPEC §14.2.4 UI contract: `HomeRepository`/`HomeState`, `AccountRepository`/`AccountDetailState`, `SyncRepository`/`SyncStatusState`, `UpdateRepository`/`UpdateState`, `ConnectivityRepository`, `BalanceView`, `AccountSummary`, `SyncSummary`, `LedgerRow`, `LedgerMonth`, `ExpenseEffect`, `Rejection`, `OnlineResult`. +5. SPEC §4 FR-5 AC-5.1; FR-6 Expense accounts (AC-6.1 to AC-6.7); FR-8 AC-8.10; FR-10 Balances; FR-11 AC-11.7; FR-15 AC-15.4, AC-15.5; FR-16 AC-16.2 to AC-16.4; §9.2, §9.3, §9.8 (relative time); §14.4.6 Rejection reasons. +6. SPEC §6.2 (module table), §9.8 Formatting, §9.9 Online-only actions, §11.2 (row M7b), §11.3 (note M7x), §14.7. +7. Files: `android/core/model/**`, `android/core/design/**` (read only), `android/core/fakes/**`, `android/build-logic/**`, `CLAUDE.md` (BUILD AND VERIFY: Screenshots; ARCHITECTURE RULES; ANTI-PATTERNS). + +## 3. Already established +- Features depend only on `:core:model`, `:core:design` and `:core:fakes`; `:core:fakes` only in test, screenshot-test and debug-preview configurations, never in a release build. Features never depend on `:data` or `:domain` (§6.2, §11.3, CLAUDE.md Layering). +- Screens consume the `*State` classes through the repository interfaces of §14.2.4; no field may be added except through a contract change (§14.2.4, §14.8). +- Fake scenarios go only in `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes//` (§11.3, §14.7). +- Strings live in the module's `src/main/res/values/strings.xml` and `values-sv/strings.xml` with keys from `docs/ux/strings.csv`; no string outside that file without a decisions entry (§13.4, §14.7, AC-19.1). +- No hard-coded colours, sizes or strings; tokens and shared components come from `:core:design`, which features may not edit (CLAUDE.md, §14.7). Money and dates go through `MoneyFormatter` and `DateFormatter` (§9.8). +- Online-only actions are disabled with "Needs connection" when offline (§9.9); a REST failure shows "Could not reach the server". +- Navigation graph, deep links and Hilt bindings are in `:app` (M8); a feature exposes its screens with navigation callbacks in the form M2b's feature convention fixed (§11.3). +- Layout and visuals follow `docs/ux/design-spec.md`; TalkBack labels as it specifies; 48 dp targets, 200 % font scale reflow (§13.1). +- No placeholder text in previews or screenshots; goldens are committed; open every changed PNG before committing (CLAUDE.md). +- Home shows the global balance from the viewer's side (" owes you X", "You owe X", "All settled up"), every active account with its balance, archived accounts in a collapsed "Archived (n)" section, "Settle up" when non-zero, the sync line with "Last synced …" and pending count, and the update banner (AC-10.1, AC-10.2, AC-15.4, §13.3 S-4). +- The account screen shows its balance ("Settled up" at zero) and expenses and settlements interleaved by date, grouped by month with household-spend month totals; rows per AC-8.10 and AC-11.7 (AC-10.3, §9.3, §13.3 S-5). +- Account editor: ratios entered as percent with up to two decimals, the second user's value always 100 minus the first, stored as bp; starts at 50/50; online only (AC-6.1, AC-6.7). Archive/unarchive any time; delete only when `deletable` (AC-6.4, AC-6.5). +- Sync status lists pending changes, "Changes not synced" from `sync_rejections` with the §14.4.6 explanations, the clock-skew warning, and "Sync now" (AC-15.5, §9.10, §13.3 S-16). +- Update: banner with version name; first time, a dialog explaining "Install unknown apps"; download progress; "Download failed, try again" (AC-16.2 to AC-16.4). + +## 4. Owned paths +From SPEC §14.7, verbatim: `android/feature/home/**`, `android/feature/account/**` (owner M7b), and from the `android/core/fakes/**` row: "M7x add files only under `fakes//`", here `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/home/`, `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/account/` (§11.3). Catalog entries and module includes may be added (§14.7, `android/gradle/**` row). + +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. + +## 5. Deliverables +- `android/feature/home/build.gradle.kts`: dependencies `:core:model`, `:core:design`; `:core:fakes` for tests and debug only. +- `android/feature/home/src/main/kotlin/se/hagstedt/kvitt/feature/home/**`: the screens of this module and their state holders. +- `android/feature/home/src/main/res/values/strings.xml`, `values-sv/strings.xml`: keys from `docs/ux/strings.csv`. +- `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/home/**`: a scenario per state below. +- `android/feature/account/build.gradle.kts`: dependencies `:core:model`, `:core:design`; `:core:fakes` for tests and debug only. +- `android/feature/account/src/main/kotlin/se/hagstedt/kvitt/feature/account/**`: the screens of this module and their state holders. +- `android/feature/account/src/main/res/values/strings.xml`, `values-sv/strings.xml`: keys from `docs/ux/strings.csv`. +- `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/account/**`: a scenario per state below. +- Screenshot tests and committed goldens for every screen × state × light/dark × sv/en. +- Unit tests for the state holders (input handling, enablement rules, error mapping). + +Screens and states. Required states for every screen below (SPEC §13.3): **loaded**, **empty**, **offline** (with pending changes), **loading** (first sync only), **error** (a failed online action), and for lists **long content** (long Swedish names, 7-digit amounts), each where it applies. A state that `docs/ux/design-spec.md` marks not applicable for a screen is skipped and named in the report. Each state is screenshot-tested in light and dark, Swedish and English. Screen-specific states from §13.3 "Must show" are added per screen: +- **S-4**: global balance owes-you, you-owe and all-settled-up; pending count > 0; rejected count > 0; clock skew; sync running; update banner shown; archived section collapsed and expanded; empty "Create your first account"; offline; loading (first sync); long content (long Swedish account names, 7-digit amounts). +- **S-5**: positive, negative and zero balance; archived badge; pending-sync marker on a row; delete allowed and not allowed; empty account; offline; error (failed archive or delete); long content. +- **S-6**: create and edit; preview line ("Of 100,00 kr: …"); non-50/50 ratio with decimals; error `name_taken`; error `validation_failed`; "Needs connection" (offline); saving in progress. +- **S-16**: all synced; pending list; rejections of each reason of §14.4.6; clock-skew warning; sync running; error (last sync failed); offline; long content. +- **S-17**: banner; first-time "Install unknown apps" explanation dialog; downloading with progress; failure "Download failed, try again"; needs install permission; offline ("Needs connection"). + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m7b -b agent-m7b main`, then in `.claude/worktrees/agent-m7b`: `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 M6 has landed (`android/core/design/` has components on `main`); if not, stop and report. If a component the design spec assigns to `:core:design` is missing, file a contract request; do not build it inside the feature. +3. Run `python3 tools/dependency-report/latest.py`; record the lines. No new library is expected; any one added is at its latest stable version from official docs; record it. +4. Write the fake scenarios under the feature's `fakes//` directory for every state listed in §5. +5. Write the state holders and screens, one screen at a time, with strings from `docs/ux/strings.csv` in both languages. +6. Write the screenshot tests; record goldens; open every PNG and compare with `docs/ux/screens/`. +7. Run the gate (§7). Commit on `agent-m7b`, `git rebase main`, rerun the gate. Never merge, push or remove the worktree. + +## 7. Gate +From SPEC §11.2, verbatim: "as M7a", that is "Android gate with screenshot tests of every state, sv and en, light and dark". +Command, from `android/`: `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check`. +Green means: `BUILD SUCCESSFUL` in this session; screenshot verification passes for `:feature:home`, `:feature:account`; the report lists, per screen, every state × light/dark × sv/en golden; unit tests 0 failures in fresh XML. + +## 8. HUMAN steps +From SPEC §11.2, verbatim: "none". + +## 9. Out of scope +- Other screens: S-1, S-2, S-3, S-18 (M7a); S-7, S-8 (M7c); S-9, S-10 (M7d); S-11, S-12 (M7e); S-13, S-14, S-15, S-19, S-20, S-21, S-22 (M7f). +- Navigation graph, deep links, Hilt bindings, notifications (M8); real repositories (M5). +- Editing `:core:design` (requests go to the orchestrator) or `:core:model`; any dependency on `:data` or `:domain`. +- Fake scenarios outside this plan's `fakes//` directories. + +## 10. Report +- Branch `agent-m7b`, head commit hash after rebasing on `main`. +- Gate output: BUILD line, actionable tasks line, screenshot task result, test counts from fresh XML. +- Screens × states table with golden file names; states skipped as not applicable. +- Versions added (if any) with sources; dependency-report lines. +- Strings used that are missing from `strings.csv` (should be none) and design-spec mismatches. +- Numbering used or `TODO(spec)` occurrences. +- Contract requests (`:core:design`, `:core:model`), 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. diff --git a/PLAN-M7c.md b/PLAN-M7c.md new file mode 100644 index 0000000..df53ed4 --- /dev/null +++ b/PLAN-M7c.md @@ -0,0 +1,82 @@ +# PLAN-M7c: Feature expense + +## 1. Goal +Build `:feature:expense`: the full-screen expense editor (S-7) and the expense detail with its timeline (S-8), against the `:core:model` interfaces and `:core:fakes`. Every state of both screens is screenshot-tested in Swedish and English, light and dark. + +## 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). +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. +7. Files: `android/core/model/**`, `android/core/design/**` (read only), `android/core/fakes/**`, `android/build-logic/**`, `CLAUDE.md` (BUILD AND VERIFY: Screenshots; ARCHITECTURE RULES; ANTI-PATTERNS). + +## 3. Already established +- Features depend only on `:core:model`, `:core:design` and `:core:fakes`; `:core:fakes` only in test, screenshot-test and debug-preview configurations, never in a release build. Features never depend on `:data` or `:domain` (§6.2, §11.3, CLAUDE.md Layering). +- Screens consume the `*State` classes through the repository interfaces of §14.2.4; no field may be added except through a contract change (§14.2.4, §14.8). +- Fake scenarios go only in `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes//` (§11.3, §14.7). +- Strings live in the module's `src/main/res/values/strings.xml` and `values-sv/strings.xml` with keys from `docs/ux/strings.csv`; no string outside that file without a decisions entry (§13.4, §14.7, AC-19.1). +- No hard-coded colours, sizes or strings; tokens and shared components come from `:core:design`, which features may not edit (CLAUDE.md, §14.7). Money and dates go through `MoneyFormatter` and `DateFormatter` (§9.8). +- Online-only actions are disabled with "Needs connection" when offline (§9.9); a REST failure shows "Could not reach the server". +- Navigation graph, deep links and Hilt bindings are in `:app` (M8); a feature exposes its screens with navigation callbacks in the form M2b's feature convention fixed (§11.3). +- Layout and visuals follow `docs/ux/design-spec.md`; TalkBack labels as it specifies; 48 dp targets, 200 % font scale reflow (§13.1). +- 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`. +- 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 or `required`; deleting asks for confirmation (AC-8.9). +- Detail shows amount, description, category, account, date, payer, shares and mode, notes, "Recurring: