From 76ee957f4153729dc45da6a25133cf45d281aa94 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Sat, 17 Oct 2026 21:23:38 +0200 Subject: [PATCH] SPEC v0.3: orchestrator resolutions of the plans' open points Records D-004 and D-015 to D-025: shells and ownership, server seams (router middleware, rate_limit layer, tasks::start, AuditWriter and PushQueue traits, next_occurrence_date in kvitt-domain, M3 owns recurring generation, M4 lands after M3), OpenAPI parsed-value equality, feature convention and strings, empty fake scenario and resolveRange exception, shared ledger rows and editor fields in :core:design, no bundled font, pruning at 04:00, development infrastructure as M2c-dev built it, M2c-pi allowances, release keystore, production import procedure. Plans and prompts close their open points; HANDOFF rewritten to the real state; CLAUDE.md: implementation agents never edit SPEC.md or docs/decisions.md. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 6 +- PLAN-M10.md | 8 +- PLAN-M11.md | 23 +++--- PLAN-M2a.md | 49 +++++++----- PLAN-M2b.md | 49 +++++++----- PLAN-M2c-dev.md | 44 ++++++----- PLAN-M2c-pi.md | 50 +++++++----- PLAN-M3.md | 40 ++++++---- PLAN-M4.md | 44 ++++++----- PLAN-M6.md | 29 ++++--- PLAN-M7a.md | 9 ++- PLAN-M7b.md | 10 ++- PLAN-M7c.md | 10 ++- PLAN-M7d.md | 9 ++- PLAN-M7e.md | 10 ++- PLAN-M7f.md | 10 ++- PLAN-M8.md | 22 +++--- PLAN-M9.md | 9 +-- PLAN-U1.md | 14 ++-- PLAN-U2.md | 12 +-- SPEC.md | 186 ++++++++++++++++++++++++++++++--------------- docs/HANDOFF.md | 53 +++++++------ docs/decisions.md | 12 +++ prompts/M1.md | 4 +- prompts/M10.md | 6 +- prompts/M11.md | 6 +- prompts/M2a.md | 6 +- prompts/M2b.md | 6 +- prompts/M2c-dev.md | 8 +- prompts/M2c-pi.md | 6 +- prompts/M3.md | 6 +- prompts/M4.md | 6 +- prompts/M5.md | 4 +- prompts/M6.md | 6 +- prompts/M7a.md | 4 +- prompts/M7b.md | 4 +- prompts/M7c.md | 4 +- prompts/M7d.md | 4 +- prompts/M7e.md | 4 +- prompts/M7f.md | 4 +- prompts/M8.md | 6 +- prompts/M9.md | 4 +- prompts/README.md | 19 ++--- prompts/U1.md | 8 +- prompts/U2.md | 10 +-- 45 files changed, 501 insertions(+), 342 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7941130..95a7f71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ Operating guide for AI agents and humans working on **Kvitt**. This is the only ## SOURCE OF TRUTH * **`SPEC.md` wins.** If a plan, this file or existing code disagrees with the spec, the spec is right. -* A revised decision updates the spec **in the same change**, with an entry in `docs/decisions.md` (numbered D-001…; never deleted, revised in place with a pointer). +* A revised decision updates the spec **in the same landing**, with an entry in `docs/decisions.md` (numbered D-001…; never deleted, revised in place with a pointer). Only a docs agent edits `SPEC.md` and `docs/decisions.md` (see Numbering). * Each `PLAN-*.md` lists the spec sections to read first. Read them before writing anything. * Never encode a threshold, limit or rule that is not in the spec. Ask, or leave a `TODO(spec)`; a wrong number is worse than a missing one. * One handoff: `docs/HANDOFF.md`, rewritten in place. One decisions log. One working-rules file (this one). Delete superseded copies the same day; never keep two sources of truth. @@ -44,7 +44,7 @@ The owner talks to one **orchestrator** session. The orchestrator plans, briefs, * **Briefs are the whole context an agent has.** A brief states: the goal, what to read first (file paths), what is already established, the exact numbers it may use (next free decision, spec version, schema), the setup steps, the gate, what to report. If the orchestrator cannot write a clear brief, it does not understand the task yet. * **Agents' reports are not shown to the owner.** The orchestrator extracts the decision: one line per finding, fixed or not, and relays every owner step in chat. * **Subagents are Opus.** Haiku or Sonnet only for a read-only scan the owner explicitly asks for that way. -* **Numbering** (decisions, spec versions, schema versions, contract requests) is handed out by the orchestrator in the brief. Two parallel agents get disjoint ranges; the second to land renumbers to be contiguous on rebase. +* **Numbering** (decisions, spec versions, schema versions, contract requests) is handed out by the orchestrator in the brief. Two parallel agents get disjoint ranges; the second to land renumbers to be contiguous on rebase. **Implementation agents never edit `SPEC.md` or `docs/decisions.md`:** they put proposed decision entries and spec text, numbered from their range, in their report, and the orchestrator has them recorded by a docs agent, landed together with the implementation. * **Parallel agents** each have their own worktree, build through `tools/gradle-serial.sh` (one Gradle build at a time on this machine), and take turns on the phone: **one agent on the phone at a time**, named in the brief. --- @@ -106,7 +106,7 @@ python3 tools/dependency-report/latest.py # version drift of android/gradle/li * **Version drift:** run the dependency report at the start of every plan. A library more than one stable minor behind gets a line in the plan's report; a toolchain component more than one line behind triggers an upgrade plan first. * **Screenshots.** Goldens are committed; `check` fails when a render differs. Re-record only for an intended change and **open every changed PNG** before committing. Code that compiles is not evidence of a good screen. * **Every schema change gets a migration and a migration test** (sqlx migration on the server, Room migration on the phone). User data stored in the database is not derived data and must survive every migration. -* **Contracts are versioned.** A change to any contract file (`contracts/openapi.yaml`, the SQLite schema, the sync protocol, the Room entities, `contracts/test-vectors/*.json`, the push payload) requires a spec version bump and a `docs/decisions.md` entry in the same change. Number both from the brief. +* **Contracts are versioned.** A change to any contract file (`contracts/openapi.yaml`, the SQLite schema, the sync protocol, the Room entities, `contracts/test-vectors/*.json`, the push payload) requires a spec version bump and a `docs/decisions.md` entry, landed together with it: the implementation agent proposes both in its report (numbered from the brief) and a docs agent records them (Numbering). Representational edits to `contracts/openapi.yaml` (key order, `$ref` naming, description wording) need neither (SPEC §14.8). * **Cargo builds** are not serialised by `gradle-serial.sh`; run at most one `cargo build`/`cargo test` per worktree at a time and never build Docker images for the Pi while a Gradle build runs. --- diff --git a/PLAN-M10.md b/PLAN-M10.md index b8f6ea9..36d06df 100644 --- a/PLAN-M10.md +++ b/PLAN-M10.md @@ -13,7 +13,7 @@ Ship the first signed release to both phones from the production server on the P ## 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). +- Release keystore `kvitt-release.jks`, alias `kvitt`, RSA 4096, validity 10000 days, created once by the owner with the `keytool` command of §10.6 in a directory outside the repository (D-024); `android/local.properties` holds its absolute path and the secrets in `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). @@ -35,7 +35,7 @@ Edit nothing outside these paths; a needed change elsewhere is a contract reques - 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`. +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 outside the repository and is referenced by absolute path from `local.properties` (never copied into a checkout or 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`. @@ -54,7 +54,7 @@ Green means: both files exist with every section filled (no `TODO` other than a ## 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-1, step 3, on the owner's Mac, in a directory outside the repository (D-024): `keytool -genkeypair -v -keystore kvitt-release.jks -alias kvitt -keyalg RSA -keysize 4096 -validity 10000`; choose the passwords, store them in the password manager, and tell the agent the keystore's absolute path 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). @@ -73,7 +73,7 @@ From SPEC §11.2, verbatim: "Create the release keystore; install the APK on bot - 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. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - HUMAN steps done and outstanding, worded for relay. - Contract requests, open questions, spec gaps (with section numbers). diff --git a/PLAN-M11.md b/PLAN-M11.md index 83dd44c..5a78cd5 100644 --- a/PLAN-M11.md +++ b/PLAN-M11.md @@ -6,7 +6,7 @@ Run the Splitwise importer once against the production database with the owner's ## 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. +3. SPEC §10.8 "Production run" (D-025), §10.2 (the `/opt/kvitt/import` mount), §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`. @@ -14,7 +14,7 @@ Run the Splitwise importer once against the production database with the owner's ## 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). +- Command per file: `kvitt-import --db /data/kvitt.sqlite --file --account "" --user "=" --user "=" [--dry-run]`, run on the Pi with the server stopped as `docker compose run --rm --entrypoint kvitt-import server …` (§10.8 "Production run", D-025). 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). @@ -25,7 +25,12 @@ From SPEC §14.7, verbatim: `docs/validation/` | "the milestone that names 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). +Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries in the report. + +Resolved open points (SPEC v0.3, D-025): +- (a) The CSVs are copied from the Mac to the Pi into `/opt/kvitt/import/`, bind-mounted read-only into `server` as `/import`; the import runs on the Pi as `docker compose run --rm --entrypoint kvitt-import server --db /data/kvitt.sqlite --file /import/ …` (`--entrypoint` because the image's entrypoint is `kvitt-server`); the CSVs are deleted from the Pi afterwards. +- (b) A backup is taken first (`docker compose exec backup /backup.sh`, confirmed on Drive). +- (c) The production user ids are resolved by display name from the database (read-only `SELECT id, role, display_name FROM users` through the `backup` container's `sqlite3`); the owner is the `admin` user. ## 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. @@ -34,10 +39,10 @@ Open points the orchestrator must settle in the start prompt's notes: (a) how th ## 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. +3. Backup (resolved point b), resolve the user ids (resolved point c), copy the CSVs to `/opt/kvitt/import/`, stop `server`, and dry-run each file with the command of resolved point (a) and `--dry-run`; start `server` again; 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. If anything was written since the backup of step 3, take a new one (`docker compose exec backup /backup.sh`, confirmed on Drive). +5. `docker compose stop server`; run the import for "Hemma" then "Vardag" with the command of resolved point (a); `docker compose start server`; check `/health`. Delete the CSVs from `/opt/kvitt/import/`. +6. Re-run both imports with `--dry-run` (copy the CSVs again, delete them afterwards): 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. @@ -63,8 +68,8 @@ From SPEC §11.2, verbatim: "Provide the CSVs; confirm the balances on the phone - 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. +- How resolved points (a) to (c) were applied. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - HUMAN steps done and outstanding, worded for relay. - Contract requests, open questions, spec gaps (with section numbers). diff --git a/PLAN-M2a.md b/PLAN-M2a.md index 196b772..00ffc99 100644 --- a/PLAN-M2a.md +++ b/PLAN-M2a.md @@ -14,7 +14,8 @@ Create the Rust Cargo workspace with its three crates, a running `kvitt-server` 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). +11. SPEC §6.3 "Server seams (D-015)", §9.5 Recurring generation (`next_occurrence_date`, clamping), §14.5.2 row "Recurring dates"; `docs/decisions.md` D-004, D-015, D-016. +12. 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. @@ -31,24 +32,33 @@ Create the Rust Cargo workspace with its three crates, a running `kvitt-server` - 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). +- `contracts/openapi.yaml` may be changed by M2a only for representational differences that do not change meaning (key order, `$ref` naming, description wording), listed in the report, with no spec bump; anything semantic is a contract change (§11.3, §14.8, D-016). ## 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`. +From SPEC §14.7 (v0.3): +- `server/Cargo.toml`, `server/Cargo.lock`, every crate's `Cargo.toml`, `server/rust-toolchain.toml`, `server/Dockerfile`, `server/crates/kvitt-domain/**` (after landing: contract-change plans only for `kvitt-domain`; M3, M4 and M9 may add dependency lines to a crate's `Cargo.toml`). +- `kvitt-server`: `migrations/`, `src/{main,lib,config,error,router,openapi,auth,time}.rs` (including `serve` and `migrate`), `src/db/**`, `src/wire/**`, `src/api/{mod,health,setup}.rs`, `src/cli/{mod,healthcheck}.rs`, and M2a's own tests (any `tests/*.rs` without the prefixes `core_`, `sync_`, `scheduler_`, `push_`). +- Shell files in later milestones' paths (D-004), which pass to their owners as soon as M2a lands: the 501 handlers in `src/api/{invites,landing,me,pictures,devices,accounts,categories,recurring,proposal,apk,rate_limit,sync}.rs`, stub subcommands in `src/cli/{bootstrap,pair_device,rotate_epoch}.rs`, `src/tasks/mod.rs` (a `start` function that does nothing yet), `src/audit/` and `src/push/` (the `AuditWriter` and `PushQueue` traits with no-op implementations, D-015), and `server/crates/kvitt-import/` (a crate that builds, with a `main.rs` that prints usage and exits non-zero). +- Exception granted by §11.3 and §14.8: representational (non-semantic) edits to `contracts/openapi.yaml` (D-016). -Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries in the report (CLAUDE.md › Orchestration). -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. +Resolved open points (SPEC v0.3; the start prompt may repeat them): +- (a) Stubs: M2a creates the shell files listed above (D-004); M3, M4 and M9 own their bodies. +- (b) `server/Cargo.lock` and every `Cargo.toml` are M2a's; later milestones add dependency lines (§14.7). +- (c) M2a's own integration tests use any name without the prefixes `core_`, `sync_`, `scheduler_`, `push_`; list the names in the report. +- (d) The protocol-header check (426), `X-Kvitt-Server-Version` and the §8.6 body limits are layers in `router.rs`. M3's rate limit is a layer in `src/api/rate_limit.rs` attached at one line in `router.rs` that M2a marks with a comment (attach a pass-through layer from the shell until M3 fills it) (D-015). +- (e) `serve` calls `tasks::start` from `src/tasks/mod.rs` once after migrations; M4 owns `src/tasks/**`. `AuditWriter` and `PushQueue` are traits with no-op implementations passed through the application state; M4 replaces the implementations, M3 calls the traits (D-015). +- (f) The OpenAPI equality test compares parsed values (YAML parsed and compared as JSON values), not text (D-016). +- Also (D-015): implement `next_occurrence_date` and the day clamping of §9.5 in `kvitt-domain` (`src/recurring.rs`, pure, Rust only, unit tests for every AC-9.2 clamping case and the `null` cases); report the exact signatures. +- Also (§10.3): `rust-toolchain.toml` pins the exact current stable Rust release, and the Dockerfile's stage 1 uses `rust:-bookworm`. ## 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/rust-toolchain.toml`: the exact current stable Rust release (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/Cargo.toml`, `src/lib.rs`, `src/split.rs`, `src/balance.rs`, `src/allocation.rs`, `src/settlement.rs`, `src/recurring.rs`: types and functions of §14.5.1 and §14.5.2, including the Rust-only recurring dates. - `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`). @@ -56,19 +66,20 @@ Open points the orchestrator must settle in the start prompt's notes (stop and a - `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). +- `src/tasks/mod.rs` with `start`; `src/audit/` and `src/push/` with the `AuditWriter` and `PushQueue` traits and no-op implementations (D-015). +- 501 stubs for the remaining routes and stub subcommands in the shell files of §4 (D-004). +- Integration tests in `kvitt-server/tests/` (names per resolved 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/`: a crate that builds and depends on `kvitt-domain` and `kvitt-server`, with a usage-only `main.rs` (D-004). ## 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. +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 that exact version (for example `1.NN.P`, not `stable`) in `rust-toolchain.toml` and the same version in the Dockerfile's `rust:-bookworm`. 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`. +6. Write `auth.rs` and `router.rs` with every route of §7, the §14.3.1 headers and §8.6 body limits as layers, the marked rate-limit line, and stub handlers in the shell files (resolved points a, d). +7. Write `openapi.rs` with utoipa annotations on every handler and wire type; make the generated document equal `contracts/openapi.yaml` (parsed values, resolved 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; calls `tasks::start`), `migrate`, `healthcheck`; write `kvitt-domain`'s recurring dates test-first. 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. @@ -95,8 +106,8 @@ From SPEC §11.2, verbatim: "none". - 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. +- How each resolved point (a) to (f) was applied; the exact `next_occurrence_date` and clamping signatures; the marked rate-limit line. +- Proposed decision entries and spec text (numbered from the start prompt's range), never written to `SPEC.md` or `docs/decisions.md` by this agent; or `TODO(spec)` occurrences. - Contract requests, open questions, spec gaps (with section numbers). - Docker image build result (size) if built. diff --git a/PLAN-M2b.md b/PLAN-M2b.md index 27862e8..e4d1a3a 100644 --- a/PLAN-M2b.md +++ b/PLAN-M2b.md @@ -11,11 +11,12 @@ Create the Gradle project in `android/` with every module of §6.2, the version 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). +8. SPEC §6.2 "Feature convention (D-017)" and "Fakes" paragraphs, §14.5.4 (analytics package, `resolveRange` exception), §14.7 "Module shells" and "Strings" paragraphs; `docs/decisions.md` D-004, D-017, D-018, D-020. +9. 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). +- Screenshot tests use Roborazzi or AGP Compose Preview Screenshot Testing, whichever is stable at implementation time; M2b chooses, wires it into `check` and reports the choice with its reasons; the orchestrator records it as D-010 (§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). @@ -24,19 +25,29 @@ Create the Gradle project in `android/` with every module of §6.2, the version - 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). +- 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 (`CUSTOM` with a null date throws a typed `IllegalArgumentException` subclass declared in `:domain`, named by M2b, D-018). - `: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). +From SPEC §14.7 (v0.3): +- `android/settings.gradle.kts`, `android/build.gradle.kts`, `android/gradle.properties`, `android/gradle/**`, `android/build-logic/**` (after landing: "later plans may add catalog entries and module includes (rebase resolves)"). +- `android/core/model/**`, `android/domain/**` (after landing: "contract-change plans only"). +- `android/core/fakes/**` (after landing: "M7x add files only under `fakes//`"). +- `android/app/**` up to and including "the M2b shell" (afterwards M8). +- Module shells (D-004), which pass to their owners as soon as M2b lands: build file, manifest and empty string files of `:core:design` (M6), `:data` (M5), `:adapter:fcm` (M8), and of every `:feature:*` (M7a to M7f) with one empty entry composable per feature. -Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries in the report (CLAUDE.md › Orchestration). -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`). +Resolved open points (SPEC v0.3; the start prompt may repeat them): +- (a) M2b creates the module shells above, including one empty entry composable per feature module (D-004). +- (b) The empty themed screen uses `MaterialTheme` with Material 3 defaults inside `:app`'s shell; `KvittTheme` in `:core:design` is M6's. +- (c) Strings: `:app` holds only the app name (`values/strings.xml` and `values-sv/strings.xml`); `:core:design` will hold shared strings, each feature its own; lint `MissingTranslation` is an error in every module, set once in the convention plugins (D-017). +- (d) Screenshot tool: M2b chooses Roborazzi or AGP Compose Preview Screenshot Testing, wires it into `check`, and reports the choice with reasons; the orchestrator records it as D-010. +- (e) Feature convention (D-017, §6.2): one composable entry point per screen taking a `hiltViewModel()`-provided ViewModel and navigation as plain lambdas; features never reference other features or the `NavHost`; `:app` owns the `NavHost`; repositories are injected through the `:core:model` interfaces, bound in `:app` to `:data` (fakes only in test and debug-preview source sets). The feature convention plugin applies Hilt and the Hilt navigation Compose dependency. +- (f) The §14.5.4 analytics types live in `:core:model`, package `se.hagstedt.kvitt.model.analytics`; the functions in `:domain` (D-017). +- (g) Empty scenario (D-018): household complete (two named users, no pictures), no accounts, exactly the ten default categories of AC-7.2, no expenses, settlements, templates, rejections or pending changes. +- Also (D-018): `resolveRange` with `CUSTOM` and a null start or end throws a typed subclass of `IllegalArgumentException` declared in `:domain`; callers map it to `validation_failed`. Name the class in the report. +- Also (D-020): no font is bundled; the app uses the Material 3 default typeface. ## 5. Deliverables - `android/settings.gradle.kts`, `android/build.gradle.kts`: the project with every module of §6.2. @@ -46,17 +57,17 @@ Open points the orchestrator must settle in the start prompt's notes (stop and a - `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/core/fakes/**`: one fake per repository interface of §14.2.4 driven by a scenario type, with the empty scenario of resolved point (g) 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). +- Module shells for `:core:design`, `:data`, `:adapter:fcm`, `:feature:{onboarding,home,account,expense,settle,analytics,settings}`, one empty entry composable per feature (D-004). +- `android/app/src/main/res/values/strings.xml` and `values-sv/strings.xml` with the app name only, and lint `MissingTranslation` as an error in every module, in `check` (D-017). +- Screenshot-test tooling wired into `check` (resolved point d) with one screenshot of the empty screen in light and dark. +- The proposed D-010 text for the screenshot tool, in the report (the orchestrator has it recorded). ## 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). +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 resolved 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. @@ -87,10 +98,10 @@ Where in the flow: after the gate is green (step 10). The orchestrator relays th - 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. +- Screenshot tool chosen and why, as the proposed D-010 text. +- How each resolved point (a) to (g) was applied; the name of the `resolveRange` exception class. - `installDebug` result on the phone, or "outstanding". -- Numbering used (decision ids, spec version) or `TODO(spec)` occurrences. +- Proposed decision entries and spec text, never written to `SPEC.md` or `docs/decisions.md` by this agent; or `TODO(spec)` occurrences. - Contract requests, open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-M2c-dev.md b/PLAN-M2c-dev.md index e364a29..87665a1 100644 --- a/PLAN-M2c-dev.md +++ b/PLAN-M2c-dev.md @@ -1,4 +1,6 @@ -# PLAN-M2c-dev: Infra for development (D-001) +# PLAN-M2c-dev: Infra for development (D-001, D-022) + +On record: landed on `main` as `c885aba`. As built (SPEC v0.3 §10 intro, §10.7, D-022): no Caddy, no TLS; bind-mounted data in `infra/dev/data/`; `infra/secrets/` mounted read-only at `/secrets`; env file `infra/dev/.env.example`; dev scripts `infra/dev/{build,up,logs,backup,copy-google-services}.sh`; runbook in `infra/README.md` (not `infra/dev/README.md`); `infra/env.example` is left to M2c-pi. Firebase resources exist. Where this plan and SPEC differ, SPEC wins. ## 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. @@ -8,7 +10,7 @@ Give development a local server on the owner's M4 Mac: a Docker compose stack wi 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. +5. `docs/decisions.md` D-001, D-002, D-022. 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 @@ -17,14 +19,14 @@ Give development a local server on the owner's M4 Mac: a Docker compose stack wi - `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). +- Firebase (D-022): no new CLI installs; `gcloud` exists, the Firebase CLI is not used; the Firebase project and the Android apps are created through the Firebase Management REST API with `gcloud auth print-access-token`; `rclone` belongs to M2c-pi. Project `kvitt-1337`; Android apps `se.hagstedt.kvitt` ("Kvitt") and `se.hagstedt.kvitt.debug` ("Kvitt debug"); one merged `google-services.json` in `infra/secrets/` (git-ignored), which M8 copies into `android/app/` with `infra/dev/copy-google-services.sh`; 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. +Also (D-022): the git-ignored contents of `infra/secrets/` (`fcm-service-account.json`, `google-services.json`) written in the main checkout `/Users/jhag/projects/kvitt` (where every worktree copies them from, CLAUDE.md), and actions on the Google Cloud project `kvitt-1337` for §10.7 steps 2 to 6. Nothing else outside the repository; `android/app/` is not touched (M8 runs the copy script). Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. @@ -33,34 +35,36 @@ Edit nothing outside these paths; a needed change elsewhere is a contract reques - `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). +- `infra/dev/.env.example`: the variables `infra/dev/compose.yaml` reads, with dev-friendly values (no secrets); the gate runs `config` with it. +- `infra/env.example`: the `.env` keys of §10.2 with comments (no secret values). +- `infra/dev/copy-google-services.sh`: copies `infra/secrets/google-services.json` to `android/app/google-services.json` of the checkout it runs in (exits non-zero with a message when the source is missing); shellcheck clean. - 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). +- `infra/secrets/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). +2. Read §2 in order. Check the tools: `command -v docker gcloud shellcheck curl`, `docker buildx version`, `docker compose version`. No new CLI is installed (D-022). If `docker` or `gcloud` 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). +6. Run `docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config`. +7. Firebase (needs HUMAN step H-1, §8). After the owner signed in, run SPEC §10.7 steps 2 to 6 (v0.3): `gcloud config set project kvitt-1337`; `gcloud services enable fcm.googleapis.com firebase.googleapis.com`; then with `TOKEN=$(gcloud auth print-access-token)` call the Firebase Management REST API (`https://firebase.googleapis.com/v1beta1/`; confirm the requests, the long-running operation polling and any quota-project header from Google's official API reference): `projects.addFirebase` for `projects/kvitt-1337` (skip if already a Firebase project), `projects.androidApps.create` for "Kvitt" / `se.hagstedt.kvitt` and "Kvitt debug" / `se.hagstedt.kvitt.debug` (skip an app that already exists), `projects.androidApps.getConfig` for each, merged into one `/Users/jhag/projects/kvitt/infra/secrets/google-services.json` holding both clients. Never print the config or the token. Then `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 the owner's phone check (§8 H-2), relayed by the orchestrator. 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. +10. Run `shellcheck $(find infra -name '*.sh')`. 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`. +From SPEC §11.2 (v0.3), verbatim: "`docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config` succeeds; `shellcheck $(find infra -name '*.sh')` clean; on the Mac (with the M2a image): `GET /health` answers 200 from the Mac; one local backup run produces a snapshot that passes `PRAGMA integrity_check`". +Commands: `docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config`; `shellcheck $(find infra -name '*.sh')`; `curl -fsS http://localhost:/health`; `sqlite3 "PRAGMA integrity_check"`. +Green means: `config` exits 0; shellcheck prints nothing and exits 0 (if shellcheck is not installed, stop and ask; do not install it); `/health` returns 200 with `"status":"ok"` from the Mac; integrity check prints `ok`. The phone check (H-2) is a HUMAN step, reported done or outstanding, not part of the gate. ## 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. +From SPEC §11.2 (v0.3), verbatim: "`gcloud auth login`; on the phone on the home Wi-Fi, open the dev server's `/health` and answer yes/no that it shows `"status":"ok"`". No open question blocks this plan (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). Expected result: the command ends with a "You are now logged in as" line. +- H-2, after step 8: on the owner's phone on the home Wi-Fi, open `http://:/health` in Chrome and answer yes/no: the page shows `"status":"ok"`. If macOS asks whether Docker may accept incoming connections, the owner clicks Allow. +- H-3 (only if step 2 finds a tool missing; not a §11.2 step): the owner decides whether and how to install it. ## 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. @@ -72,9 +76,9 @@ From SPEC §11.2, verbatim: "`gcloud auth login`, `firebase login`". No open que - 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). +- Where the secrets were written (paths only, never contents); the REST calls made (method and resource name only). - Whether the dev stack is left running, and its URL. -- Numbering used or `TODO(spec)` occurrences. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - HUMAN steps done and outstanding, worded for relay. - Contract requests, open questions, spec gaps (with section numbers). diff --git a/PLAN-M2c-pi.md b/PLAN-M2c-pi.md index 641ca1a..adc0425 100644 --- a/PLAN-M2c-pi.md +++ b/PLAN-M2c-pi.md @@ -10,12 +10,12 @@ Deploy Kvitt's production stack on the Raspberry Pi 3: Caddy with the Let's Encr 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). +5. `docs/decisions.md` D-001, D-002, D-022, D-023, D-025. +6. Files: everything under `infra/` from M2c-dev (`infra/dev/**`, `infra/backup/**`, `infra/README.md`, `infra/.shellcheckrc`; `infra/env.example` does not exist yet and is this plan's), `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). +- Registry: Gitea at `http://gitea.lan`, plain HTTP, home Wi-Fi only; the Pi's Docker daemon trusts it via `insecure-registries` (its own HUMAN step, D-023); 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). @@ -24,26 +24,33 @@ Deploy Kvitt's production stack on the Raspberry Pi 3: Caddy with the Let's Encr - 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). +- The `cert-sync` and `backup` containers run as uid 1000; `ha_ssh_key`, `ha_known_hosts` and `rclone.conf` on the Pi have mode 0600, owned by uid 1000 (§10.2, D-023). +- The `server` service mounts `/opt/kvitt/import:/import:ro` for the one-off Splitwise import (§10.2, §10.8, D-025); the directory exists and is empty outside the import. +- `rclone` is needed on the Mac for H-7 only; installing it is the owner's decision (D-022 leaves it to this plan). - 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"). +From SPEC §14.7 (v0.3): `infra/**` (owner "M2c-dev, then M2c-pi (D-001)"; after landing: "M10 updates runbooks"), `.gitea/workflows/**` (D-023), and the git-ignored `infra/secrets/**` in the main checkout. -Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. +Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries in the report. -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). +Resolved open points (SPEC v0.3, D-023): +- (a) Key-based SSH from the Mac to the Kvitt Pi is a HUMAN step (H-0). The allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive are requested from the owner by the orchestrator at the start of this sprint and stated in the start prompt; without them, stop before step 7. +- (b) The Gitea Actions workflow lives at `.gitea/workflows/` at the repository root, owned by this plan. Mirroring or pushing the repository to Gitea needs the owner's explicit ask; without it, write the workflow and report it as not yet run. +- (c) `ha_ssh_key`, `ha_known_hosts`, `rclone.conf` on the Pi: mode 0600, owned by uid 1000, the uid the `cert-sync` and `backup` containers run as. Verify that `backup` can read the `data` volume written by uid 10001 and record how in `infra/README.md`. +- Gitea credentials are a HUMAN step (H-2). ## 5. Deliverables -- `infra/compose.yaml`: the §10.2 stack. +- `infra/compose.yaml`: the §10.2 stack, including the `/opt/kvitt/import:/import:ro` mount on `server` and uid 1000 for `cert-sync` and `backup`. - `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/env.example`: written 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. +- `.gitea/workflows/.yaml`: the image build from the owner's example (resolved point b). +- `infra/README.md` (extend M2c-dev's file; keep its dev and Firebase sections): 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}`. +- 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}`, the empty directory `/opt/kvitt/import/`. - 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 @@ -51,9 +58,9 @@ Open points the orchestrator must settle in the start prompt's notes: (a) agent 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). +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 $(find infra -name '*.sh')`. 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. +7. Pi provisioning over SSH (needs OQ-3, the allowances of resolved point a, H-0, H-1, H-3, H-4): create `/opt/kvitt/`, `/opt/kvitt/secrets/` and `/opt/kvitt/import/`; copy the secrets with the modes of resolved point (c) and §10.7 step 7; the owner adds `insecure-registries` for `gitea.lan` (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. @@ -63,21 +70,22 @@ Open points the orchestrator must settle in the start prompt's notes: (a) agent 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`. +From SPEC §11.2 (v0.3), verbatim: "`docker compose -f infra/compose.yaml config` succeeds; `shellcheck $(find infra -name '*.sh')` clean; 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; `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"`". +From SPEC §11.2 (v0.3), verbatim: "Answer OQ-3; grant at the start of the sprint the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive (D-023); set up key-based SSH from the Mac to the Kvitt Pi; provide the example Gitea Actions workflow and the Gitea credentials; install Docker on the Pi if missing; add `gitea.lan` to `insecure-registries` on the Pi; 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-A, at the start of the sprint (through the orchestrator): grant the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive. Blocks steps 7 to 13. +- H-0, before step 7, on the owner's Mac: key-based SSH login to the Kvitt Pi, for example `ssh-copy-id @`, then `ssh @ true` succeeds without a password prompt. Blocked by OQ-3. - 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-2, step 6: provide the example Gitea Actions workflow and the Gitea credentials (written by the owner into a file the agent names inside `infra/secrets/`, never pasted into chat). Not blocked by an open question; blocks the Gitea Actions part and any push to `gitea.lan` that needs credentials. - 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-4, step 7, on the Kvitt Pi (sudo; its own HUMAN step, §10.3, D-023): 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). @@ -93,8 +101,8 @@ Each with where it is needed, the exact command, and what blocks it (OQ-1 and OQ - 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. +- How resolved points (a) to (c) were applied; which allowances the start prompt granted. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - HUMAN steps done and outstanding, worded for relay. - Contract requests, open questions, spec gaps (with section numbers). diff --git a/PLAN-M3.md b/PLAN-M3.md index a75d9d0..8a64ce9 100644 --- a/PLAN-M3.md +++ b/PLAN-M3.md @@ -11,34 +11,42 @@ Implement the REST surface of the server other than sync: invites and pairing, p 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. +8. SPEC §11.2 (row M3), §11.3, §14.7, §14.8; §6.3 "Server seams (D-015)"; §13.3 "Invite landing page" copy; `docs/decisions.md` D-004, D-015. 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). +- Routes, the utoipa `ApiDoc`, error type, auth extractors, wire objects, DB pool and sequence exist from M2a; M2a created M3's files as shells with 501 handlers and stub subcommands, and M3 fills in the bodies (§11.3, D-004). +- Seams (D-015): the protocol check, `X-Kvitt-Server-Version` and the §8.6 body limits are M2a's layers in `router.rs`; M3's rate limit is a layer in `src/api/rate_limit.rs` attached at the one line in `router.rs` that M2a marked (the only line M3 edits there). Audit entries and pushes go through the `AuditWriter` and `PushQueue` traits M2a defined (no-op until M4 replaces the implementations); M3 never writes `audit_log` rows or FCM requests itself. `kvitt-domain` has `next_occurrence_date` and the day clamping (§14.5.2). - `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). +- 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, all copy exactly as the §13.3 "Invite landing page" table (en "Install Kvitt, then open this link again on your phone.", sv "Installera Kvitt och öppna sedan den här länken igen på din telefon."); 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). +- 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 via `kvitt-domain` (§14.3.4, §9.5). +- Recurring generation (D-015): M3 writes the one public function that generates the due periods of one template per §9.5 (candidate periods, one transaction per candidate, split resolution, audit `create` with `source` `recurring` through `AuditWriter`, `last_generated_period` bump, `recurring_expense_created` through `PushQueue`) in `src/api/recurring.rs`; E-28 and E-29 call it, and M4's scheduler calls it for every template. M4 lands after M3. - 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". +From SPEC §14.7 (v0.3): `kvitt-server`: `src/api/{invites,landing,me,pictures,devices,accounts,categories,recurring,proposal,apk,rate_limit}.rs`, `src/cli/{bootstrap,pair_device}.rs`, `tests/core_*.rs` (owner M3, shells from M2a; after landing: "later server plans"). +Also from §14.7: M3 may add dependency lines to a crate's `Cargo.toml` (second to land resolves `Cargo.lock` on rebase), may add one line per new module to `mod.rs` files it needs, and edits the one marked rate-limit line in `router.rs`. 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. +Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries in the report. + +Resolved open points (SPEC v0.3; the start prompt may repeat them): +- (a) M3 writes the recurring generation function (above) and calls the `AuditWriter`/`PushQueue` traits; M2a provides `next_occurrence_date` and the clamping in `kvitt-domain`; M4 replaces the trait implementations and its scheduler calls M3's function; M4 lands after M3 (D-015). +- (b) E-2 is implemented by M2a (`src/api/setup.rs`); M3 only tests it. +- (c) The rate limit is a layer in `src/api/rate_limit.rs`, attached at the marked line in `router.rs`; the body limits are M2a's layers (D-015). +- (d) The landing page copy is the §13.3 "Invite landing page" table, in `src/api/landing.rs`. ## 5. Deliverables - `src/api/invites.rs`: E-3, E-12, E-13, E-14. @@ -48,34 +56,34 @@ Open points the orchestrator must settle in the start prompt's notes: (a) E-28's - `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/recurring.rs`: E-28 to E-30 and the public recurring generation function (D-015). - `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/api/rate_limit.rs`: §8.6 rate limits and AC-2.12, as a layer for the marked line in `router.rs`. - `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/`. +- `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/`; and every generation case of §9.5 through the generation function (catch-up of several periods, clamping 29/30/31 and February 2027, mode `default` resolving the current account ratio, other modes from the template splits, idempotency on the unique index, a deleted generated expense not recreated, resume without backfill, archived account continues, `next_occurrence_date`), with recording fakes of `AuditWriter` and `PushQueue`. ## 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). +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 with the generation function (resolved point a); E-33; E-9 to E-11; E-5, E-6; E-4; rate limiting (resolved 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". +From SPEC §11.2 (v0.3), verbatim: "Server gate; integration tests per endpoint and per error code listed for it; every generation case of §9.5 (catch-up, clamping, split resolution, idempotency, resume without backfill, `next_occurrence_date`)". 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. +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, and a table §9.5 generation case → test name. ## 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. +- E-31, E-32 sync, the real audit writer and diff, push sender, the scheduler loop, `src/tasks/**`, `rotate-epoch`, `sync_mutations` pruning (M4). The generation function is M3's (resolved point a). - 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/**`. @@ -85,8 +93,8 @@ From SPEC §11.2, verbatim: "none". - 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. +- How resolved points (a) to (d) were applied; the generation function's signature (M4 calls it). +- Proposed decision entries and spec text (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - Contract requests (anything needed in M2a's files), open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-M4.md b/PLAN-M4.md index 588fca4..0dc0456 100644 --- a/PLAN-M4.md +++ b/PLAN-M4.md @@ -10,38 +10,46 @@ Implement the sync protocol (pull and push), the append-only audit log with its 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`. +7. SPEC §11.2 (row M4), §11.3, §14.7, §14.8; §6.3 "Server seams (D-015)"; `docs/decisions.md` D-004, D-015, D-021. +8. Files: `contracts/examples/sync/*.json`, `contracts/examples/push/*.json`, `contracts/test-vectors/*.json`, the M2a code under `server/` (including the shells `src/tasks/mod.rs`, `src/audit/`, `src/push/`), M3's generation function in `src/api/recurring.rs` once M3 has landed, `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). +- `sync_mutations` rows older than 90 days are pruned daily at 04:00 Europe/Stockholm (DST-aware) (§14.1, §14.4.9, D-021). +- Seams (D-015): `serve` calls `tasks::start` in `src/tasks/mod.rs` (M2a's stub, now M4's); M4 starts the scheduler, the push sender and the pruning from it. M2a defined the traits `AuditWriter` (`src/audit/`) and `PushQueue` (`src/push/`) with no-op implementations; M4 replaces the implementations with the real writer and diff and the FCM queue, keeping the trait signatures (a change to them is a contract request). M3 and M9 call the traits. +- Recurring generation (D-015): the function that generates the due periods of one template is M3's, in `src/api/recurring.rs`; M4's scheduler calls it for every template with `deleted_at IS NULL` and `paused = 0`. M4 lands after M3: if M3 has not landed when the scheduler is written, write the loop against the signature in M3's report and rebase on M3 before the gate. - 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). +- Scheduler: tokio task inside `kvitt-server`, runs at start and daily 06:00 Europe/Stockholm (DST-aware), and generates through M3's function (candidate periods, clamping, one transaction per candidate, split resolution, audit, `last_generated_period`, push are inside that function) (§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". +From SPEC §14.7 (v0.3): `kvitt-server`: `src/api/sync.rs`, `src/tasks/**`, `src/sync/**`, `src/audit/**`, `src/push/**`, `src/scheduler/**`, `src/cli/rotate_epoch.rs`, `tests/sync_*.rs`, `tests/scheduler_*.rs`, `tests/push_*.rs` (owner M4, shells from M2a; after landing: "later server plans"). +Also from §14.7: M4 may add dependency lines to a crate's `Cargo.toml` (second to land resolves `Cargo.lock` on rebase) and one line per new module to `mod.rs` files it needs. 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"). +Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries in the report. + +Resolved open points (SPEC v0.3; the start prompt may repeat them): +- (a) `serve` calls `tasks::start` (`src/tasks/mod.rs`, created by M2a, owned by M4); everything M4 runs in the background starts there (D-015). +- (b) The generation function is M3's; M4's scheduler calls it; M4 replaces the `AuditWriter` and `PushQueue` implementations that M3 calls; M4 lands after M3 (D-015). +- (c) Pruning runs daily at 04:00 Europe/Stockholm (D-021). ## 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/audit/**`: the real `AuditWriter` implementation and the diff of §14.4.7, used by sync, M3's generation function and the importer. +- `src/push/**`: the real `PushQueue` implementation: queue, FCM v1 sender, OAuth token cache, recipient selection, `sync_batch` aggregation. +- `src/tasks/**`: `start` launching the scheduler, the push sender and the pruning. +- `src/scheduler/**`: the daily 06:00 Europe/Stockholm tick (and one run at start) calling M3's generation function for every active template; the daily 04:00 `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/scheduler_*.rs`: the scheduler cases of §9.5: run at start, next 06:00 tick computed across both DST changes, catch-up after downtime through M3's function, paused and deleted templates skipped; the 04:00 pruning (rows of 90 days and older removed, newer kept). The per-template generation cases are M3's tests. - `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 @@ -51,15 +59,15 @@ Open points the orchestrator must settle in the start prompt's notes: (a) how `s 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). +7. Scheduler with an injectable clock, calling M3's generation function; tests for the scheduler cases. +8. `rotate-epoch`; pruning at 04:00. +9. Wiring through `tasks::start` (resolved 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". +From SPEC §11.2 (v0.3), verbatim: "Server gate; integration tests for every sync rule of §14.4 and the scheduler cases of §9.5 (run at start, daily 06:00 on DST days, catch-up through M3's generation function); 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. +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 scheduler case to a test name with no gaps. ## 8. HUMAN steps From SPEC §11.2, verbatim: "none". @@ -74,8 +82,8 @@ From SPEC §11.2, verbatim: "none". - 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. +- How resolved points (a) to (c) were applied. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - Contract requests, open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-M6.md b/PLAN-M6.md index b009fd7..9aa8437 100644 --- a/PLAN-M6.md +++ b/PLAN-M6.md @@ -7,21 +7,24 @@ Implement `:core:design` from the approved `docs/ux/design-spec.md`: theme, colo 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). +4. SPEC §9.7 Icon and colour tokens, with the hex values as updated by the spec change from U2's proposal; that change lands before M6 starts (§11.1). §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). +7. SPEC §11.2 (row M6), §11.3, §14.7 (including the Strings paragraph), §6.2 "Feature convention (D-017)"; `docs/decisions.md` D-017, D-019, D-020. 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. +- Shared components named in §6.2: money text, avatar, category chip, balance header, charts; the ledger rows (expense row per AC-8.10 and settlement row per AC-11.7, used by S-5, S-12 and the pending list of S-16) and the expense editor fields (amount entry, description, account picker, category grid, date, payer toggle, split section with live preview, notes, used by S-7 and S-19) (D-019); plus `MoneyFormatter`, `DateFormatter` (§6.2). The full inventory is the design spec's. +- Typography uses the Material 3 default typeface; no font file is bundled and no licence step is needed (D-020). +- Shared strings in `:core:design` include the notification copy of §13.3 and the channel name "Activity"/"Aktivitet" of §14.6, which `:app` uses; their keys never start with a feature name (D-017). - Money: sv `1 234,50 kr` (U+00A0 thousands and before `kr`, comma decimal, two decimals); other locales `SEK 1,234.50` (U+00A0 after `SEK`); never a minus sign; dates in lists `3 okt.` / `3 Oct`, with year when not the current year; detail `lördag 3 oktober 2026` / `Saturday, 3 October 2026`; times `14:02`; relative "last synced" per §9.8 (§9.8). Formatting lives in `:core:design`, never in `:domain` (§9.8). - Colour token names are the 12 of §9.7 (`ColorToken`); icon tokens the 30 of §9.7 (`CategoryIcons.ALL`) (§9.7, §14.2.4). - 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). +- No hard-coded colours, sizes or strings in features: tokens come from one design file; shared strings live in `:core:design` with keys from `docs/ux/strings.csv`; lint `MissingTranslation` is an error in this module (CLAUDE.md, §14.7 Strings, D-017). +- M2b created the module shell (build file, manifest, empty string files); M6 owns it and fills it in (D-004). - Screenshot goldens are committed; open every changed PNG before committing (CLAUDE.md). - `:core:design` depends only on `:core:model` (§6.2). @@ -31,18 +34,22 @@ Also from §14.7: "later plans may add catalog entries and module includes (reba 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. +Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries (for example the chart choice) in the report. + +Resolved open points (SPEC v0.3): +- (a) M6 builds the ledger rows and the expense editor fields in `:core:design` (D-019). +- (b) No font is bundled; the app uses the Material 3 default typeface (D-020). +- (c) The §9.7 colour change lands in the spec before M6 starts (§11.1); confirm it in step 2. ## 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/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, the ledger rows and the expense editor fields of D-019). - `.../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). +- `android/core/design/src/main/res/values/strings.xml`, `values-sv/strings.xml`: shared strings from `docs/ux/strings.csv`, including the notification copy and channel name. - 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). +- The proposed decision text for the chart choice (Vico or Canvas), in the report. ## 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`. @@ -75,8 +82,8 @@ Where in the flow: after step 8, before committing the final goldens; the orches - 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. +- How resolved points (a) to (c) were applied; the public API of the ledger rows and editor fields (for M7b, M7c, M7e, M7f). +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - Contract requests, open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-M7a.md b/PLAN-M7a.md index b25c11f..68b9461 100644 --- a/PLAN-M7a.md +++ b/PLAN-M7a.md @@ -16,10 +16,13 @@ Build `:feature:onboarding`: pairing (S-1), the partner profile step (S-2), wait - 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). +- 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 unless the report proposes it as a decision entry for the orchestrator (§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). +- Feature convention (§6.2, D-017): one composable entry point per screen, taking a `hiltViewModel()`-provided ViewModel and its navigation as plain lambdas; the feature never references another feature or the `NavHost`; ViewModels get repositories through the `:core:model` interfaces. Navigation graph, deep links and Hilt bindings are in `:app` (M8). +- M2b created this module's shell with one empty entry composable (D-004); this plan owns it and replaces it with the real entry points. +- Keys are prefixed by the feature name; lint `MissingTranslation` is an error in this module (§6.2, §13.4, D-017). +- The states each screen must have are the ones `docs/ux/design-spec.md` lists for it (§13.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). @@ -77,7 +80,7 @@ From SPEC §11.2, verbatim: "none". - 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. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - Contract requests (`:core:design`, `:core:model`), open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-M7b.md b/PLAN-M7b.md index 434e7fe..655fc91 100644 --- a/PLAN-M7b.md +++ b/PLAN-M7b.md @@ -16,10 +16,14 @@ Build `:feature:home` (Home S-4, sync status S-16, update banner and dialog S-17 - 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). +- 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 unless the report proposes it as a decision entry for the orchestrator (§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). +- Feature convention (§6.2, D-017): one composable entry point per screen, taking a `hiltViewModel()`-provided ViewModel and its navigation as plain lambdas; the feature never references another feature or the `NavHost`; ViewModels get repositories through the `:core:model` interfaces. Navigation graph, deep links and Hilt bindings are in `:app` (M8). +- M2b created this module's shell with one empty entry composable (D-004); this plan owns it and replaces it with the real entry points. +- Keys are prefixed by the feature name; lint `MissingTranslation` is an error in this module (§6.2, §13.4, D-017). +- The states each screen must have are the ones `docs/ux/design-spec.md` lists for it (§13.3). +- Shared UI (D-019): the ledger rows (S-5, S-16) come from `:core:design`, built by M6; never copy or re-implement it here. - 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). @@ -81,7 +85,7 @@ From SPEC §11.2, verbatim: "none". - 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. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - Contract requests (`:core:design`, `:core:model`), open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-M7c.md b/PLAN-M7c.md index df53ed4..d1a082a 100644 --- a/PLAN-M7c.md +++ b/PLAN-M7c.md @@ -16,10 +16,14 @@ Build `:feature:expense`: the full-screen expense editor (S-7) and the expense d - 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). +- 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 unless the report proposes it as a decision entry for the orchestrator (§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). +- Feature convention (§6.2, D-017): one composable entry point per screen, taking a `hiltViewModel()`-provided ViewModel and its navigation as plain lambdas; the feature never references another feature or the `NavHost`; ViewModels get repositories through the `:core:model` interfaces. Navigation graph, deep links and Hilt bindings are in `:app` (M8). +- M2b created this module's shell with one empty entry composable (D-004); this plan owns it and replaces it with the real entry points. +- Keys are prefixed by the feature name; lint `MissingTranslation` is an error in this module (§6.2, §13.4, D-017). +- The states each screen must have are the ones `docs/ux/design-spec.md` lists for it (§13.3). +- Shared UI (D-019): the expense editor fields (S-7) come from `:core:design`, built by M6; never copy or re-implement it here. - 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). @@ -75,7 +79,7 @@ From SPEC §11.2, verbatim: "none". - 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. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - Contract requests (`:core:design`, `:core:model`), open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-M7d.md b/PLAN-M7d.md index ac8b3ff..e49ed67 100644 --- a/PLAN-M7d.md +++ b/PLAN-M7d.md @@ -16,10 +16,13 @@ Build `:feature:settle`: settle up, global and per account (S-9), and the settle - 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). +- 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 unless the report proposes it as a decision entry for the orchestrator (§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). +- Feature convention (§6.2, D-017): one composable entry point per screen, taking a `hiltViewModel()`-provided ViewModel and its navigation as plain lambdas; the feature never references another feature or the `NavHost`; ViewModels get repositories through the `:core:model` interfaces. Navigation graph, deep links and Hilt bindings are in `:app` (M8). +- M2b created this module's shell with one empty entry composable (D-004); this plan owns it and replaces it with the real entry points. +- Keys are prefixed by the feature name; lint `MissingTranslation` is an error in this module (§6.2, §13.4, D-017). +- The states each screen must have are the ones `docs/ux/design-spec.md` lists for it (§13.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). - Global settle-up: direction defaults to the user who owes paying, amount to the absolute global balance (empty when zero); the proposal comes from `SettlementRepository.start`/`recompute` (never computed in the feature); one editable row per proposed account plus "Add account" (any account, active or archived); confirm enabled only when allocations sum to the amount; remaining difference shown live (AC-11.3). @@ -75,7 +78,7 @@ From SPEC §11.2, verbatim: "none". - 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. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - Contract requests (`:core:design`, `:core:model`), open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-M7e.md b/PLAN-M7e.md index d8102c0..a1d1580 100644 --- a/PLAN-M7e.md +++ b/PLAN-M7e.md @@ -16,10 +16,14 @@ Build `:feature:analytics`: the analytics screen with range, granularity, measur - 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). +- 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 unless the report proposes it as a decision entry for the orchestrator (§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). +- Feature convention (§6.2, D-017): one composable entry point per screen, taking a `hiltViewModel()`-provided ViewModel and its navigation as plain lambdas; the feature never references another feature or the `NavHost`; ViewModels get repositories through the `:core:model` interfaces. Navigation graph, deep links and Hilt bindings are in `:app` (M8). +- M2b created this module's shell with one empty entry composable (D-004); this plan owns it and replaces it with the real entry points. +- Keys are prefixed by the feature name; lint `MissingTranslation` is an error in this module (§6.2, §13.4, D-017). +- The states each screen must have are the ones `docs/ux/design-spec.md` lists for it (§13.3). +- Shared UI (D-019): the expense ledger row (S-12) comes from `:core:design`, built by M6; never copy or re-implement it here. - 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). - Presets `this_month`, `last_3_months`, `last_6_months`, `last_12_months`, `this_year`, `all_time`, `custom` (start and end pickers); default `last_6_months`; granularity `month` (default) or `week`; measure `household` (default) or `my_share`; group by `category` (default) or `account` (AC-14.2 to AC-14.5). @@ -74,7 +78,7 @@ From SPEC §11.2, verbatim: "none". - 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. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - Contract requests (`:core:design`, `:core:model`), open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-M7f.md b/PLAN-M7f.md index 0962ecf..ff996ba 100644 --- a/PLAN-M7f.md +++ b/PLAN-M7f.md @@ -16,10 +16,14 @@ Build `:feature:settings`: the settings hub (S-13), profile (S-14), categories ( - 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). +- 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 unless the report proposes it as a decision entry for the orchestrator (§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). +- Feature convention (§6.2, D-017): one composable entry point per screen, taking a `hiltViewModel()`-provided ViewModel and its navigation as plain lambdas; the feature never references another feature or the `NavHost`; ViewModels get repositories through the `:core:model` interfaces. Navigation graph, deep links and Hilt bindings are in `:app` (M8). +- M2b created this module's shell with one empty entry composable (D-004); this plan owns it and replaces it with the real entry points. +- Keys are prefixed by the feature name; lint `MissingTranslation` is an error in this module (§6.2, §13.4, D-017). +- The states each screen must have are the ones `docs/ux/design-spec.md` lists for it (§13.3). +- Shared UI (D-019): the expense editor fields (S-19) come from `:core:design`, built by M6; never copy or re-implement it here. - 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). - Hub rows: Profile, Categories, Recurring expenses, Devices, Invite partner or Pair a device (admin only), Sync status (opens S-16 via `:app`), About; own and partner's avatar and name (§13.3 S-13, §8.4). @@ -81,7 +85,7 @@ From SPEC §11.2, verbatim: "none". - 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. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - Contract requests (`:core:design`, `:core:model`), open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-M8.md b/PLAN-M8.md index 5919d9a..f16bd65 100644 --- a/PLAN-M8.md +++ b/PLAN-M8.md @@ -7,14 +7,17 @@ Assemble the app: `:app` navigation and deep links, Hilt bindings from the `:cor 1. SPEC §6.2 Android app (module table, the paragraph on the architecture check) and §13.2 Navigation model. 2. SPEC §14.6 Push payload v1, "Phone handling" (items 1 to 4). 3. SPEC §4 FR-2 AC-2.6, AC-2.9, AC-2.10; FR-3 AC-3.3, AC-3.4; FR-5 AC-5.1; FR-13 Push notifications (AC-13.5 to AC-13.8); FR-15 AC-15.3; FR-16 In-app update; §13.3 system notification copy table. -4. SPEC §8.2 (token never logged), §8.5 Transport, §8.8 Phone hardening, §10.5.3 (the `kvitt.tls.pinnedRoot` property), §10.6 (version code and name properties, release signing keys), §10.7 (`google-services.json`). +4. SPEC §8.2 (token never logged), §8.5 Transport, §8.8 Phone hardening, §10.5.3 (the `kvitt.tls.pinnedRoot` property), §10.6 (version code and name properties, release signing keys), §10.7 (`google-services.json` in `infra/secrets/`, copied by `infra/dev/copy-google-services.sh`). 5. SPEC §14.4.8 Epoch recovery and re-pairing (the `Unrecognised` path), §14.2.4 (`SessionState`, `UpdateState`). -6. SPEC §10 intro "Development first (D-001)" and `infra/dev/README.md` (how to run and bootstrap the development server). -7. SPEC §11.2 (row M8), §11.3 (note M8), §14.7. -8. Files: `android/app/**`, `android/build-logic/**`, `android/gradle/libs.versions.toml`, the public APIs of `android/data/**` (its Hilt modules), `android/feature/*/**` (their screen entry points), `android/core/model/**`, `docs/ux/strings.csv` (notification copy), `docs/decisions.md` D-001, `CLAUDE.md` (THE PHONE, THE OWNER, BUILD AND VERIFY). +6. SPEC §10 intro "Development first (D-001)" and `infra/README.md` (dev stack section) and the scripts in `infra/dev/` (how to run and bootstrap the development server). +7. SPEC §11.2 (row M8), §11.3 (note M8), §14.7, §6.2 "Feature convention (D-017)"; `docs/decisions.md` D-017, D-022. +8. Files: `android/app/**`, `android/build-logic/**`, `android/gradle/libs.versions.toml`, the public APIs of `android/data/**` (its Hilt modules), `android/feature/*/**` (their screen entry points), `android/core/model/**`, `docs/ux/strings.csv` (notification copy, which lives in `:core:design`), `docs/decisions.md` D-001, `CLAUDE.md` (THE PHONE, THE OWNER, BUILD AND VERIFY). ## 3. Already established - Only `:app` sees every module and binds the `:data` repositories to the `:core:model` interfaces through Hilt; features never see `:data` or `:domain` (§6.2, CLAUDE.md Layering). M8 is the only plan that edits `:app` after M2b (§11.3). +- Feature convention (D-017): each feature exposes one composable entry point per screen with a `hiltViewModel()`-provided ViewModel and navigation as plain lambdas; `:app` owns the `NavHost` and connects every lambda to a route; fakes are bound only in test and debug-preview source sets. +- Strings (D-017): `:app` holds only the app name; the notification copy and the channel name come from `:core:design`'s shared strings. +- `google-services.json` is in `infra/secrets/` of the main checkout (M2c-dev, D-022); M8 copies it into `android/app/` (git-ignored) with `infra/dev/copy-google-services.sh`. - Navigation: bottom navigation Home (Översikt), Analytics (Statistik), Settings (Inställningar); FAB "Add expense" on Home and Account detail; account detail, editors, settle up, details and settings sub-screens are pushed screens with back; the expense editor is full screen (§13.2). - Deep links: `kvitt://invite?server=…&code=…` and `https://icehack.asuscomm.com:8443/i/` (unverified link) open S-1 with both fields filled (AC-2.6). Only `MainActivity` and the FCM service are exported (§8.8). - After pairing: full sync, then the `POST_NOTIFICATIONS` request (AC-2.9); partner pairing goes through S-2 before Home (AC-2.10). @@ -32,20 +35,20 @@ From SPEC §14.7, verbatim: `android/adapter/fcm/**`, `android/app/**` (after th Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. -Note: network security config (§8.5), manifest hardening (§8.8) and release signing and version properties (§10.6) are not named in §11.2's M8 row, but they live in `:app`, which only M8 edits (§11.3); they are listed as deliverables here. The orchestrator confirms or reassigns them in the start prompt. +Network security config (§8.5), manifest hardening (§8.8), release signing and version properties (§10.6) are M8's (§11.2 row M8, v0.3). Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries in the report. ## 5. Deliverables - `android/app/build.gradle.kts`: dependencies on every module, Hilt, Firebase (via `google-services.json`), version properties, release signing config read from `local.properties`. - `android/app/src/main/AndroidManifest.xml`: `MainActivity` with launcher and deep-link filters, FCM service from `:adapter:fcm`, `POST_NOTIFICATIONS`, `REQUEST_INSTALL_PACKAGES`, backup disabled, data extraction rules. - `android/app/src/main/res/xml/network_security_config.xml` (release) and the debug variant; `data_extraction_rules.xml`. - `android/app/src/main/kotlin/se/hagstedt/kvitt/**`: `Application` (Hilt), `MainActivity`, navigation graph, deep-link parsing, Hilt modules binding interfaces to `:data`, push event handling and notification building, revoke and `Unrecognised` routing, notification-permission request after pairing. -- `android/app/src/main/res/values/strings.xml`, `values-sv/strings.xml`: notification copy and channel name from `docs/ux/strings.csv`. +- `android/app/src/main/res/values/strings.xml`, `values-sv/strings.xml`: the app name only (from M2b's shell); notification text is built from `:core:design` strings. - `android/adapter/fcm/**`: `FirebaseMessagingService`, token registration, mapping to `PushEvent`, unit tests for the mapping. - Unit tests in `:app`: deep-link parsing, notification text building for each §14.6 type and the generic fallback, notification ids. - An end-to-end run report in the plan report (no file): pairing and the offline test on the phone. ## 6. Steps -1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m8 -b agent-m8 main`, then in `.claude/worktrees/agent-m8`: `git reset --hard main`. Copy `android/local.properties` and `android/app/google-services.json` from the main checkout (never commit them; add `sdk.dir` to `local.properties` if missing). Run `tools/install-git-hooks.sh`. +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m8 -b agent-m8 main`, then in `.claude/worktrees/agent-m8`: `git reset --hard main`. Copy `android/local.properties` from the main checkout, then run `infra/dev/copy-google-services.sh /Users/jhag/projects/kvitt` to place `android/app/google-services.json` from the main checkout's `infra/secrets/` (never commit either; add `sdk.dir` to `local.properties` if missing). Run `tools/install-git-hooks.sh`. 2. Read §2 in order. Confirm M3, M4, M5, M7a to M7f and M2c-dev have landed; if not, stop and report. 3. Run `python3 tools/dependency-report/latest.py`; record the lines. Versions: Firebase BoM and the Google services Gradle plugin at latest stable (firebase.google.com release notes); Hilt navigation and Navigation Compose at latest stable; confirm current FCM Android and `PackageInstaller` usage from official docs; record each. 4. Hilt modules and the navigation graph; wire every feature screen; deep links. @@ -53,7 +56,7 @@ Note: network security config (§8.5), manifest hardening (§8.8) and release si 6. Revoke wipe, `Unrecognised` routing, notification permission after pairing, update banner wiring. 7. Manifest hardening, network security configs, version properties, release signing config (no keystore needed for debug). 8. Run the gate's build part: `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check` from `android/`. -9. Development server: start it per `infra/dev/README.md` with an image built from current `main` (no Gradle build running while Docker builds), reset its dev data, run `kvitt-server bootstrap`, note the code (never log it to a committed file). +9. Development server: start it per `infra/README.md` (dev stack section) and the scripts in `infra/dev/` with an image built from current `main` (no Gradle build running while Docker builds), reset its dev data, run `kvitt-server bootstrap`, note the code (never log it to a committed file). 10. Phone (only if the start prompt names this plan as the one on the phone): `/opt/homebrew/share/android-commandlinetools/platform-tools/adb shell pm list packages | grep se.hagstedt.kvitt` to confirm ids; `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH tools/gradle-serial.sh installDebug`. The debug build must not replace a build holding the owner's real data without asking. 11. End-to-end test with the owner (HUMAN, §8): pairing with the bootstrap code and the Mac's LAN URL; the agent then creates a partner invite and accepts it with `curl` as a scripted partner device on the dev server, so the household is complete (FR-5); the owner adds an expense offline, reconnects; the agent confirms the expense reached the server (pull with the scripted partner's token) and pulls the app's sync state over USB (`adb shell run-as se.hagstedt.kvitt.debug …`) to show pending count 0. If push is configured, the agent also posts an expense as the scripted partner and asks the owner whether a notification appeared. 12. Commit on `agent-m8`, `git rebase main`, rerun the gate. Never merge, push or remove the worktree. @@ -77,9 +80,8 @@ Where in the flow: step 11, after the debug build is installed and the dev serve - Branch `agent-m8`, head commit hash after rebasing on `main`. - Gate output: BUILD line, actionable tasks line, test counts from fresh XML; the end-to-end result (pair, offline pending, synced, pending count 0), with what the agent checked itself. - Versions chosen with sources; dependency-report lines. -- Whether the start prompt confirmed the network security config, hardening and signing deliverables. - Dev server state left behind (running or stopped; scripted partner device name). -- Numbering used or `TODO(spec)` occurrences. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - HUMAN steps done and outstanding, worded for relay. - Contract requests, open questions, spec gaps (with section numbers). diff --git a/PLAN-M9.md b/PLAN-M9.md index 9abd801..28a577d 100644 --- a/PLAN-M9.md +++ b/PLAN-M9.md @@ -11,7 +11,7 @@ Implement the one-off CLI `kvitt-import` that imports Splitwise CSV exports, one 5. SPEC §14.1 SQLite schema v1 (`import_key` columns, `accounts`, `expenses`, `expense_splits`, `settlements`, `settlement_allocations`, `audit_log`) and its server-side rules. 6. SPEC §9.1 Splits (exact rule), §9.6 Limits and validation, §14.4.7 Audit diff (`create`), §14.5.2 (`compute_split`, `validate_settlement`). 7. SPEC §11.2 (row M9), §14.7, §14.8. -8. Files: `server/crates/kvitt-server/src/lib.rs` and the modules it exposes (DB pool, sequence, audit writer), `server/Cargo.toml`, `CLAUDE.md` (ANTI-PATTERNS: never commit personal data or the Splitwise CSV). +8. Files: `server/crates/kvitt-server/src/lib.rs` and the modules it exposes (DB pool, sequence, the `AuditWriter` trait and M4's implementation, D-015), M2a's shell of `server/crates/kvitt-import/` (D-004), `server/Cargo.toml`, `CLAUDE.md` (ANTI-PATTERNS: never commit personal data or the Splitwise CSV). ## 3. Already established - Command shape: `kvitt-import --db --file --account "" --user "=" --user "=" [--fx =]... [--dry-run]` (§10.8). @@ -38,8 +38,8 @@ Edit nothing outside these paths; a needed change elsewhere is a contract reques ## 5. Deliverables - `server/crates/kvitt-import/Cargo.toml`: depends on `kvitt-domain` and `kvitt-server` (library). - `server/crates/kvitt-import/src/main.rs`: CLI parsing per §10.8. -- `server/crates/kvitt-import/src/**`: CSV reader (quoting, both header variants, member matching by name), row classification (expense, payment, total, blank), amount parsing, split derivation, fx conversion, import key, category mapping, writer (one transaction, audit through the server's audit writer), summary. -- `server/crates/kvitt-import/data/splitwise_categories.csv`: the mapping table, columns `splitwise_name,language,default_key`, every English and Swedish name of the §10.8 table (location chosen inside the crate because §14.7 gives `contracts/**` to M1; the orchestrator may move it). +- `server/crates/kvitt-import/src/**`: CSV reader (quoting, both header variants, member matching by name), row classification (expense, payment, total, blank), amount parsing, split derivation, fx conversion, import key, category mapping, writer (one transaction, audit through the `AuditWriter` trait, nothing through `PushQueue`), summary. +- `server/crates/kvitt-import/data/splitwise_categories.csv`: the mapping table, columns `splitwise_name,language,default_key`, every English and Swedish name of the §10.8 table (location fixed in §10.8 and §14.7, v0.3). - `server/crates/kvitt-import/tests/fixtures/*.csv`: synthetic CSVs only (English variant, Swedish variant with reversed member order, quoted commas, payments in both languages, totals rows in both languages, blank rows, a non-SEK row, a truncated description, an unknown category, an invalid row, duplicate identical lines). - `server/crates/kvitt-import/tests/*.rs`: integration tests against a temp database created with the server's migrations. @@ -72,9 +72,8 @@ Where in the flow: already done; both exports are in `export/` (git-ignored) and - Branch `agent-m9`, head commit hash after rebasing on `main`. - Gate output: fmt and clippy exit status, every `test result:` line. - Versions of crates added, with sources. -- Location of the category table file. - Optional dry-run aggregates (row counts per file, unmapped category names with counts), if run. -- Numbering used or `TODO(spec)` occurrences. +- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences. - Contract requests, open questions, spec gaps (with section numbers). ## 11. Numbering diff --git a/PLAN-U1.md b/PLAN-U1.md index 845f887..da704b4 100644 --- a/PLAN-U1.md +++ b/PLAN-U1.md @@ -26,7 +26,7 @@ Read these, in this order, and nothing else before starting. Everything the UX w - Money format sv `1 234,50 kr` with U+00A0; dates in lists `3 okt.`, detail `lördag 3 oktober 2026`, times `14:02` (§9.8). - Balance wording: " owes you X", "You owe X", "All settled up" (account screens "Settled up") (§9.3); effect lines "You lent", "You borrowed", "Not involved", "Paid for yourself" (§9.2). - Phase 1 screens per direction: S-4 Home, S-7 Expense editor, S-5 Account detail, S-9 Settle up, S-11 Analytics, S-8 Expense detail with timeline, each light and dark, Swedish, names "Jacob" and "Anna", Swedish shops and amounts, no lorem ipsum (§13.4). -- No external requests except Google Fonts, and only for a font whose licence allows bundling as an Android font resource (§13.4). +- No external requests except Google Fonts (§13.4). The app bundles no font and uses the Material 3 default typeface (D-020); a direction may preview another typeface, but U2's design spec replaces it with the default. - Suggested starting points "Kvitto", "Skärgård", "Kontor" may be used, replaced or merged; at least two clearly different directions (§13.4). - The UX agent writes no Kotlin (§13, §13.5). @@ -42,7 +42,7 @@ Edit nothing outside these paths; a needed change elsewhere is a contract reques - `docs/ux/directions/README.md`: comparison table (character, strengths, risks, implementation cost in Compose) and the recommendation with reasons. ## 6. Steps -1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-ux -b agent-ux main`, then in `.claude/worktrees/agent-ux`: `git reset --hard main`. No git-ignored files are needed for this plan; do not copy any. Do not use the old `UX-sprint` branch or any worktree outside `/Users/jhag/projects/kvitt`. +1. Worktree. The owner runs the UX sprint in a separate session in the worktree `~/orca/workspaces/kvitt/UX-sprint` on branch `UX-sprint`; work only there and `git rebase main` before reporting. No git-ignored files are needed for this plan; do not copy any. 2. Read §2 of this plan in order. 3. Choose 2 or 3 directions (start from §13.4's suggestions or replace them) and write one paragraph each in `docs/ux/directions/README.md` before drawing anything. 4. Build the realistic Swedish data set once (shared by all directions): users Jacob and Anna; accounts such as "Vardag", "Huset", "Japan 2027" plus one archived account; expenses at Swedish shops (ICA, Coop, Systembolaget, SJ, Clas Ohlson) with amounts that exercise thousands separators; one recurring expense; a settlement with two allocations; a timeline with create and change lines per AC-12.4 and a pending line per AC-12.5; analytics over `last_6_months` by category. @@ -51,11 +51,11 @@ Edit nothing outside these paths; a needed change elsewhere is a contract reques 7. Export PNGs of every screen of every direction into `png/`. Use a headless browser already installed on this Mac (check with `command -v`; for example Google Chrome's `--headless --screenshot`). If none is installed, stop and ask; do not install tools without the owner's yes. 8. Write the comparison table and recommendation in `docs/ux/directions/README.md`. 9. Self-check the gate (§7): open each `index.html` (file loads with no external request other than Google Fonts), every screen listed for phase 1 appears in each direction, all copy Swedish, no placeholder text. Grep the HTML for `lorem`, `TODO`, `XXX`, `placeholder`. -10. Commit on `agent-ux` (only files under `docs/ux/`), then `git rebase main`. Never merge, push or remove the worktree. +10. Commit on `UX-sprint` (only files under `docs/ux/`), then `git rebase main`. Never merge, push or remove the worktree. ## 7. Gate -From SPEC §11.2, verbatim: "Files exist and open; every screen of §13.3 appears in each direction; Swedish copy, no placeholder text". -Green means: every deliverable of §5 exists; each `index.html` opens in a browser offline (Google Fonts excepted); each direction shows each phase-1 screen of §13.4 (S-4, S-7, S-5, S-9, S-11, S-8) in light and dark; every visible string is Swedish; a grep for `lorem|TODO|XXX` in `docs/ux/` returns nothing. Note: §11.2 says "every screen of §13.3" while §13.4 lists six screens for phase 1; deliver the six of §13.4 and state in the report how the other screens are covered (see the report's open questions) unless the start prompt says otherwise. +From SPEC §11.2 (v0.3), verbatim: "Files exist and open; each direction shows the six phase-1 screens of §13.4 (S-4, S-7, S-5, S-9, S-11, S-8) in light and dark; Swedish copy, no placeholder text". +Green means: every deliverable of §5 exists; each `index.html` opens in a browser offline (Google Fonts excepted); each direction shows each phase-1 screen of §13.4 (S-4, S-7, S-5, S-9, S-11, S-8) in light and dark; every visible string is Swedish; a grep for `lorem|TODO|XXX` in `docs/ux/` returns nothing. ## 8. HUMAN steps From SPEC §11.2, verbatim: "Review 2-3 directions, pick one, give comments". @@ -70,11 +70,11 @@ Where in the flow: after this plan's report, the orchestrator shows the owner th - M6 design system work and any Android code. ## 10. Report -- Branch `agent-ux` and the head commit hash after rebasing on `main`. +- Branch `UX-sprint` and the head commit hash after rebasing on `main`. - Gate: the list of files produced (per direction, count of PNGs) and the result of the placeholder grep. - Fonts chosen per direction with their licence and whether they bundle as Android font resources; any other external dependency. - The recommendation in one line. -- Open questions and spec gaps with section numbers (including how the "every screen of §13.3" gate wording was handled). +- Open questions and spec gaps with section numbers. - Numbering used (none expected; say `none`). - HUMAN step to relay: "Review 2-3 directions, pick one, give comments", with the PNG paths. diff --git a/PLAN-U2.md b/PLAN-U2.md index b153483..289afea 100644 --- a/PLAN-U2.md +++ b/PLAN-U2.md @@ -23,7 +23,7 @@ Read these, in this order, and nothing else before starting. Everything needed i - Rejection explanations on S-16 are the English texts of §14.4.6. - Notification channel id `activity`, name "Activity" / "Aktivitet" (§14.6). - Initials: first letter of the first two words of the display name, uppercase, on a colour derived from the user id; the UX sprint defines the two colours (AC-4.5). -- `strings.csv` columns `key,en,sv,notes`; keys `snake_case` prefixed by screen (example `home_balance_owes_you`); placeholders as Android format arguments (`%1$s`); plurals marked in `notes` with their quantities (§13.4). Feature agents add no strings outside this file without a decisions entry (§13.4). +- `strings.csv` columns `key,en,sv,notes`; keys `snake_case` prefixed by the feature module that holds them (`home_balance_owes_you`, `expense_save`; shared `:core:design` strings, the notification copy included, never start with a feature name; the app name is the only `:app` string) (§6.2, §13.4, D-017); placeholders as Android format arguments (`%1$s`); plurals marked in `notes` with their quantities (§13.4). Feature agents add no strings outside this file unless the orchestrator records a decision for them (§13.4). - Category default names (en, sv) are fixed in AC-7.2; every enum shown to users needs strings in both languages (AC-19.3). - Colour token names fixed; new hex values for §9.7 are proposed, and the orchestrator turns them into a spec change (§13.4). - The UX agent writes no Kotlin (§13). @@ -40,7 +40,7 @@ Edit nothing outside these paths; a needed change elsewhere is a contract reques - `docs/ux/colour-tokens-proposal.md`: the proposed light and dark hex values for the 12 tokens of §9.7 with contrast ratios against both surfaces, for the orchestrator's spec change. ## 6. Steps -1. Worktree. If `.claude/worktrees/agent-ux` exists (U1 not landed yet), work there and `git rebase main`. Otherwise, from the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-ux -b agent-ux main`, then in it `git reset --hard main`. No git-ignored files are needed; copy none. +1. Worktree. The owner runs the UX sprint in a separate session in `~/orca/workspaces/kvitt/UX-sprint` on branch `UX-sprint`; work there and `git rebase main`. No git-ignored files are needed; copy none. 2. Read §2 in order. List the owner's comments and how each is addressed (goes into the report). 3. Write the token section of `design-spec.md` first, then the component inventory, deriving components from what S-1 to S-22 need (balance header, account row, ledger rows for expenses and settlements, money text, avatar with initials, category chip and grid, split control, allocation row, timeline line, sync line, update banner, charts, empty and error blocks, "Needs connection" hint). 4. For each screen S-1 to S-22, in id order: layout, each applicable state of §13.3 plus the screen-specific states its "Must show" column implies (for example S-1 errors `invite_invalid`, network, `rate_limited`; S-8 deleted; S-9 over-settle warning), interactions, TalkBack labels. Mark non-applicable states explicitly. @@ -48,10 +48,10 @@ Edit nothing outside these paths; a needed change elsewhere is a contract reques 6. Render every screen and state, light and dark, Swedish, to `docs/ux/screens/` with a headless browser already installed (check with `command -v`; stop and ask if none). 7. Write `colour-tokens-proposal.md`. 8. Self-check the gate (§7) with a script you do not commit: every row of `strings.csv` has non-empty `en` and `sv`; every key is unique; every screen S-1 to S-22 has a section in `design-spec.md` covering all six state names (present or "not applicable"); grep for `lorem|TODO|XXX` is empty. -9. Commit on `agent-ux` (only files under `docs/ux/`), `git rebase main`. Never merge, push or remove the worktree. +9. Commit on `UX-sprint` (only files under `docs/ux/`), `git rebase main`. Never merge, push or remove the worktree. ## 7. Gate -From SPEC §11.2, verbatim: "Every string key has `en` and `sv`; every screen has all states of §13.3". +From SPEC §11.2 (v0.3), verbatim: "Every string key has `en` and `sv`; every screen has every state that `design-spec.md` lists for it (§13.3)". The per-screen state list in `design-spec.md` is the binding one (§13.3). Green means: the self-check of step 8 passes with zero missing translations, zero duplicate keys, all 22 screens present with each of the six §13.3 states either specified or marked not applicable, and a PNG for every specified state in light and dark. ## 8. HUMAN steps @@ -66,11 +66,11 @@ Where in the flow: after this plan's report; M6 starts only after the owner's ap - Any file outside `docs/ux/`. ## 10. Report -- Branch `agent-ux` and head commit hash after rebasing on `main`. +- Branch `UX-sprint` and head commit hash after rebasing on `main`. - Gate self-check output lines (string count, missing translations 0, screens 22, PNG count). - How each owner comment was addressed, one line each. - The proposed §9.7 hex values (summary line; detail in the proposal file). -- Fonts and their licences (owner must clear a bundled font before M6, CLAUDE.md "Licences first"). +- Typography in the Material 3 default typeface; no font is bundled (D-020). - Open questions and spec gaps with section numbers; any screen that needs data not in §14.2.4. - Numbering used (none expected; say `none`). - HUMAN step to relay: "Approve the design spec", with the paths to review. diff --git a/SPEC.md b/SPEC.md index 8e1cc74..c9a1a92 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,6 +1,6 @@ # Kvitt: specification -Version **0.2** (2026-10-17). Schema version **1**. Sync protocol version **1**. Push payload version **1**. +Version **0.3** (2026-10-17). Schema version **1**. Sync protocol version **1**. Push payload version **1**. This file is the single source of truth for Kvitt. It is written for implementation agents that have no other context. If code, a plan or `CLAUDE.md` disagrees with this file, this file is right. Words in `code` are literal identifiers, paths, enum values or error codes and must be used exactly as written. Anything not decided here is listed in §12 as `TODO(spec)`; do not invent a value for it. @@ -119,7 +119,7 @@ The server starts with an empty database. The household is created from the comm * AC-2.2 The admin creates a `device` invite for either user from Settings (to pair a second phone). * AC-2.3 An invite is single-use and expires 72 hours after creation. An expired, cancelled, used or unknown code is refused with `invite_invalid`; the response does not reveal which. * AC-2.4 Accepting an invite takes the code, a device name and (for `admin_setup` and `partner`) a display name. It returns a bearer token, the user, the device and the household's users. For a `partner` invite it creates the `member` user. -* AC-2.5 `GET /i/` serves an HTML landing page in Swedish if the request's `Accept-Language` prefers `sv`, English otherwise. It shows: the app name, "Download Kvitt" linking to the latest APK (FR-16), an "Open in Kvitt" button linking to `kvitt://invite?server=&code=`, the code in groups of 4, and one sentence of instructions. It does not reveal whether the code is valid and contains no personal data. +* AC-2.5 `GET /i/` serves an HTML landing page in Swedish if the request's `Accept-Language` prefers `sv`, English otherwise. It shows: the app name, "Download Kvitt" linking to the latest APK (FR-16), an "Open in Kvitt" button linking to `kvitt://invite?server=&code=`, the code in groups of 4, and one sentence of instructions (copy in §13.3, "Invite landing page"). It does not reveal whether the code is valid and contains no personal data. * AC-2.6 The app opens from `kvitt://invite?server=…&code=…` and from `https://icehack.asuscomm.com:8443/i/` (unverified link, the user may get a chooser) directly into the pairing screen with both fields filled. * AC-2.7 The pairing screen also accepts manual entry: a server URL (prefilled with `https://icehack.asuscomm.com:8443` in release builds) and the code. Codes are normalised per §8.3 before sending. * AC-2.8 Release builds accept only `https://` server URLs. Debug builds also accept `http://` for `localhost`, `10.0.2.2` and private LAN addresses (`10.*`, `172.16.*`-`172.31.*`, `192.168.*`). @@ -403,15 +403,16 @@ Local-only phone tables: `sync_state` and `sync_rejections` (§14.2). | Push | Firebase Cloud Messaging, only inside `:adapter:fcm` | Free tier; vendor isolated per `CLAUDE.md` | | Secrets | Android Keystore: an AES-256-GCM key (non-exportable) encrypts the device token; ciphertext in a private DataStore file | `EncryptedSharedPreferences` is deprecated | | Build | Gradle with version catalog `android/gradle/libs.versions.toml`, Gradle daemon on JDK 25 (pinned), bytecode target Java 17 | Existing pin | -| Tests | JUnit, Compose screenshot tests (Roborazzi or the Android Gradle Plugin's Compose Preview Screenshot Testing, whichever is stable at implementation time; record the choice in decisions), Room migration tests, Ktor `MockEngine` | No mocking libraries (`CLAUDE.md`) | +| Tests | JUnit, Compose screenshot tests (Roborazzi or the Android Gradle Plugin's Compose Preview Screenshot Testing, whichever is stable at implementation time; M2b chooses and reports it, the orchestrator records it as D-010), Room migration tests, Ktor `MockEngine` | No mocking libraries (`CLAUDE.md`) | +| Typeface | The Material 3 default typeface; the app bundles no font file (D-020) | No licence step, no font resource | Module graph (directory `android/`): | Module | Kind | Contents | May depend on | |---|---|---|---| -| `:core:model` | pure Kotlin/JVM | Domain types (§14.5.1), `*State` classes, repository interfaces (§14.2.4) | kotlin, kotlinx | +| `:core:model` | pure Kotlin/JVM | Domain types (§14.5.1), analytics types (§14.5.4, package `se.hagstedt.kvitt.model.analytics`), `*State` classes, repository interfaces (§14.2.4) | kotlin, kotlinx | | `:domain` | pure Kotlin/JVM | Split, balance, allocation, analytics functions (§14.5); test-vector tests | `:core:model` | -| `:core:design` | Android library | Theme, tokens, typography, shared components (money text, avatar, category chip, balance header, charts), `MoneyFormatter`, `DateFormatter` | `:core:model` | +| `:core:design` | Android library | Theme, tokens, typography, shared components (money text, avatar, category chip, balance header, charts; the ledger rows for expenses and settlements used by S-5, S-12 and S-16; the expense editor fields used by S-7 and S-19, D-019), `MoneyFormatter`, `DateFormatter`, shared strings | `:core:model` | | `:core:fakes` | Android library, used only by tests, screenshot tests and debug previews | Scenario-driven fakes of every repository interface (they call `:domain` for computed values so fakes never disagree with the real rules) | `:core:model`, `:domain` | | `:data` | Android library | Room DB, DAOs, entities, Ktor API client and DTOs, sync engine, WorkManager workers, token store, repository implementations, APK updater | `:core:model`, `:domain` | | `:adapter:fcm` | Android library | `FirebaseMessagingService`, token registration, maps messages to `PushEvent` (`:core:model`) | `:core:model` | @@ -426,6 +427,16 @@ Module graph (directory `android/`): Features never call `:domain` directly: repositories return computed state (balances, previews, proposals), implemented in `:data` with `:domain`. The architecture check (Gradle task in `check`, owned by milestone M2b) fails the build on any other dependency edge and on `java.time`, `java.io` or `android.*` imports in `:core:model` and `:domain`. +**Feature convention (D-017).** Binding for every feature module: + +* Each feature exposes one composable entry point per screen. The entry point takes a ViewModel provided with `hiltViewModel()` and its navigation as plain lambdas (for example `onOpenExpense: (ExpenseId) -> Unit`, `onBack: () -> Unit`). +* Features never reference other features or the `NavHost`. `:app` owns the `NavHost` and connects the lambdas to routes. +* ViewModels receive repositories through the `:core:model` interfaces (§14.2.4). `:app` binds the interfaces through Hilt to `:data`; bindings to `:core:fakes` exist only in test and debug-preview source sets and never reach a release build. +* Shared UI that two features need is built in `:core:design` by M6 (D-019); a feature never copies another feature's component. +* Strings: `:app` holds only the app name; `:core:design` holds the shared strings (including the notification copy and channel name of §13.3 and §14.6, which `:app` uses); each feature module holds its own. Lint `MissingTranslation` is an error in every module. Keys of a feature module are prefixed by the feature name (`home_`, `account_`, `expense_`, ...); keys in `:core:design` never start with a feature name. This rule wins over any other key convention (§13.4, §14.7). + +**Fakes.** Every fake in `:core:fakes` has at least the empty scenario (D-018): the household is complete (two named users, no pictures), there are no accounts, the categories are exactly the ten defaults of AC-7.2, and there are no expenses, settlements, templates, rejections or pending changes. Feature agents add their own scenarios (§11.3). + ### 6.3 Server | Concern | Choice | Why | @@ -447,10 +458,17 @@ Cargo workspace (directory `server/`): | Crate | Kind | Contents | |---|---|---| -| `kvitt-domain` | library, no I/O | Money types, split, balance, allocation, settlement validation (§14.5), test-vector tests. Depends only on `serde`, `serde_json` (tests) and `uuid` | +| `kvitt-domain` | library, no I/O | Money types, split, balance, allocation, settlement validation (§14.5), `next_occurrence_date` and the day clamping of §9.5 (D-015), test-vector tests. Depends only on `serde`, `serde_json` (tests) and `uuid` | | `kvitt-server` | binary `kvitt-server` | Config, DB, migrations, HTTP API, auth, sync, audit, push, scheduler, pictures, APK, CLI subcommands (§10.4) | | `kvitt-import` | binary `kvitt-import` | Splitwise CSV importer (§10.8). Depends on `kvitt-domain` and reuses `kvitt-server` as a library for DB access and audit writing (`kvitt-server` exposes a `lib.rs`) | +**Server seams (D-015).** So that M3, M4 and M9 work in parallel on M2a's skeleton: + +* **Middleware in `router.rs` (M2a):** the `X-Kvitt-Protocol` check (426 `protocol_unsupported`), the `X-Kvitt-Server-Version` response header and the §8.6 body limits (1 MiB, 5 MiB for `PUT /me/picture`) are layers in `src/router.rs`. M3's rate limit (§8.6, AC-2.12) is a layer defined in `src/api/rate_limit.rs` and attached at one line in `router.rs` that M2a marks with a comment; that line is the only one M3 edits in `router.rs`. +* **Background tasks:** M2a creates `src/tasks/mod.rs` with a `start` function that `serve` calls once after migrations (initially doing nothing). M4 owns `src/tasks/**` and starts the scheduler, the push sender and the `sync_mutations` pruning from it. +* **Audit and push interfaces:** M2a defines the traits `AuditWriter` (in `src/audit/`) and `PushQueue` (in `src/push/`) with no-op implementations, and passes them through the application state. M4 replaces the implementations (audit writer and diff of §14.4.7, FCM queue of §14.6); M3 and M9 only call the traits. +* **Recurring generation:** M2a implements `next_occurrence_date` and the day clamping in `kvitt-domain` (§14.5.2). M3 writes the one function that generates the due periods of one template (§9.5: candidate periods, one transaction per candidate, audit through `AuditWriter`, push through `PushQueue`) in `src/api/recurring.rs`, used by E-28 and E-29; M4's scheduler calls that function for every template. M4 therefore lands after M3 (it may start in parallel and rebases on M3 before its gate). + ### 6.4 Why these choices * **Phone computes balances and analytics.** Offline is a hard requirement and the data set is small (six years of two-person history is a few thousand rows). Computing on the phone keeps the server dumb and the Pi idle. @@ -676,7 +694,7 @@ Consequences, stated so nobody "fixes" them: * The unique index on `(recurring_template_id, recurring_period)` makes generation idempotent; a conflict means the period was already generated and is skipped silently. * Pausing stops generation. Resuming does not backfill periods whose occurrence passed while paused: on resume the server sets `last_generated_period` to the latest period whose occurrence instant is before now, if that is later than the current value. * Changing `day_of_month` or `start_period` never regenerates past periods. -* `next_occurrence_date` (in the wire form) is the clamped date of the first candidate period after `last_generated_period` that is ≥ `start_period` and ≤ `end_period`; `null` when paused, deleted or past `end_period`. +* `next_occurrence_date` (in the wire form, computed with `kvitt-domain::next_occurrence_date`, §14.5.2) is the clamped date of the first candidate period after `last_generated_period` that is ≥ `start_period` and ≤ `end_period`; `null` when paused, deleted or past `end_period`. * A template's account may be archived (generation continues). Account deletion is blocked while a non-deleted template references it (AC-6.5). ### 9.6 Limits and validation @@ -774,7 +792,7 @@ When the phone has no validated network (Android `NET_CAPABILITY_VALIDATED`) or All infrastructure files live in `infra/` (ownership §14.7). Secrets live in `infra/secrets/` on the Mac (git-ignored) and in `/opt/kvitt/secrets/` on the Pi; neither is ever committed. -**Development first (D-001).** During development the server runs locally on the owner's M4 Mac (Docker compose from `infra/dev/compose.yaml`, or plain `cargo run` in `server/`), and debug builds pair against it over the home Wi-Fi (debug builds accept `http://` to private LAN addresses, AC-2.8). The development stack has the `server` service, an optional `caddy` with a self-signed certificate or no TLS for local use, and the `backup` job runnable on demand without the Drive upload. Firebase (§10.7) is set up during development because push is needed then. Everything in this section that concerns the Pi (the Pi hosts and router of §10.1, §10.2, the registry and Pi deploy of §10.3, §10.5 TLS, publishing to the Pi in §10.6, the Drive upload of §10.9) is built by milestone M2c-pi, which runs after M8 and before M10. +**Development first (D-001).** During development the server runs locally on the owner's M4 Mac (Docker compose from `infra/dev/compose.yaml`, or plain `cargo run` in `server/`), and debug builds pair against it over the home Wi-Fi (debug builds accept `http://` to private LAN addresses, AC-2.8). The development stack (built by M2c-dev, landed `c885aba`, D-022) has the `server` service and the `backup` job runnable on demand without the Drive upload; it has no Caddy and no TLS (plain `http://`, which debug builds accept on private LAN addresses). Its data is a bind-mounted folder on the Mac (`infra/dev/data/`, git-ignored, and `infra/dev/backups/` for local snapshots); it mounts the whole `infra/secrets/` folder read-only at `/secrets` instead of a single key file (a missing `fcm-service-account.json` means push is disabled, §10.4); its env file is `infra/dev/.env.example`; its `server` healthcheck is `kvitt-server healthcheck`; its `backup` runs as uid 10001 so that it can open the WAL files the server owns. The Pi's env file stays `infra/env.example`, written by M2c-pi. Firebase (§10.7) is set up during development because push is needed then. Everything in this section that concerns the Pi (the Pi hosts and router of §10.1, §10.2, the registry and Pi deploy of §10.3, §10.5 TLS, publishing to the Pi in §10.6, the Drive upload of §10.9) is built by milestone M2c-pi, which runs after M8 and before M10. ### 10.1 Hosts and network @@ -795,7 +813,7 @@ File `infra/compose.yaml`, deployed to `/opt/kvitt/compose.yaml` on the Pi with | Service | Image | Ports / volumes | Memory limit | Healthcheck | |---|---|---|---|---| | `caddy` | official `caddy` image, pinned by version and digest | publishes `8443:8443`; `./Caddyfile:/etc/caddy/Caddyfile:ro`; `certs:/certs:ro`; `caddy_data:/data` | 64 MiB | `wget -qO- http://localhost:2019/config/ >/dev/null` | -| `server` | `${KVITT_REGISTRY}/kvitt/server:${KVITT_TAG}` | `data:/data`; `/opt/kvitt/secrets/fcm-service-account.json:/secrets/fcm-service-account.json:ro`; no published port (listens on 8080 internally) | 160 MiB (budget: RSS under 100 MB in steady state) | `GET http://localhost:8080/health` via the binary's own `kvitt-server healthcheck` subcommand (no curl in the image) | +| `server` | `${KVITT_REGISTRY}/kvitt/server:${KVITT_TAG}` | `data:/data`; `/opt/kvitt/secrets/fcm-service-account.json:/secrets/fcm-service-account.json:ro`; `/opt/kvitt/import:/import:ro` (empty except during the one-off import, §10.8, D-025); no published port (listens on 8080 internally) | 160 MiB (budget: RSS under 100 MB in steady state) | `GET http://localhost:8080/health` via the binary's own `kvitt-server healthcheck` subcommand (no curl in the image) | | `cert-sync` | `${KVITT_REGISTRY}/kvitt/cert-sync:${KVITT_TAG}` | `certs:/certs`; `./Caddyfile:/etc/caddy/Caddyfile:ro`; `/opt/kvitt/secrets/ha_ssh_key:/secrets/ha_ssh_key:ro`; `/opt/kvitt/secrets/ha_known_hosts:/secrets/known_hosts:ro` | 32 MiB | both cert files exist and are non-empty | | `backup` | `${KVITT_REGISTRY}/kvitt/backup:${KVITT_TAG}` | `data:/data`; `backups:/backups`; `/opt/kvitt/secrets/rclone.conf:/config/rclone/rclone.conf:ro` | 96 MiB | last successful run marker newer than 26 hours, or no run yet since start | @@ -803,18 +821,19 @@ File `infra/compose.yaml`, deployed to `/opt/kvitt/compose.yaml` on the Pi with * `caddy` `depends_on` `cert-sync` with `condition: service_healthy` and `server` with `condition: service_started`. * Every service: `restart: unless-stopped`, logging driver `json-file` with `max-size: 10m`, `max-file: 3`, `TZ=Europe/Stockholm`. * One internal bridge network `kvitt`. +* The `cert-sync` and `backup` containers run as uid 1000. Their secret files on the Pi (`/opt/kvitt/secrets/ha_ssh_key`, `ha_known_hosts`, `rclone.conf`) have mode 0600 and are owned by uid 1000 (D-023). The FCM key keeps the mode of §10.7. The development stack runs `backup` as uid 10001 instead, because opening the WAL database needs access to the files the server owns; M2c-pi reconciles the two (uid 1000 with access to the `data` volume, or uid 10001 with the secrets owned to match) and records the choice in `infra/README.md`. `.env` keys (exhaustive): `KVITT_REGISTRY`, `KVITT_TAG`, `HA_HOST`, `HA_PORT`, `HA_USER`, `HA_CERT_DIR`, `RCLONE_REMOTE` (default `gdrive:`), `RCLONE_FOLDER_ID`. ### 10.3 Images, registry and build * Script `infra/build-images.sh [tag]` on the Mac: `docker buildx build --platform linux/arm64 --push` for the three Kvitt images, tag defaults to the short git SHA of `HEAD`, also tags `latest`. Refuses to run with a dirty working tree. -* `server` image: multi-stage. Stage 1 `rust:-bookworm` builds `kvitt-server` and `kvitt-import` in release mode with `--locked` for `aarch64-unknown-linux-gnu` (native on the M4; no `target-cpu=native`, the Pi 3 is a Cortex-A53). Stage 2 `gcr.io/distroless/cc-debian12:nonroot` (arm64) with the two binaries in `/usr/local/bin/`, user 10001, entrypoint `kvitt-server`, command `serve`. +* `server` image: multi-stage. Stage 1 `rust:-bookworm`, where `` is the exact Rust release pinned in `server/rust-toolchain.toml` (the current stable when M2a pins it; the two always change together), builds `kvitt-server` and `kvitt-import` in release mode with `--locked` for `aarch64-unknown-linux-gnu` (native on the M4; no `target-cpu=native`, the Pi 3 is a Cortex-A53). Stage 2 `gcr.io/distroless/cc-debian12:nonroot` (arm64) with the two binaries in `/usr/local/bin/`, user 10001, entrypoint `kvitt-server`, command `serve`. * `cert-sync` image: `alpine` with `openssh-client`, `curl` and a shell script `infra/cert-sync/sync.sh` run by `crond`. * `backup` image: `alpine` with `sqlite`, `gzip`, `tar`, `rclone` and `infra/backup/backup.sh` run by `crond`. * Script `infra/deploy.sh ` on the Mac: copies `compose.yaml` and `Caddyfile` to the Pi over SSH, sets `KVITT_TAG` in `/opt/kvitt/.env`, runs `docker compose pull && docker compose up -d`, waits for `server` to be healthy, prints `GET /health`. -* The Pi's Docker daemon must trust `gitea.lan` as an insecure (plain HTTP) registry (`insecure-registries`); M2c-pi writes the exact step. -* Later, images are built by Gitea Actions on a Pi from an example workflow the owner provides (`TODO(spec)` until provided, D-001). Until then `infra/build-images.sh` on the Mac pushes to `gitea.lan`. +* The Pi's Docker daemon must trust `gitea.lan` as an insecure (plain HTTP) registry (`insecure-registries`). Adding it is its own HUMAN step (sudo on the Pi); M2c-pi writes the exact step. +* Later, images are built by Gitea Actions on a Pi from an example workflow the owner provides (`TODO(spec)` until provided, D-001). The workflow file lives in `.gitea/workflows/` at the repository root, owned by M2c-pi (D-023). Until then `infra/build-images.sh` on the Mac pushes to `gitea.lan`. ### 10.4 Server configuration and CLI @@ -827,7 +846,7 @@ Environment variables (exhaustive): | `KVITT_PUBLIC_URL` | `https://icehack.asuscomm.com:8443` | Used in invite links and the landing page | | `KVITT_FCM_CREDENTIALS` | `/secrets/fcm-service-account.json` | Service-account key; if the file is missing, push is disabled and a warning is logged at start | | `KVITT_FCM_PROJECT_ID` | `kvitt-1337` | FCM project | -| `KVITT_TRUSTED_PROXY` | `caddy` | Host name whose resolved address may set `X-Forwarded-For` | +| `KVITT_TRUSTED_PROXY` | `caddy` | Host name whose resolved address may set `X-Forwarded-For`. The server must start when the name does not resolve (as on the development stack, which has no Caddy); then no peer is trusted and the peer address is the client IP (D-022) | | `RUST_LOG` | `info` | tracing filter | The time zone `Europe/Stockholm` is a constant in code, not configuration. @@ -846,7 +865,7 @@ Files under `KVITT_DATA_DIR`: | `bootstrap` | FR-1 | 0 ok, 2 already initialized | | `pair-device --user ` | AC-1.6 | 0 ok, 3 unknown user | | `rotate-epoch` | Sets a new random `sync_epoch` (§10.9) | 0 | -| `healthcheck` | Calls `http://127.0.0.1:8080/health`, exit 0 if 200 | 0 or 1 | +| `healthcheck` | Calls `http://127.0.0.1:8080/health`, exit 0 if 200; used by the compose healthchecks of both stacks (no curl in the image) | 0 or 1 | | `migrate` | Run migrations only | 0 | Run them on the Pi with `docker compose exec server kvitt-server ` (or `docker compose run --rm server ` when `server` is stopped). @@ -896,7 +915,13 @@ Used only if the certificate sync cannot be made to work. Documented in `infra/R ### 10.6 APK hosting and release * `version_code` = `git rev-list --count HEAD` on the commit being released; `version_name` = `YYYY.MM.DD-` of that commit. Both are passed to Gradle as properties `kvitt.versionCode` and `kvitt.versionName` by the release script; debug builds use `1` and `dev`. -* The release keystore is created once by the owner (HUMAN, §11) at `android/keystore/release.jks` (git-ignored); its passwords and alias are in `android/local.properties` keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias`, `kvitt.release.keyPassword`. Losing it means the phones must uninstall and reinstall (data comes back by sync). The owner keeps a copy in their password manager. +* The release keystore is created once by the owner (HUMAN, §11, D-024) on the Mac, in a directory outside the repository, with exactly: + + ``` + keytool -genkeypair -v -keystore kvitt-release.jks -alias kvitt -keyalg RSA -keysize 4096 -validity 10000 + ``` + + The file is never inside the repository or a worktree. `android/local.properties` holds its absolute path and secrets in the keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias` (`kvitt`), `kvitt.release.keyPassword`. Losing it means the phones must uninstall and reinstall (data comes back by sync). The owner keeps a copy in their password manager. * `infra/publish-apk.sh` on the Mac: builds `:app:assembleRelease` through `tools/gradle-serial.sh`, computes SHA-256 and size, copies the APK to the Pi's `data` volume as `apk/kvitt-.apk`, writes `apk/latest.json` atomically (write temp, rename): ```json @@ -913,17 +938,23 @@ Used only if the certificate sync cannot be made to work. Documented in `infra/R * The server reads `latest.json` on each `GET /apk/latest` (no cache) and serves `GET /apk/files/{file_name}` only for a `file_name` matching `^kvitt-[0-9]+\.apk$` that exists, with `Content-Type: application/vnd.android.package-archive`. * Older APKs stay on the Pi; the publish script keeps the newest 5 and deletes older ones. -### 10.7 Firebase setup (M2c-dev, with HUMAN sign-ins) +### 10.7 Firebase setup (M2c-dev, with a HUMAN sign-in) -1. HUMAN: `gcloud auth login` and `firebase login` on the Mac with the owner's Google account. +No new command-line tool is installed for this (D-022): `gcloud` exists on the Mac, the Firebase CLI is not used, and the Firebase project and Android apps are created through the Firebase Management REST API (`https://firebase.googleapis.com/v1beta1/`) with the access token of `gcloud auth print-access-token`. The agent confirms the exact requests from Google's official API reference at implementation time. + +1. HUMAN: `gcloud auth login` on the Mac with the owner's Google account. 2. `gcloud config set project kvitt-1337`; `gcloud services enable fcm.googleapis.com firebase.googleapis.com`. -3. `firebase projects:addfirebase kvitt-1337` (skip if already a Firebase project). -4. `firebase apps:create ANDROID "Kvitt" --package-name se.hagstedt.kvitt` and `firebase apps:create ANDROID "Kvitt debug" --package-name se.hagstedt.kvitt.debug`. -5. `firebase apps:sdkconfig ANDROID ` for each, merged into one `android/app/google-services.json` (git-ignored; the file holds both clients). +3. Add Firebase to the project (`projects.addFirebase`), skipped if it is already a Firebase project. +4. Create two Android apps (`projects.androidApps.create`): display name "Kvitt", package `se.hagstedt.kvitt`; display name "Kvitt debug", package `se.hagstedt.kvitt.debug`. +5. Fetch each app's config (`projects.androidApps.getConfig`) and merge them into one `google-services.json` holding both clients, written to `infra/secrets/google-services.json` in the main checkout (git-ignored). M8 copies it into `android/app/google-services.json` (git-ignored) with `infra/dev/copy-google-services.sh
`, written by M2c-dev. 6. `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 infra/secrets/fcm-service-account.json --iam-account kvitt-fcm@kvitt-1337.iam.gserviceaccount.com`. -7. Copy the key to `/opt/kvitt/secrets/fcm-service-account.json` on the Pi (mode 0440, group 10001). +7. Copy the key to `/opt/kvitt/secrets/fcm-service-account.json` on the Pi (mode 0440, group 10001). This step belongs to M2c-pi. 8. If an organisation policy blocks service-account key creation, stop and report; do not change org policies. +The agent may write these git-ignored files under `infra/secrets/` in the main checkout and act on the Google Cloud project `kvitt-1337` for steps 2 to 6 (D-022); nothing else outside the repository. + +State after M2c-dev (`c885aba`): steps 1 to 6 are done. Firebase is added to project `kvitt-1337`; Android apps exist for `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; the APIs `firebase.googleapis.com`, `fcm.googleapis.com`, `cloudresourcemanager.googleapis.com` and `iam.googleapis.com` are enabled; the service account is `kvitt-fcm@kvitt-1337.iam.gserviceaccount.com` with role `roles/firebasecloudmessaging.admin` (a role named `roles/firebasemessaging.admin` does not exist); `infra/secrets/fcm-service-account.json` and `infra/secrets/google-services.json` (both clients) exist in the main checkout. `infra/README.md` documents the REST calls for repeating or inspecting the setup. Step 7 remains for M2c-pi. + The FCM free tier is sufficient; no billing account is required. ### 10.8 Splitwise import @@ -947,7 +978,7 @@ kvitt-import --db /data/kvitt.sqlite --file --account "|||||||` where `` is the 0-based occurrence index of an identical line earlier in the same file. Rows whose key exists are skipped. * Every created row: `created_by_user_id` = `updated_by_user_id` = the owner; `updated_by_device_id` null; `created_at` = `updated_at` = the import time; one audit entry `create` with `source` = `import`; new `server_version`s from the global sequence; no pushes. * Description is truncated to 100 code points; the full original goes into `notes` when truncated. -* Category mapping (case-insensitive exact match on the `Category` column, English and Swedish names, D-003). The importer commits this table as a data file in its crate (PLAN-M9) and reads it from there; it is the only mapping. The Swedish names are those observed in the real Swedish export, each placed with its English Splitwise equivalent: +* Category mapping (case-insensitive exact match on the `Category` column, English and Swedish names, D-003). The importer commits this table as the data file `server/crates/kvitt-import/data/splitwise_categories.csv` (columns `splitwise_name,language,default_key`) and reads it from there; it is the only mapping. The Swedish names are those observed in the real Swedish export, each placed with its English Splitwise equivalent: | Splitwise category (English) | Splitwise category (Swedish) | Kvitt `default_key` | |---|---|---| @@ -965,6 +996,14 @@ kvitt-import --db /data/kvitt.sqlite --file --account " --account "" --user "…" --user "…"`, first with `--dry-run`, then without; `docker compose start server`; check `/health`. (`--entrypoint` is needed because the image's entrypoint is `kvitt-server`, §10.3.) +5. Delete the CSV files from `/opt/kvitt/import/`. + ### 10.9 Backups and restore Backup (`infra/backup/backup.sh`, daily 03:30 Europe/Stockholm, also runnable as `docker compose exec backup /backup.sh`): @@ -1004,7 +1043,7 @@ Each milestone is one plan (`PLAN-.md`, written by the orchestrator's plans ### 11.1 Dependency graph ``` -M0 spec ──┬─► U1 UX directions ──(owner picks)──► U2 UX design spec ──► M6 design system ──► M7a..M7f features ─┐ +M0 spec ──┬─► U1 directions ─(owner picks)─► U2 design spec ─► §9.7 colours ─► M6 design ─► M7a..M7f features ──┐ │ │ ├─► M1 contracts + vectors ─┬─► M2a server skeleton + domain ─┬─► M3 server core API ───────────┐ │ │ │ ├─► M4 server sync/audit/push ─┬──┤ │ @@ -1018,44 +1057,46 @@ M0 spec ──┬─► U1 UX directions ──(owner picks)──► U2 UX desi M11 import real history ◄── M9, M10 ``` -After M1, M2a and M2b land, these run in parallel without touching each other's files (§14.7): M3, M4, M5, M2c-dev, M6 (once U2 is approved), M7a to M7f (once M6 lands), M9 (once M4 lands). M2c-pi runs after M8 and before M10 (D-001). +After M1, M2a and M2b land, these run in parallel without touching each other's files (§14.7): M3, M4 (lands after M3, D-015), M5, M2c-dev, M6 (once U2 is approved and the §9.7 colour change from U2's proposal has landed in this spec), M7a to M7f (once M6 lands), M9 (once M4 lands). M2c-pi runs after M8 and before M10 (D-001). ### 11.2 Milestone table | Id | Name | Deliverable | Gate | HUMAN | Depends on | |---|---|---|---|---|---| | M0 | Spec v0.1 | `SPEC.md`, filled `CLAUDE.md`, repository bootstrap | none (docs only) | Review the spec; answer §12 | none | -| U1 | UX directions | §13.4 phase 1 files in `docs/ux/` | Files exist and open; every screen of §13.3 appears in each direction; Swedish copy, no placeholder text | Review 2-3 directions, pick one, give comments | M0 | -| U2 | UX design spec | §13.4 phase 2 files in `docs/ux/` | Every string key has `en` and `sv`; every screen has all states of §13.3 | Approve the design spec | U1 and the owner's pick | +| U1 | UX directions | §13.4 phase 1 files in `docs/ux/` | Files exist and open; each direction shows the six phase-1 screens of §13.4 (S-4, S-7, S-5, S-9, S-11, S-8) in light and dark; Swedish copy, no placeholder text | Review 2-3 directions, pick one, give comments | M0 | +| U2 | UX design spec | §13.4 phase 2 files in `docs/ux/` | Every string key has `en` and `sv`; every screen has every state that `design-spec.md` lists for it (§13.3) | Approve the design spec | U1 and the owner's pick | | M1 | Contracts and vectors | `contracts/openapi.yaml`, `contracts/schema/server-v1.sql`, `contracts/test-vectors/*.json`, `contracts/examples/**/*.json`, `tools/contracts-check.py` | `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 | none | M0 | -| M2a | Server skeleton and `kvitt-domain` | Cargo workspace, three crates, config, DB pool, migration `0001_init.sql` byte-identical to `contracts/schema/server-v1.sql`, error type with every §14.3.2 code, auth extractor (§8.2), all routes of §7 registered with handlers returning 501 `not_implemented`, utoipa document equal to `contracts/openapi.yaml`, `GET /health` and `GET /setup/status` implemented, `kvitt-domain` passing all vectors, `server/Dockerfile` | Server gate (includes the OpenAPI match test, the migration identity test, the vector tests) | none | M1 | -| M2b | Android skeleton and `:domain` | Gradle project in `android/` with every module of §6.2, version catalog, architecture check, `:core:model` types and repository interfaces of §14.2.4, `:domain` passing all vectors (including analytics), `:core:fakes` with an empty scenario for each repository, `:app` launching to an empty themed screen, string resource files for both languages, lint rule for missing translations | Android gate | Plug the phone in once to confirm `installDebug` works | M1 | -| M2c-dev | Infra for development (D-001) | `infra/dev/compose.yaml` (`server` built from `server/Dockerfile`, optional `caddy` with a self-signed certificate or no TLS, `backup` runnable on demand), `infra/dev/README.md` (start, stop, bootstrap, pairing a debug build over the home Wi-Fi), `infra/backup/{Dockerfile, backup.sh}` (Drive upload skipped when `RCLONE_FOLDER_ID` is empty), `infra/env.example`, Firebase project and apps (§10.7), service-account key in `infra/secrets/`, `google-services.json` in `android/app/` | `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` | `gcloud auth login`, `firebase login` | M0 (uses the M2a image for the health check when available) | -| M2c-pi | Infra on the Pi (D-001) | `infra/` Pi compose stack, Caddyfile, cert-sync image, build, deploy and publish scripts, `infra/README.md` (Pi provisioning, router forwarding, trust of the `gitea.lan` registry), `infra/RESTORE.md`, Drive backups to folder id `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV`, Gitea Actions image build from the owner's example workflow | `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 | 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"` | M2c-dev, M8 | -| M3 | Server core API | E-2 to E-30 and E-33 implemented (§14.3), invite landing page, pictures, rate limiting, `bootstrap`, `pair-device` | Server gate; integration tests per endpoint and per error code listed for it | none | M2a | -| M4 | Server sync, audit, push, scheduler | E-31, E-32, audit writer and diff, `sync_mutations` pruning, FCM sender, scheduler, `rotate-epoch` | 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 | none | M2a | +| M2a | Server skeleton and `kvitt-domain` | Cargo workspace, three crates, config, DB pool, migration `0001_init.sql` byte-identical to `contracts/schema/server-v1.sql`, error type with every §14.3.2 code, auth extractor (§8.2), all routes of §7 registered with handlers returning 501 `not_implemented`, utoipa document equal to `contracts/openapi.yaml`, `GET /health` and `GET /setup/status` implemented, `kvitt-domain` passing all vectors plus `next_occurrence_date`, `server/Dockerfile`; shell files in M3, M4 and M9 paths (D-004); the seams of §6.3 (D-015) | Server gate (includes the OpenAPI match test, the migration identity test, the vector tests) | none | M1 | +| M2b | Android skeleton and `:domain` | Gradle project in `android/` with every module of §6.2, version catalog, architecture check, `:core:model` types and repository interfaces of §14.2.4, `:domain` passing all vectors (including analytics), `:core:fakes` with the empty scenario (§6.2, D-018) for each repository, module shells for every module with one empty entry composable per feature (D-004), the feature convention of §6.2 in `build-logic` (D-017), `:app` launching to an empty themed screen, `:app` string files with the app name only in both languages, lint `MissingTranslation` as an error in every module | Android gate | Plug the phone in once to confirm `installDebug` works | M1 | +| M2c-dev | Infra for development (D-001, D-022) | Landed `c885aba`. `infra/dev/compose.yaml` (`server` built from `server/Dockerfile`, no Caddy, no TLS, bind-mounted data, `infra/secrets/` mounted read-only at `/secrets`, `backup` runnable on demand), `infra/dev/.env.example`, the dev scripts in `infra/dev/` (build, up, logs, backup, `copy-google-services.sh`), `infra/README.md` (dev stack and Firebase), `infra/backup/{Dockerfile, backup.sh}` (Drive upload skipped when `RCLONE_FOLDER_ID` is empty), Firebase project and apps through the REST API (§10.7), service-account key and `google-services.json` in `infra/secrets/` | `docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config` succeeds; `shellcheck $(find infra -name '*.sh')` clean; on the Mac (with the M2a image): `GET /health` answers 200 from the Mac; one local backup run produces a snapshot that passes `PRAGMA integrity_check` | `gcloud auth login`; on the phone on the home Wi-Fi, open the dev server's `/health` and answer yes/no that it shows `"status":"ok"` | M0 (uses the M2a image for the health check when available) | +| M2c-pi | Infra on the Pi (D-001) | `infra/` Pi compose stack, Caddyfile, cert-sync image, build, deploy and publish scripts, `infra/README.md` (Pi provisioning, router forwarding, trust of the `gitea.lan` registry), `infra/RESTORE.md`, Drive backups to folder id `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV`, Gitea Actions image build from the owner's example workflow in `.gitea/workflows/`, the `/opt/kvitt/import` bind mount (§10.2) | `docker compose -f infra/compose.yaml config` succeeds; `shellcheck $(find infra -name '*.sh')` clean; 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 | Answer OQ-3; grant at the start of the sprint the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive (D-023); set up key-based SSH from the Mac to the Kvitt Pi; provide the example Gitea Actions workflow and the Gitea credentials; install Docker on the Pi if missing; add `gitea.lan` to `insecure-registries` on the Pi; set the router forward 8443 → Kvitt Pi; add the cert-sync public key to the Home Assistant SSH add-on; `rclone authorize "drive"` | M2c-dev, M8 | +| M3 | Server core API | E-3 to E-30 and E-33 implemented (§14.3; E-2 is M2a's, M3 tests it), invite landing page, pictures, rate limiting (`src/api/rate_limit.rs`), `bootstrap`, `pair-device`, the recurring generation function of §6.3 (D-015) | Server gate; integration tests per endpoint and per error code listed for it; every generation case of §9.5 (catch-up, clamping, split resolution, idempotency, resume without backfill, `next_occurrence_date`) | none | M2a | +| M4 | Server sync, audit, push, scheduler | E-31, E-32, the real `AuditWriter` (writer and diff) and `PushQueue` (FCM sender), `src/tasks/**` starting them, `sync_mutations` pruning daily at 04:00 (D-021), scheduler, `rotate-epoch` | Server gate; integration tests for every sync rule of §14.4 and the scheduler cases of §9.5 (run at start, daily 06:00 on DST days, catch-up through M3's generation function); FCM sender tested against a local stub HTTP server | none | M2a; lands after M3 (D-015) | | M5 | Android data layer | Room DB v1 (§14.2) with exported schema, DAOs, Ktor client and DTOs, token store, sync engine and workers, real repositories, APK updater | 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) | none | M1, M2b | -| M6 | Design system | `:core:design` theme, tokens, typography, shared components, charts, formatters (§9.8) per `docs/ux/design-spec.md`, screenshot tests of every component in light and dark, Swedish and English | Android gate | Look at the component screenshots the agent opens and posts (yes/no) | U2, M2b | +| M6 | Design system | `:core:design` theme, tokens, typography (Material 3 default typeface, D-020), shared components including the ledger rows and expense editor fields (D-019), charts, formatters (§9.8), shared strings (including the notification copy) per `docs/ux/design-spec.md` and `docs/ux/strings.csv`, screenshot tests of every component in light and dark, Swedish and English | Android gate | Look at the component screenshots the agent opens and posts (yes/no) | U2, M2b, the §9.7 colour change | | M7a | Feature: onboarding | `:feature:onboarding` screens S-1, S-2, S-3, S-18 | Android gate with screenshot tests of every state, sv and en, light and dark | none | M6 | | M7b | Feature: home and account | `:feature:home`, `:feature:account` screens S-4, S-5, S-6, S-16, S-17 | as M7a | none | M6 | | M7c | Feature: expense | `:feature:expense` screens S-7, S-8 | as M7a | none | M6 | | M7d | Feature: settle | `:feature:settle` screens S-9, S-10 | as M7a | none | M6 | | M7e | Feature: analytics | `:feature:analytics` screens S-11, S-12 | as M7a | none | M6 | | M7f | Feature: settings | `:feature:settings` screens S-13, S-14, S-15, S-19, S-20, S-21, S-22 | as M7a | none | M6 | -| M8 | App integration | `:app` navigation and deep links, Hilt bindings from interfaces to `:data` and `:domain`, `:adapter:fcm`, notification building (AC-13.5), update banner wiring, revoke wipe | Android gate; on the owner's phone against the development server on the Mac (D-001): pair, add an expense offline, reconnect, see it synced | Tap through the pairing and the offline test as relayed by the orchestrator | M3, M4, M5, M7a-M7f, M2c-dev (Firebase, development server) | +| M8 | App integration | `:app` navigation and deep links, Hilt bindings from interfaces to `:data` and `:domain`, `:adapter:fcm`, notification building (AC-13.5), update banner wiring, revoke wipe, network security config (§8.5), manifest hardening (§8.8), release signing and version properties (§10.6), `google-services.json` copied with `infra/dev/copy-google-services.sh` | Android gate; on the owner's phone against the development server on the Mac (D-001): pair, add an expense offline, reconnect, see it synced | Tap through the pairing and the offline test as relayed by the orchestrator | M3, M4, M5, M7a-M7f, M2c-dev (Firebase, development server) | | M9 | Splitwise importer | `kvitt-import` per §10.8 with tests over synthetic CSV fixtures | Server gate | Provide the CSV files (kept outside git) when OQ-2 is answered | M2a, M4 | -| M10 | First release | Release build signed, published, both phones paired, push working both ways, nightly backup on Drive, restore drill report, memory check | `docs/validation/first-release.md` and `docs/validation/restore-drill.md` complete; server RSS under 100 MB | 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 | M2c-pi, M3, M4, M8 | -| M11 | Import history | Real import run, `docs/validation/splitwise-import.md` with per-account totals compared to Splitwise's balances (aggregates only) | Imported balances equal Splitwise's "Total balance" rows per group | Provide the CSVs; confirm the balances on the phone match Splitwise | M9, M10 | +| M10 | First release | Release build signed, published, both phones paired, push working both ways, nightly backup on Drive, restore drill report, memory check | `docs/validation/first-release.md` and `docs/validation/restore-drill.md` complete; server RSS under 100 MB | Create the release keystore with the `keytool` command of §10.6, outside the repository; install the APK on both phones from the landing page; allow "Install unknown apps"; partner pairs with the invite; confirm notifications arrive | M2c-pi, M3, M4, M8 | +| M11 | Import history | Real import run on the Pi per §10.8 "Production run" (D-025), `docs/validation/splitwise-import.md` with per-account totals compared to Splitwise's balances (aggregates only) | Imported balances equal Splitwise's "Total balance" rows per group | Provide the CSVs; confirm the balances on the phone match Splitwise | M9, M10 | ### 11.3 Notes per milestone -* **M1** writes the OpenAPI file by hand from §14.3 (no code exists yet). M2a then annotates handlers so the generated document matches it exactly; M2a may only change `contracts/openapi.yaml` for representational differences that do not change meaning (key order, `$ref` naming), recorded in the plan report. Any semantic change follows the contract-change rule (§14.8). -* **M2a** owns the router and registers every route up front so that M3 and M4 only fill in handler bodies in their own files. The same for the utoipa `ApiDoc`. -* **M2b** writes the `*State` classes and repository interfaces of §14.2.4 so that M5 and M7 work against the same interfaces in parallel. -* **M7x** features use `:core:fakes` only; they never depend on `:data`. Each feature agent adds its fake scenarios in `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes//` and nowhere else in that module. +* **M1** writes the OpenAPI file by hand from §14.3 (no code exists yet). M2a then annotates handlers so the generated document matches it; the equality test compares parsed values, not text (D-016). M2a may change `contracts/openapi.yaml` only representationally (key order, `$ref` naming, description wording), listed in the plan report, with no spec bump (§14.8). Any semantic change follows the contract-change rule (§14.8). +* **M2a** owns the router and registers every route up front so that M3 and M4 only fill in handler bodies in their own files. The same for the utoipa `ApiDoc`. M2a creates the shell files of the later server milestones (D-004, §14.7) and the seams of §6.3 (D-015). `server/rust-toolchain.toml` pins the exact current stable Rust release and the Dockerfile uses the same version (§10.3). +* **M2b** writes the `*State` classes and repository interfaces of §14.2.4 so that M5 and M7 work against the same interfaces in parallel. It creates a module shell for every module of §6.2, with one empty entry composable per feature module (D-004), puts the feature convention of §6.2 (D-017) into the feature convention plugin, and fixes how `resolveRange` signals an invalid `CUSTOM` range (§14.5.4, D-018). Its screenshot-test choice becomes D-010. +* **M7x** features use `:core:fakes` only; they never depend on `:data`. Each feature agent adds its fake scenarios in `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes//` and nowhere else in that module. Each fills in its own module shell from M2b, follows the feature convention of §6.2 and takes shared UI from `:core:design` (D-019). * **M2c-dev / M2c-pi** (D-001): M2c-dev builds only what development needs on the Mac and runs now; M2c-pi builds everything that concerns the Pi (registry trust, TLS, router, cert-sync, Pi deploy, APK publishing to the Pi, Drive upload, Gitea Actions) and runs after M8, before M10. -* **M8** is the only plan that edits `:app` after M2b. -* **M10**: the orchestrator relays every HUMAN step in chat. +* **M8** is the only plan that edits `:app` after M2b. It owns the `:app` items network security config (§8.5), manifest hardening (§8.8), release signing and version properties (§10.6), and copies `google-services.json` from `infra/secrets/` with `infra/dev/copy-google-services.sh` (§10.7). +* **M10**: the orchestrator relays every HUMAN step in chat. The keystore is created with the command of §10.6 (D-024). +* **M2c-pi**: the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive are requested from the owner at the start of that sprint (D-023). +* **M11** runs the import per §10.8 "Production run" (D-025). --- @@ -1097,7 +1138,7 @@ A brief for one UX agent (Opus) who has read this spec and nothing else. It runs ### 13.3 Screens and what each must show -Each screen must be designed in these states where they apply: **loaded**, **empty**, **offline** (with pending changes), **loading** (first sync only; afterwards data is local), **error** (a failed online action), and, for lists, **long content** (long Swedish names, 7-digit amounts). +Each screen must be designed in these states where they apply: **loaded**, **empty**, **offline** (with pending changes), **loading** (first sync only; afterwards data is local), **error** (a failed online action), and, for lists, **long content** (long Swedish names, 7-digit amounts). Which states apply to which screen is listed per screen in U2's `docs/ux/design-spec.md`; that list is the binding one for U2's gate and for the feature milestones' screenshot tests. | Id | Screen | Must show | Actions | |---|---|---|---| @@ -1140,12 +1181,21 @@ System notifications (copy needed in both languages, built per AC-13.5): The UX agent writes the Swedish versions. +Invite landing page (E-4, AC-2.5). Server-rendered HTML, so these strings live in the server (M3's `src/api/landing.rs`), not in the app's resources: + +| Element | English | Swedish | +|---|---|---| +| App name | Kvitt | Kvitt | +| Download button | Download Kvitt | Ladda ner Kvitt | +| Open button | Open in Kvitt | Öppna i Kvitt | +| Instruction sentence | Install Kvitt, then open this link again on your phone. | Installera Kvitt och öppna sedan den här länken igen på din telefon. | + ### 13.4 Deliverables **Phase 1 (U1): directions.** In `docs/ux/directions/`: * 2 or 3 visual directions, each a self-contained HTML file `docs/ux/directions/-/index.html` (no external requests except Google Fonts, if a direction uses a font whose licence allows bundling in an app; the font must then also be bundleable as an Android font resource) rendering at a 411 dp wide phone frame: S-4 Home, S-7 Expense editor, S-5 Account detail, S-9 Settle up, S-11 Analytics, S-8 Expense detail with timeline, each in light and dark, in Swedish, with realistic household data (names "Jacob" and "Anna", Swedish shops and amounts; no lorem ipsum). -* Each direction states: name and one-line idea; colour palette (primary, secondary, tertiary, surfaces, error, "owed" and "owing" semantic colours, the 12 category colours) in light and dark with contrast ratios; typography (Material 3 type scale with chosen typeface and why; tabular figures for amounts); shape (corner radii per component size); elevation and motion principles; iconography (Material Symbols style: outlined, rounded or sharp, weight, fill rules); how the balance header looks in its three states; how it handles 200 % font scale (one screenshot). +* Each direction states: name and one-line idea; colour palette (primary, secondary, tertiary, surfaces, error, "owed" and "owing" semantic colours, the 12 category colours) in light and dark with contrast ratios; typography (Material 3 type scale and why; tabular figures for amounts; the app bundles no font and uses the Material 3 default typeface, D-020, so a direction that previews another typeface has it replaced by the default in U2's design spec); shape (corner radii per component size); elevation and motion principles; iconography (Material Symbols style: outlined, rounded or sharp, weight, fill rules); how the balance header looks in its three states; how it handles 200 % font scale (one screenshot). * `docs/ux/directions/README.md`: a comparison table of the directions (character, strengths, risks, implementation cost in Compose), and the UX agent's recommendation with reasons. * PNG exports of every screen of every direction (`docs/ux/directions/-/png/`), so the orchestrator can show them to the owner without a browser. @@ -1158,7 +1208,7 @@ Suggested starting points the UX agent may use, replace or merge (it must delive **Phase 2 (U2): design spec for the chosen direction.** After the owner picks (and comments), in `docs/ux/`: * `design-spec.md`: final tokens (colour roles light and dark, category colour hex values, typography scale, shapes, spacing scale in dp, elevation, motion durations and easing), component inventory (each with anatomy, states, sizes), and for every screen S-1 to S-22: layout description, all states of §13.3, interaction details, TalkBack labels. -* `strings.csv`: every user-visible string of the app with columns `key,en,sv,notes`, keys in `snake_case` prefixed by screen (`home_balance_owes_you`), placeholders as Android format arguments (`%1$s`), plurals marked in `notes` with their quantities. This becomes `values/strings.xml` and `values-sv/strings.xml` in M6 and M7; feature agents add no strings that are not in this file without a decisions entry. +* `strings.csv`: every user-visible string of the app with columns `key,en,sv,notes`, keys in `snake_case` prefixed by the feature module that holds them (`home_balance_owes_you`, `expense_save`; shared strings for `:core:design` never start with a feature name; the app name is the only `:app` string; §6.2, D-017), placeholders as Android format arguments (`%1$s`), plurals marked in `notes` with their quantities. This becomes `values/strings.xml` and `values-sv/strings.xml` in M6 and M7; feature agents add no strings that are not in this file without a decisions entry. * `screens/-.png` for every screen and state, light and dark, Swedish. * Updated colour hex values for §9.7, proposed as a spec change (decisions entry, spec version bump) by the orchestrator. @@ -1415,7 +1465,7 @@ Server-side rules on top of the DDL: * At most two rows in `users` (checked in code; `household_full`). * `meta.sync_epoch` is replaced by `kvitt-server rotate-epoch` with a new `lower(hex(randomblob(16)))`. * No row of `users`, `devices`, `accounts`, `categories`, `recurring_templates`, `expenses`, `settlements` or `audit_log` is ever hard-deleted. Child rows (`account_default_shares`, `recurring_template_splits`, `expense_splits`, `settlement_allocations`) are replaced (delete and insert) when their parent is updated. -* `invites` rows are never deleted; `sync_mutations` rows are pruned after 90 days. +* `invites` rows are never deleted; `sync_mutations` rows are pruned after 90 days by a daily job at 04:00 Europe/Stockholm (DST-aware, always local 04:00) that deletes rows whose `received_at` is older than 90 days (D-021). ### 14.2 Room entities (phone) @@ -2048,7 +2098,7 @@ The phone renders `changes` with ids resolved to names (FR-12). | Periodic background sync | 15 minutes (WorkManager minimum), requires a connected network | | FCM-triggered sync | expedited one-off work | | Retry backoff | exponential from 30 seconds (WorkManager) | -| `sync_mutations` retention | 90 days | +| `sync_mutations` retention | 90 days, pruned daily at 04:00 Europe/Stockholm (D-021) | | Tombstone retention | forever | | Token lifetime | until revoked | | Notification sync wait (AC-13.5) | 20 seconds | @@ -2057,7 +2107,7 @@ With these values a phone offline for 14 days loses nothing: tokens do not expir ### 14.5 Domain rules as pure functions -Implemented twice: Kotlin in `:domain` (package `se.hagstedt.kvitt.domain`, input and output types in `:core:model` package `se.hagstedt.kvitt.model.money`) and Rust in `kvitt-domain`. Both are pure (no I/O, no clock, no randomness, no locale) and must pass every vector in `contracts/test-vectors/`. Analytics exists only in Kotlin. The rules themselves are §9.1, §9.3 and §9.4; this section fixes signatures, error tokens and the vector format. +Implemented twice: Kotlin in `:domain` (package `se.hagstedt.kvitt.domain`, input and output types in `:core:model` package `se.hagstedt.kvitt.model.money`) and Rust in `kvitt-domain`. Both are pure (no I/O, no clock, no randomness, no locale) and must pass every vector in `contracts/test-vectors/`. Analytics exists only in Kotlin; `next_occurrence_date` and the day clamping exist only in Rust (D-015). The rules themselves are §9.1, §9.3 and §9.4; this section fixes signatures, error tokens and the vector format. #### 14.5.1 Types @@ -2120,6 +2170,7 @@ pub enum SettlementError { AmountOutOfRange, SameUser, UserUnknown, NoAllocation | Proposal | `fun proposeAllocation(amountOre: Long, accounts: List): AllocationResult` | `pub fn propose_allocation(amount_ore: i64, accounts: &[AccountPosition]) -> Result` | | Settlement check | `fun validateSettlement(amountOre: Long, fromUserId: String, toUserId: String, userIds: List, allocations: List): SettlementError?` | `pub fn validate_settlement(amount_ore: i64, from_user_id: &str, to_user_id: &str, user_ids: &[String], allocations: &[Allocation]) -> Result<(), SettlementError>` | | Over-settle warnings | `fun overSettledAccounts(allocations: List, accounts: List): List` | `pub fn over_settled_accounts(allocations: &[Allocation], accounts: &[AccountPosition]) -> Vec` | +| Recurring dates (D-015) | none | `next_occurrence_date` and the clamped occurrence date of a period (module `recurring.rs`); M2a fixes the exact signatures and lists them in its report | Semantics beyond §9: @@ -2128,6 +2179,7 @@ Semantics beyond §9: * `proposeAllocation`: `amountOre` outside 1 to 1 000 000 000 (§9.6) is `amount_out_of_range`. `accounts` are all non-deleted accounts with the payer's net per account (callers compute nets with `computeBalances`, excluding the settlement being edited). Errors in enum order. Allocations are in the order produced by §9.4. `overSettledAccountIds` lists the account that received `excessOre` (empty when `excessOre` is 0). * `validateSettlement`: errors in enum order; `null` / `Ok(())` when valid. Reference existence is not checked here (the server checks it, §14.4.4). * `overSettledAccounts`: ids, in allocation order, whose allocation exceeds `max(0, −payerNetOre)`; an allocation for an account not in `accounts` counts as net 0. +* Recurring dates (Rust only): the clamped date of period `YYYY-MM` for `day_of_month` 1 to 31 is that day, or the last day of the month when the month is shorter (§9.5, AC-9.2). `next_occurrence_date` takes the template's `day_of_month`, `start_period`, `end_period`, `last_generated_period`, `paused` and deleted state and returns the date defined in §9.5 (`null` when paused, deleted or past `end_period`). Pure: no clock, no time zone. No vector file covers them; M2a's unit tests cover every clamping case of AC-9.2 and the `null` cases. #### 14.5.3 Error tokens on the wire and in vectors @@ -2135,6 +2187,8 @@ Snake-case of the enum names: `amount_out_of_range`, `users_invalid`, `payer_unk #### 14.5.4 Analytics (Kotlin only) +Types in `:core:model`, package `se.hagstedt.kvitt.model.analytics` (D-017); functions in `:domain`. + ```kotlin data class AnalyticsExpense(val date: LocalDate, val accountId: String, val categoryId: String, val amountOre: Long, val shares: Map) data class DateRange(val start: LocalDate, val endInclusive: LocalDate) @@ -2151,7 +2205,7 @@ fun resolveRange(preset: RangePreset, today: LocalDate, earliestExpense: LocalDa (`LocalDate` is `kotlinx.datetime.LocalDate`.) * `spendSeries`: expenses outside the range are ignored. Buckets cover the range completely, in order, including empty ones. Month keys `YYYY-MM`, week keys `YYYY-Www` (ISO week-numbering year, week 1 contains the first Thursday). A bucket's `start`/`endInclusive` are clipped to the range. `totals` maps group id (category id or account id) to öre, omitting zero groups; `total` is their sum. Measure `Household` adds `amountOre`; `Share(u)` adds `shares[u]` (0 if absent). `groupTotals` sums over all buckets, omitting zeros. -* `resolveRange`: `THIS_MONTH` = first of the current month to `today`; `LAST_3_MONTHS`, `LAST_6_MONTHS`, `LAST_12_MONTHS` = first day of the month N−1 months before the current month to `today`; `THIS_YEAR` = 1 January to `today`; `ALL_TIME` = `earliestExpense` (or `today` when null) to `today`; `CUSTOM` = the custom dates, swapped if start > end. Expenses dated after `today` are therefore excluded from presets; a custom range may include future dates. `CUSTOM` with a null `customStart` or `customEnd` is an error, `validation_failed` (no vector covers it; how the function signals it given its `DateRange` return type is fixed by M2b and recorded in its report); `ALL_TIME` with `earliestExpense` after `today` gives the range `today`..`today`. +* `resolveRange`: `THIS_MONTH` = first of the current month to `today`; `LAST_3_MONTHS`, `LAST_6_MONTHS`, `LAST_12_MONTHS` = first day of the month N−1 months before the current month to `today`; `THIS_YEAR` = 1 January to `today`; `ALL_TIME` = `earliestExpense` (or `today` when null) to `today`; `CUSTOM` = the custom dates, swapped if start > end. Expenses dated after `today` are therefore excluded from presets; a custom range may include future dates. `CUSTOM` with a null `customStart` or `customEnd` throws an exception of a typed subclass of `IllegalArgumentException` declared in `:domain` (M2b names it); the caller (the `AnalyticsRepository` implementation in `:data`, and its fake) catches it and maps it to `validation_failed` (D-018; no vector covers it); `ALL_TIME` with `earliestExpense` after `today` gives the range `today`..`today`. #### 14.5.5 Test-vector format @@ -2247,6 +2301,7 @@ Repository layout (paths are binding): ``` SPEC.md CLAUDE.md README-agents.md PLAN-*.md +.gitea/workflows/ (Gitea Actions image build, M2c-pi) contracts/ openapi.yaml schema/server-v1.sql @@ -2257,17 +2312,16 @@ prompts/ tools/ gradle-serial.sh clean-builds.sh install-git-hooks.sh git-hooks/ dependency-report/ contracts-check.py server/ Cargo.toml (workspace) Cargo.lock Dockerfile rust-toolchain.toml - crates/kvitt-domain/ src/{lib.rs, split.rs, balance.rs, allocation.rs, settlement.rs} tests/vectors.rs + crates/kvitt-domain/ src/{lib.rs, split.rs, balance.rs, allocation.rs, settlement.rs, recurring.rs} tests/vectors.rs crates/kvitt-server/ migrations/0001_init.sql src/{main.rs, lib.rs, config.rs, error.rs, router.rs, openapi.rs, auth.rs, time.rs} src/db/{mod.rs, pool.rs, sequence.rs} src/wire/{mod.rs, user.rs, device.rs, invite.rs, account.rs, category.rs, expense.rs, settlement.rs, recurring.rs, audit.rs, sync.rs} - src/api/{mod.rs, health.rs, setup.rs, invites.rs, landing.rs, me.rs, pictures.rs, devices.rs, accounts.rs, categories.rs, recurring.rs, proposal.rs, apk.rs, sync.rs} - src/ratelimit.rs - src/sync/ src/audit/ src/push/ src/scheduler/ + src/api/{mod.rs, health.rs, setup.rs, invites.rs, landing.rs, me.rs, pictures.rs, devices.rs, accounts.rs, categories.rs, recurring.rs, proposal.rs, apk.rs, sync.rs, rate_limit.rs} + src/tasks/ src/sync/ src/audit/ src/push/ src/scheduler/ src/cli/{mod.rs, bootstrap.rs, pair_device.rs, rotate_epoch.rs, healthcheck.rs} tests/ - crates/kvitt-import/ src/ tests/fixtures/ + crates/kvitt-import/ src/ data/splitwise_categories.csv tests/fixtures/ android/ settings.gradle.kts build.gradle.kts gradle.properties gradle/{libs.versions.toml, gradle-daemon-jvm.properties, wrapper/} build-logic/ (convention plugins, architecture check) @@ -2276,6 +2330,7 @@ android/ app/ infra/ compose.yaml Caddyfile env.example build-images.sh deploy.sh publish-apk.sh README.md RESTORE.md + .shellcheckrc dev/{compose.yaml, .env.example, lib.sh, build.sh, up.sh, logs.sh, backup.sh, copy-google-services.sh} dev/data/, dev/backups/ (git-ignored) cert-sync/{Dockerfile, sync.sh} backup/{Dockerfile, backup.sh} secrets/ (git-ignored) ``` @@ -2285,12 +2340,12 @@ Ownership (an agent edits only paths its milestone owns; anything else needs the |---|---|---| | `SPEC.md`, `CLAUDE.md`, `README-agents.md`, `docs/HANDOFF.md`, `docs/decisions.md`, `PLAN-*.md`, `prompts/`, `tools/` (except `tools/contracts-check.py`) | docs agent briefed by the orchestrator | same | | `contracts/**`, `tools/contracts-check.py` | M1 | contract-change plans only (§14.8) | -| `server/Cargo.toml`, `server/rust-toolchain.toml`, `server/Dockerfile`, `server/crates/kvitt-domain/**` | M2a | 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` | M2a | contract-change plans; M3/M4 may add one line per new module to `mod.rs` files they need | -| `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` | M3 | later server plans | -| `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` | M4 | later server plans | -| `server/crates/kvitt-import/**` | M9 | same | -| `android/settings.gradle.kts`, `android/build.gradle.kts`, `android/gradle.properties`, `android/gradle/**`, `android/build-logic/**` | M2b | later plans may add catalog entries and module includes (rebase resolves) | +| `server/Cargo.toml`, `server/Cargo.lock`, every crate's `Cargo.toml`, `server/rust-toolchain.toml`, `server/Dockerfile`, `server/crates/kvitt-domain/**` | M2a | contract-change plans only for `kvitt-domain`; M3, M4 and M9 may add dependency lines 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` (including the `serve` and `migrate` code), `src/db/**`, `src/wire/**`, `src/api/{mod,health,setup}.rs`, `src/cli/{mod,healthcheck}.rs`, M2a's own tests (any `tests/*.rs` without the prefixes `core_`, `sync_`, `scheduler_`, `push_`) | M2a | contract-change plans; M3/M4 may add one line per new module to `mod.rs` files they need; M3 edits the one marked rate-limit line in `router.rs` (D-015) | +| `kvitt-server`: `src/api/{invites,landing,me,pictures,devices,accounts,categories,recurring,proposal,apk,rate_limit}.rs`, `src/cli/{bootstrap,pair_device}.rs`, `tests/core_*.rs` | M3 (M2a creates the shells, D-004) | later server plans | +| `kvitt-server`: `src/api/sync.rs`, `src/tasks/**`, `src/sync/**`, `src/audit/**`, `src/push/**`, `src/scheduler/**`, `src/cli/rotate_epoch.rs`, `tests/sync_*.rs`, `tests/scheduler_*.rs`, `tests/push_*.rs` | M4 (M2a creates the shells, the `tasks::start` stub and the `AuditWriter`/`PushQueue` traits with no-op implementations, D-004, D-015) | later server plans | +| `server/crates/kvitt-import/**` | M9 (M2a creates a crate that builds with a usage-only `main.rs`, D-004) | same | +| `android/settings.gradle.kts`, `android/build.gradle.kts`, `android/gradle.properties`, `android/gradle/**`, `android/build-logic/**`, the module shells (build file, manifest, one empty entry composable per feature) of `:core:design`, `:data`, `:adapter:fcm` and every `:feature:*` (D-004) | M2b | later plans may add catalog entries and module includes (rebase resolves) | | `android/core/model/**`, `android/domain/**` | M2b | contract-change plans only | | `android/core/fakes/**` | M2b | M7x add files only under `fakes//` | | `android/core/design/**` | M6 | M7x may not edit; requests go through the orchestrator | @@ -2303,16 +2358,22 @@ Ownership (an agent edits only paths its milestone owns; anything else needs the | `android/feature/analytics/**` | M7e | | | `android/feature/settings/**` | M7f | | | `infra/**` | M2c-dev, then M2c-pi (D-001) | M10 updates runbooks | +| `.gitea/workflows/**` | M2c-pi (D-023) | later infra plans | +| git-ignored `infra/secrets/**` in the main checkout | M2c-dev (D-022), then M2c-pi | M8 copies `google-services.json` from it (§10.7) | | `docs/ux/**` | U1, U2 | | | `docs/validation/` | the milestone that names the file | | -Strings: each feature module has its own `src/main/res/values/strings.xml` and `values-sv/strings.xml` with keys prefixed by the feature name; shared strings live in `:core:design`. Keys come from `docs/ux/strings.csv` (§13.4). +Module shells (D-004): a shell file or module created by M2a or M2b in a path owned by a later milestone holds only what makes the build and the 501 stubs work; the later milestone owns it from the moment the shell lands and writes the body. + +Strings: each feature module has its own `src/main/res/values/strings.xml` and `values-sv/strings.xml` with keys prefixed by the feature name; shared strings (the notification copy included) live in `:core:design`; `:app` holds only the app name; lint `MissingTranslation` is an error in every module (§6.2, D-017). Keys come from `docs/ux/strings.csv` (§13.4). ### 14.8 Contract files and the change rule Contract files (exhaustive): `contracts/openapi.yaml`, `contracts/schema/server-v1.sql` and any later `server-vN.sql`, `contracts/test-vectors/*.json`, `contracts/examples/**`, the Room exported schema `android/data/schemas/**`, `android/core/model/**` (the UI contract and domain types), the sync protocol (§14.4) and the push payload (§14.6). -Rule: a change to any contract file, or to §14 of this spec, requires in the same change: a spec version bump (in the header and the change log), a `docs/decisions.md` entry, and the matching implementation changes on both sides, or a plan that schedules them. A schema change also bumps the schema version (`meta.schema_version` and a new migration on the server; a new Room version and migration on the phone). A sync protocol change bumps `X-Kvitt-Protocol`; a push payload change bumps `v`. Numbers come from the orchestrator's brief. +Representational edits to `contracts/openapi.yaml` (key order, `$ref` naming, description wording) do not change meaning and need no spec bump or decision entry; they are listed in the report of the plan that made them (D-016). The OpenAPI equality test compares parsed values, not text. + +Rule: a change to any other contract file, or a semantic change to `contracts/openapi.yaml` or to §14 of this spec, requires, landed together by the orchestrator: a spec version bump (in the header and the change log), a `docs/decisions.md` entry, and the matching implementation changes on both sides, or a plan that schedules them. A schema change also bumps the schema version (`meta.schema_version` and a new migration on the server; a new Room version and migration on the phone). A sync protocol change bumps `X-Kvitt-Protocol`; a push payload change bumps `v`. Numbers come from the orchestrator's brief. Implementation agents never edit `SPEC.md` or `docs/decisions.md`: they put the proposed spec text and decision entry in their report, and the orchestrator has a docs agent record them (CLAUDE.md › Orchestration). --- @@ -2322,3 +2383,4 @@ Rule: a change to any contract file, or to §14 of this spec, requires in the sa |---|---|---| | 0.1 | 2026-10-17 | First version, written from the owner interview: scope, requirements, data model, stack, API outline, security, business rules, deployment, milestones, UX sprint brief, contracts (schema v1, Room v1, OpenAPI outline, sync protocol v1, domain functions and vectors, push payload v1, ownership map). | | 0.2 | 2026-10-17 | Owner answers. D-001: development-first deployment on the Mac; M2c split into M2c-dev (now) and M2c-pi (after M8, before M10); registry is Gitea at `http://gitea.lan` (OQ-1 answered). D-002: local backup files deleted as soon as confirmed on Drive. D-003: Splitwise CSV format confirmed for English and Swedish exports, Swedish category names mapped, accounts "Hemma" and "Vardag" (OQ-2 answered). OQ-4 answered (Drive folder id). §10, §11, §12, §14.7 updated. Clarifications from M1 (no new decisions): non-deleted rows only into domain functions, analytics vector keys, sync push result key sets, E-1 503 body, optional/nullable request fields of E-3, E-12, E-17, E-33, `since` required on E-31, proposal amount range, bp above 10000, `resolveRange` edge cases, §9.6 limits not in JSON Schema, example directory names, `not_implemented` on no operation (§14.3, §14.4.3, §14.5). | +| 0.3 | 2026-10-17 | Orchestrator resolutions of the plans' open points. D-004: M2a and M2b create shell files and modules in later milestones' paths, ownership of `Cargo.lock` and every `Cargo.toml` (§11.2, §11.3, §14.7). D-015: server seams: middleware in `router.rs`, rate limit in `src/api/rate_limit.rs` on one marked line, `src/tasks/` for M4, `AuditWriter`/`PushQueue` traits with no-op implementations, `next_occurrence_date` and day clamping in `kvitt-domain` (Rust only), M3 writes the recurring generation function, M4 lands after M3 (§6.3, §9.5, §11, §14.5.2, §14.7). D-016: the OpenAPI equality test compares parsed values; representational edits need no spec bump (§11.3, §14.8). D-017: feature convention, analytics types in `se.hagstedt.kvitt.model.analytics`, string placement and key prefixes, `MissingTranslation` in every module (§6.2, §13.4, §14.5.4, §14.7). D-018: empty fake scenario; `resolveRange` throws a typed `IllegalArgumentException` subclass for an incomplete `CUSTOM` range (§6.2, §14.5.4). D-019: ledger rows and expense editor fields in `:core:design` (§6.2, §11.2). D-020: no bundled font, Material 3 default typeface (§6.2, §13.4). D-021: `sync_mutations` pruning daily at 04:00 (§14.1, §14.4.9). D-022: development infrastructure as M2c-dev built it (`c885aba`): Firebase through the REST API with a gcloud token, no Firebase CLI, secrets and `google-services.json` in `infra/secrets/` copied by `infra/dev/copy-google-services.sh`, Firebase resources and the `kvitt-fcm` service account (`roles/firebasecloudmessaging.admin`) exist; dev stack without Caddy or TLS, bind-mounted data, `infra/secrets/` mounted at `/secrets`, env file `infra/dev/.env.example`, `healthcheck` used by compose; the server starts when `KVITT_TRUSTED_PROXY` does not resolve; new M2c-dev gate (§10 intro, §10.4, §10.7, §11.2, §14.7). D-023: M2c-pi allowances, HUMAN steps, `.gitea/workflows/`, secret modes for uid 1000 (§10.2, §10.3, §11.2, §14.7). D-024: release keystore command, kept outside the repository (§10.6). D-025: production import procedure and the `/opt/kvitt/import` bind mount (§10.2, §10.8). Without a decision id: exact Rust version pin shared by `rust-toolchain.toml` and the Dockerfile (§10.3); the §9.7 colour change lands before M6 (§11.1); per-screen states are U2's list (§13.3); landing page copy (§13.3, AC-2.5); M8 owns network security config, hardening, signing and version properties (§11.2); U1's gate names the six phase-1 screens (§11.2); category table path (§10.8); implementation agents never edit the spec or the decisions log (§14.8). | diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index d91aa96..8dacc85 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -3,42 +3,41 @@ Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this. ## 1. Where things stand -- `main` at `8bef22a` (root commit "Scaffold: agent starter kit (seeded from Ebb)"). No gate yet: neither `android/` nor `server/` has a project. -- Branch `agent-spec` (on top of `main`, not landed): `SPEC.md` v0.1, filled `CLAUDE.md`, Gradle files moved to `android/`, tool scripts adjusted. +- `main` at `c885aba` (M2c-dev) on top of `5e1be38` (plans, prompts, SPEC v0.2); after this docs change lands, `main` also carries SPEC v0.3 (branch `agent-spec3`). Landed: M0 spec, M1 contracts and vectors (`c6df2ab`), M2c-dev infra for development (`c885aba`). No server or Android project yet, so neither gate runs. +- Worktrees in flight under `.claude/worktrees/`: `agent-m2a` (M2a server skeleton and `kvitt-domain`), `agent-m2b` (M2b Android skeleton and `:domain`), `agent-spec3` (this docs change: SPEC v0.3, D-004, D-015 to D-025). Both M2 agents started from `5e1be38`; their briefs carried the orchestrator's resolutions that v0.3 now records. +- The owner runs the UX sprint (U1, then U2) in a separate Claude session in the worktree `~/orca/workspaces/kvitt/UX-sprint` on branch `UX-sprint` (based on `cab3e93`, SPEC v0.1); it commits only under `docs/ux/` and rebases on `main` before landing. +- Development infrastructure exists (M2c-dev): `infra/dev/` compose stack on the Mac (server and on-demand backup, no Caddy, no TLS), Firebase on project `kvitt-1337` with Android apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`, service account `kvitt-fcm` with `roles/firebasecloudmessaging.admin`; `infra/secrets/fcm-service-account.json` and `infra/secrets/google-services.json` in the main checkout (git-ignored). The dev stack's health check from the phone is a HUMAN step and waits for M2a's image. - Installed on the owner's phone: nothing. -- **Bootstrap exception (2026-10-17):** the repository had no commits, so the normal worktree flow could not start. The spec agent created the root commit directly in the main checkout (stray `.DS_Store` files deleted, `.DS_Store` added to `.gitignore`), then branched `agent-spec` there. From now on the normal flow of CLAUDE.md › Orchestration applies. ## 2. Open work (one line each, with who owns it: agent or owner) -- Land `agent-plans` (orchestrator): SPEC v0.2 (D-001 to D-003), one `PLAN-.md` per milestone and a start prompt each in `prompts/`, dependency order in `prompts/README.md`. -- Plans (SPEC §11.2 v0.2), in dependency order; prompt `prompts/.md`: - - M1 Contracts and vectors: landed (`c6df2ab`); `PLAN-M1.md` on record. - - U1 UX directions (owner's session, branch `agent-ux`): `PLAN-U1.md`, can start now. Owner then picks a direction. - - U2 UX design spec (owner's session, branch `agent-ux`): `PLAN-U2.md`, after U1 and the owner's pick. Owner approves. - - M2a Server skeleton and `kvitt-domain`: `PLAN-M2a.md`, ready (needs its open points settled in the brief). - - M2b Android skeleton and `:domain`: `PLAN-M2b.md`, ready (needs its open points settled in the brief). - - M2c-dev Infra for development: `PLAN-M2c-dev.md`, ready (owner: `gcloud auth login`, `firebase login`). - - M3 Server core API: `PLAN-M3.md`, after M2a (decide the M3/M4 recurring-generation coupling first). - - M4 Server sync, audit, push, scheduler: `PLAN-M4.md`, after M2a. - - M5 Android data layer: `PLAN-M5.md`, after M2b. - - M6 Design system: `PLAN-M6.md`, after U2 approved, M2b, and the §9.7 colour spec change. - - M7a to M7f Features (onboarding, home and account, expense, settle, analytics, settings): `PLAN-M7a.md` to `PLAN-M7f.md`, after M6, in parallel. - - M9 Splitwise importer: `PLAN-M9.md`, after M2a and M4. - - M8 App integration: `PLAN-M8.md`, after M3, M4, M5, M7a to M7f, M2c-dev. - - M2c-pi Infra on the Pi: `PLAN-M2c-pi.md`, after M8, before M10 (owner: answer OQ-3). - - M10 First release: `PLAN-M10.md`, after M2c-pi, M3, M4, M8. - - M11 Import history: `PLAN-M11.md`, after M9 and M10. -- Owner: answer SPEC §12 OQ-3 (needed by M2c-pi only). +- Land `agent-spec3` (orchestrator): SPEC v0.3, decisions D-004 and D-015 to D-025, plans and prompts with every open point closed, CLAUDE.md numbering rule (implementation agents never edit SPEC or decisions). +- Then record M2a's and M2b's proposed decisions from their reports (D-005 to D-014, including D-010 the screenshot tool) through a docs agent (orchestrator). +- Plans (SPEC §11.2 v0.3), in dependency order; prompt `prompts/.md`: + - M1 Contracts and vectors: landed (`c6df2ab`). + - M2c-dev Infra for development: landed (`c885aba`); outstanding: phone `/health` check (owner) once M2a's image exists. + - U1 UX directions: owner's session (`UX-sprint`), in progress; owner then picks a direction. + - U2 UX design spec: owner's session (`UX-sprint`), after U1 and the pick; owner approves; then the §9.7 colour change goes into the spec (docs agent) before M6. + - M2a Server skeleton and `kvitt-domain`: agent in flight (`agent-m2a`). + - M2b Android skeleton and `:domain`: agent in flight (`agent-m2b`); plug the phone in once for `installDebug` (owner). + - M3 Server core API: after M2a. + - M4 Server sync, audit, push, scheduler: after M2a, lands after M3 (D-015). + - M5 Android data layer: after M2b. + - M6 Design system: after U2 approved, M2b and the §9.7 colour change. + - M7a to M7f Features: after M6, in parallel. + - M9 Splitwise importer: after M2a and M4. + - M8 App integration: after M3, M4, M5, M7a to M7f, M2c-dev. + - M2c-pi Infra on the Pi: after M8, before M10 (owner: answer OQ-3, grant the Pi, Home Assistant Pi, Gitea and Drive allowances at the start, D-023). + - M10 First release: after M2c-pi, M3, M4, M8 (owner: keystore per §10.6). + - M11 Import history: after M9 and M10. ## 3. Owner questions (asked, unanswered) -- OQ-1 Local container registry address, HTTP or HTTPS, credentials (SPEC §12). -- OQ-2 Splitwise CSV format confirmed against a real export, group list and target account names, non-SEK rows (SPEC §12). -- OQ-3 LAN addresses and SSH users of the Kvitt Pi and the Home Assistant Pi; certificate directory on the HA Pi; which SSH add-on (SPEC §12). -- OQ-4 Google Drive folder id for backups (SPEC §12). +- OQ-3 LAN addresses and SSH users of the Kvitt Pi and the Home Assistant Pi; SSH port of the HA Pi; certificate directory there; which SSH add-on (SPEC §12). Needed by M2c-pi only. (OQ-1, OQ-2, OQ-4 are answered: D-001, D-003, §10.9.) ## 4. Paused (waiting on something outside the repo) +- M2c-dev phone health check: waits for M2a's server image. ## 5. Numbering -Next free: D-001 (no decisions logged yet; the interview decisions are in SPEC v0.1 itself). SPEC v0.1 is written; the next spec version is v0.2. Schema version 1 is defined in SPEC §14.1 (not yet implemented); the next schema version is 2. Sync protocol 1, push payload 1. +Next free after `agent-spec3`: decision **D-026** (D-001 to D-004 and D-015 to D-025 are used; D-005 to D-014 are reserved for the M2a and M2b agents' proposals, D-010 being the screenshot tool). Spec version: v0.3 written; next **v0.4**. Schema version **1** (defined, not yet implemented); next schema version **2**. Sync protocol **1**, push payload **1**. ## 6. How the work is run See CLAUDE.md › Orchestration. HUMAN steps in reports are relayed in chat by the orchestrator; the owner does not diff --git a/docs/decisions.md b/docs/decisions.md index 75905e4..010912b 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -8,3 +8,15 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th | D-001 | 2026-10-17 | Development-first deployment. During development the server runs locally on the owner's M4 Mac (Docker compose `infra/dev/compose.yaml` or `cargo run`) and debug builds pair against it over the home Wi-Fi. M2c splits into M2c-dev (Mac compose stack, Firebase setup, local backup job; runs now) and M2c-pi (Pi provisioning, cert-sync, router forward 8443, Gitea registry and Gitea Actions, Drive backups; after M8, before M10). The registry is Gitea at `http://gitea.lan` (plain HTTP, home Wi-Fi only); the owner provides an example Gitea Actions workflow later. OQ-1 answered; OQ-3 now blocks only M2c-pi. | Owner wants to develop against a local server and defer the Pi. | SPEC §10 (intro, §10.1, §10.3, §10.7), §11.1, §11.2, §11.3, §12, §14.7; PLAN-M2c-dev, PLAN-M2c-pi | | D-002 | 2026-10-17 | Backup retention on the Pi: local snapshot files are deleted as soon as their upload to Drive is confirmed (`rclone lsf`), not after 30 days. Drive keeps everything. | Owner's choice; the Pi's disk is small and Drive is the archive. | SPEC §10.9 step 5; PLAN-M2c-pi | | D-003 | 2026-10-17 | Splitwise CSV format confirmed from two real exports (OQ-2 answered). English variant header `Date,Description,Category,Cost,Currency,Amelie Bennich,Jacob Hagstedt` (group "hemma", account "Hemma"); Swedish variant header `Datum,Beskrivning,Kategori,Kostnad,Valuta,Jacob Hagstedt,Amelie Bennich` (general group, account "Vardag", about 1750 rows). Member columns differ in order, so the importer reads member names from the header. ISO dates, point decimal, double-quoted fields with commas, trailing empty line then `Total balance` / `Totalsumma`. Payment category `Payment` / `Betalning`. All rows SEK. Swedish category names are mapped through a committed table (unknown names to `other`, counted as a warning). The partner's display name is Amelie. The CSV files stay in `export/` (git-ignored), never committed or quoted. | Real exports differ by Splitwise UI language. | SPEC §10.8, §12; PLAN-M9 (mapping notes), PLAN-M11 | +| D-004 | 2026-10-17 | Shells and ownership. M2a creates shell files in M3, M4 and M9 paths (501 handlers, stub subcommands `bootstrap`, `pair-device`, `rotate-epoch`, a `kvitt-import` crate whose `main.rs` prints usage); M2b creates a module shell for every module of §6.2 with one empty entry composable per feature. The later milestone owns each shell from the moment it lands and writes the body. M2a owns `server/Cargo.lock`, every `Cargo.toml`, `main.rs`, `lib.rs`, config, `serve` and `migrate`, `router.rs` and its own tests (any `tests/*.rs` without the `core_`, `sync_`, `scheduler_`, `push_` prefixes); later milestones may add dependency lines to `Cargo.toml`. | PLAN-M2a and PLAN-M2b open points (a), (b), (c): the skeletons need files in paths §14.7 gave to later milestones. | SPEC §11.2, §11.3, §14.7; PLAN-M2a, PLAN-M2b, PLAN-M3, PLAN-M4, PLAN-M6, PLAN-M7a to M7f, PLAN-M9 | +| D-015 | 2026-10-17 | Server seams. The `X-Kvitt-Protocol` 426 check, the `X-Kvitt-Server-Version` header and the §8.6 body limits are layers in `router.rs` (M2a). M3's rate limit is a layer in `src/api/rate_limit.rs` (replacing `src/ratelimit.rs`) attached at one line in `router.rs` that M2a marks. M2a creates `src/tasks/mod.rs` with a `start` function called from `serve`; M4 owns `src/tasks/**`. M2a implements `next_occurrence_date` and the day clamping in `kvitt-domain` (Rust only, `src/recurring.rs`) and defines the traits `AuditWriter` (`src/audit/`) and `PushQueue` (`src/push/`) with no-op implementations; M4 replaces the implementations, M3 and M9 call the traits. Filled in by the docs agent: M3 writes the per-template recurring generation function in `src/api/recurring.rs` (E-28, E-29 and M4's scheduler call it), M3 tests the §9.5 generation cases, M4 the scheduler cases, and M4 lands after M3. | PLAN-M2a (d), (e); PLAN-M3 (a), (c); PLAN-M4 (a), (b): M3 and M4 both need §9.5 logic, audit and push while running in parallel. | SPEC §6.3, §9.5, §11.1, §11.2, §14.5, §14.7; PLAN-M2a, PLAN-M3, PLAN-M4, PLAN-M9 | +| D-016 | 2026-10-17 | The OpenAPI equality test compares parsed values, not text. Representational edits to `contracts/openapi.yaml` (key order, `$ref` naming, description wording) need no spec bump or decision entry; they are listed in the plan report. | PLAN-M2a open point (f); utoipa output differs from hand-written YAML in form only. | SPEC §11.3, §14.8; CLAUDE.md (Contracts are versioned); PLAN-M2a | +| D-017 | 2026-10-17 | Feature convention. Each feature exposes one composable entry point per screen taking a `hiltViewModel()`-provided ViewModel and navigation as plain lambdas; features never reference other features or the `NavHost`; `:app` owns the `NavHost`; repositories are injected through the `:core:model` interfaces, bound in `:app` to `:data` (fakes only in test and debug-preview source sets). The §14.5.4 analytics types live in `:core:model`, package `se.hagstedt.kvitt.model.analytics`. Strings: `:app` holds only the app name, `:core:design` the shared strings (the notification copy and channel name included), each feature its own; lint `MissingTranslation` is an error in every module; keys are prefixed by the feature name (§14.7 wins over §13.4, which said "by screen"); `:core:design` keys never start with a feature name. | PLAN-M2b open points (c), (e), (f); M7a to M7f need one convention fixed in `build-logic`. | SPEC §6.2, §13.4, §14.5.4, §14.7; PLAN-M2b, PLAN-M6, PLAN-M7a to M7f, PLAN-M8, PLAN-U2 | +| D-018 | 2026-10-17 | The empty fake scenario: household complete (two named users, no pictures), no accounts, exactly the ten default categories, no expenses, settlements, templates, rejections or pending changes. `resolveRange` with `CUSTOM` and a null start or end throws a typed subclass of `IllegalArgumentException` declared in `:domain` (named by M2b); the caller maps it to `validation_failed`. | PLAN-M2b open point (g) and the `resolveRange` signalling left open in v0.2. | SPEC §6.2, §11.3, §14.5.4; PLAN-M2b | +| D-019 | 2026-10-17 | Shared UI needed by two features is built in `:core:design` by M6: the ledger rows (expense and settlement rows, used by S-5, S-12 and the pending list of S-16) and the expense editor fields (used by S-7 and S-19). Features never copy each other's components. | PLAN-M6 open point (a); feature modules cannot depend on each other. | SPEC §6.2, §11.2, §11.3; PLAN-M6, PLAN-M7b, M7c, M7e, M7f | +| D-020 | 2026-10-17 | No bundled font: the app uses the Material 3 default typeface. No licence step. | PLAN-M6 open point (b); avoids a font licence clearance. | SPEC §6.2, §11.2, §13.4; PLAN-M2b, PLAN-M6, PLAN-U1, PLAN-U2 | +| D-021 | 2026-10-17 | `sync_mutations` pruning runs daily at 04:00 Europe/Stockholm (DST-aware), deleting rows whose `received_at` is older than 90 days. | PLAN-M4 open point (c); the spec said only "daily". | SPEC §14.1 (rules text, not the DDL), §14.4.9, §11.2; PLAN-M4 | +| D-022 | 2026-10-17 | Development infrastructure (M2c-dev, landed `c885aba`). No new CLI installs: Firebase project and Android apps through the Firebase Management REST API with a `gcloud` access token, no Firebase CLI; `rclone` belongs to M2c-pi. The agent may write git-ignored secrets, `google-services.json` included, under `infra/secrets/`; M8 copies it into `android/app/` with `infra/dev/copy-google-services.sh`. Phone health check against the dev server is a HUMAN step. Gate: `docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config`; `shellcheck $(find infra -name '*.sh')`. As built: dev stack without Caddy or TLS, bind-mounted data folder, the whole `infra/secrets/` mounted read-only at `/secrets` (a missing key file means push disabled), env file `infra/dev/.env.example` (the Pi's stays `infra/env.example` for M2c-pi), `kvitt-server healthcheck` used by compose, backups with `sqlite3 .backup` (§10.9). The server must start when `KVITT_TRUSTED_PROXY` does not resolve. Firebase on `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm@kvitt-1337.iam.gserviceaccount.com` with `roles/firebasecloudmessaging.admin` (`roles/firebasemessaging.admin` does not exist). | Owner's tools already on the Mac; M2c-dev report. | SPEC §10 intro, §10.4, §10.7, §11.2, §11.3, §14.7; PLAN-M2c-dev, PLAN-M8 | +| D-023 | 2026-10-17 | M2c-pi: key-based SSH to the Kvitt Pi and the Gitea credentials are HUMAN steps; the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive are requested from the owner at the start of that sprint; the Gitea Actions workflow lives at `.gitea/workflows/`, owned by M2c-pi; secret files on the Pi (`ha_ssh_key`, `ha_known_hosts`, `rclone.conf`) mode 0600 owned by uid 1000, the uid of the `cert-sync` and `backup` containers; adding `gitea.lan` to `insecure-registries` is its own HUMAN step. | PLAN-M2c-pi open points (a) to (c). | SPEC §10.2, §10.3, §11.2, §11.3, §14.7; PLAN-M2c-pi | +| D-024 | 2026-10-17 | Release keystore: `keytool -genkeypair -v -keystore kvitt-release.jks -alias kvitt -keyalg RSA -keysize 4096 -validity 10000`, created by the owner and kept outside the repository (was `android/keystore/release.jks`); `local.properties` holds its absolute path. | §10.6 did not fix the key parameters (PLAN-M10 H-1); a keystore outside every checkout cannot be committed or deleted with a worktree. | SPEC §10.6, §11.2, §11.3; PLAN-M10 | +| D-025 | 2026-10-17 | Production Splitwise import (M11): a backup first; user ids resolved by display name from the database; CSVs copied to the Pi into `/opt/kvitt/import/`, bind-mounted read-only into `server` as `/import`; the import runs as `docker compose run --rm --entrypoint kvitt-import server ...` with the server stopped (`--entrypoint` because the image's entrypoint is `kvitt-server`); CSVs deleted afterwards. The category table stays at `server/crates/kvitt-import/data/splitwise_categories.csv`. | PLAN-M11 open points (a) to (c); PLAN-M9 table location. | SPEC §10.2, §10.8, §11.2, §11.3; PLAN-M2c-pi, PLAN-M9, PLAN-M11 | diff --git a/prompts/M1.md b/prompts/M1.md index e3ae16e..92cfb75 100644 --- a/prompts/M1.md +++ b/prompts/M1.md @@ -4,9 +4,9 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M1.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. diff --git a/prompts/M10.md b/prompts/M10.md index 82d7577..332a383 100644 --- a/prompts/M10.md +++ b/prompts/M10.md @@ -4,14 +4,14 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M10.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. Orchestrator notes for this run: -(To fill: Give the keystore parameters (algorithm, size, validity); name this agent as the one on the phone.) +(To fill: Name this agent as the one on the phone. The keystore command is fixed in SPEC §10.6, D-024.) {{ORCHESTRATOR_NOTES}} Essentials (the plan has the detail): diff --git a/prompts/M11.md b/prompts/M11.md index 52d4aee..16f8a29 100644 --- a/prompts/M11.md +++ b/prompts/M11.md @@ -4,14 +4,14 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M11.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. Orchestrator notes for this run: -(To fill: Settle open points (a) to (c) below.) +(To fill: Confirm the owner's allowance to act on the production Pi. Open points (a) to (c) are resolved in PLAN-M11 §4, SPEC v0.3, D-025.) {{ORCHESTRATOR_NOTES}} Essentials (the plan has the detail): diff --git a/prompts/M2a.md b/prompts/M2a.md index ba2217a..da2a46e 100644 --- a/prompts/M2a.md +++ b/prompts/M2a.md @@ -4,14 +4,14 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M2a.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. Orchestrator notes for this run: -(To fill: Settle the plan's open points (a) to (f) in the notes below.) +(To fill: Overrides only. The plan's open points (a) to (f) are resolved in PLAN-M2a §4, SPEC v0.3.) {{ORCHESTRATOR_NOTES}} Essentials (the plan has the detail): diff --git a/prompts/M2b.md b/prompts/M2b.md index c1c6f91..1d76da5 100644 --- a/prompts/M2b.md +++ b/prompts/M2b.md @@ -4,14 +4,14 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M2b.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. Orchestrator notes for this run: -(To fill: Settle the plan's open points (a) to (g) in the notes below; say whether this agent may use the phone.) +(To fill: Say whether this agent may use the phone. The plan's open points (a) to (g) are resolved in PLAN-M2b §4, SPEC v0.3.) {{ORCHESTRATOR_NOTES}} Essentials (the plan has the detail): diff --git a/prompts/M2c-dev.md b/prompts/M2c-dev.md index a38a3cd..d637198 100644 --- a/prompts/M2c-dev.md +++ b/prompts/M2c-dev.md @@ -4,20 +4,20 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M2c-dev.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. Orchestrator notes for this run: -(To fill: State whether the agent may write `infra/secrets/` and `android/app/google-services.json` in the main checkout and run gcloud/firebase.) +(To fill: Overrides only. The allowances (infra/secrets/ in the main checkout, the Google Cloud project kvitt-1337 through the REST API) are D-022.) {{ORCHESTRATOR_NOTES}} Essentials (the plan has the detail): - First step: from the main checkout, `git worktree add .claude/worktrees/agent-m2c-dev -b agent-m2c-dev main`; work only there, on branch `agent-m2c-dev`; then copy the git-ignored files the plan names. - Edit only `infra/**` (dev parts), plus git-ignored secrets in the main checkout if allowed below; anything else is a contract request to the orchestrator. -- Gate: `docker compose -f infra/dev/compose.yaml config`, shellcheck, `/health` from Mac and phone, one local backup. Report only results you ran in this session. +- Gate: `docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config`, `shellcheck $(find infra -name '*.sh')`, `/health` from the Mac, one local backup; the phone check is a HUMAN step. Report only results you ran in this session. - Stop and ask when the plan says so, when a step could touch the owner's real data, or when the spec is ambiguous in a way that changes a decision. - Commit on your branch when the gate is green, `git rebase main`, rerun the gate. Never merge, push or remove the worktree. diff --git a/prompts/M2c-pi.md b/prompts/M2c-pi.md index 51388fb..b982555 100644 --- a/prompts/M2c-pi.md +++ b/prompts/M2c-pi.md @@ -4,14 +4,14 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M2c-pi.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. Orchestrator notes for this run: -(To fill: Give the OQ-3 answers and settle open points (a) to (c) below; runs after M8, before M10.) +(To fill: Give the OQ-3 answers and the allowances the owner granted for the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive (D-023); runs after M8, before M10. Open points (a) to (c) are resolved in PLAN-M2c-pi §4.) {{ORCHESTRATOR_NOTES}} Essentials (the plan has the detail): diff --git a/prompts/M3.md b/prompts/M3.md index 5c4681c..ac1ad7f 100644 --- a/prompts/M3.md +++ b/prompts/M3.md @@ -4,14 +4,14 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M3.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. Orchestrator notes for this run: -(To fill: Settle the plan's open points (a) to (d) below (M3/M4 coupling first).) +(To fill: Overrides only. Open points (a) to (d) are resolved in PLAN-M3 §4, SPEC v0.3, D-015.) {{ORCHESTRATOR_NOTES}} Essentials (the plan has the detail): diff --git a/prompts/M4.md b/prompts/M4.md index b99e138..e9fec64 100644 --- a/prompts/M4.md +++ b/prompts/M4.md @@ -4,14 +4,14 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M4.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. Orchestrator notes for this run: -(To fill: Settle the plan's open points (a) to (c) below.) +(To fill: Overrides only. Open points (a) to (c) are resolved in PLAN-M4 §4, SPEC v0.3; M4 lands after M3.) {{ORCHESTRATOR_NOTES}} Essentials (the plan has the detail): diff --git a/prompts/M5.md b/prompts/M5.md index 967a73c..8c4ec24 100644 --- a/prompts/M5.md +++ b/prompts/M5.md @@ -4,9 +4,9 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M5.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. diff --git a/prompts/M6.md b/prompts/M6.md index 1d9dbf6..6f34fc1 100644 --- a/prompts/M6.md +++ b/prompts/M6.md @@ -4,14 +4,14 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M6.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. Orchestrator notes for this run: -(To fill: Confirm U2 approval, the §9.7 hex spec change, font clearance, and open point (a) below.) +(To fill: Confirm U2 approval and that the §9.7 colour change has landed in SPEC. Open points (a) to (c) are resolved in PLAN-M6 §4, SPEC v0.3.) {{ORCHESTRATOR_NOTES}} Essentials (the plan has the detail): diff --git a/prompts/M7a.md b/prompts/M7a.md index 2337bc3..4a3b683 100644 --- a/prompts/M7a.md +++ b/prompts/M7a.md @@ -4,9 +4,9 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M7a.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. diff --git a/prompts/M7b.md b/prompts/M7b.md index 34fc67d..5c2bb0e 100644 --- a/prompts/M7b.md +++ b/prompts/M7b.md @@ -4,9 +4,9 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M7b.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. diff --git a/prompts/M7c.md b/prompts/M7c.md index 9b57ee7..77be6d2 100644 --- a/prompts/M7c.md +++ b/prompts/M7c.md @@ -4,9 +4,9 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M7c.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. diff --git a/prompts/M7d.md b/prompts/M7d.md index 792c06a..64779c7 100644 --- a/prompts/M7d.md +++ b/prompts/M7d.md @@ -4,9 +4,9 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M7d.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. diff --git a/prompts/M7e.md b/prompts/M7e.md index 27bb8ed..4bffcd3 100644 --- a/prompts/M7e.md +++ b/prompts/M7e.md @@ -4,9 +4,9 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M7e.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. diff --git a/prompts/M7f.md b/prompts/M7f.md index a6e0427..52d5a15 100644 --- a/prompts/M7f.md +++ b/prompts/M7f.md @@ -4,9 +4,9 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M7f.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. diff --git a/prompts/M8.md b/prompts/M8.md index 288ace8..56725dc 100644 --- a/prompts/M8.md +++ b/prompts/M8.md @@ -4,14 +4,14 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M8.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. Orchestrator notes for this run: -(To fill: Confirm the extra `:app` deliverables (PLAN §4 note); name this agent as the one on the phone.) +(To fill: Name this agent as the one on the phone. The extra `:app` deliverables are M8's per SPEC §11.2 v0.3.) {{ORCHESTRATOR_NOTES}} Essentials (the plan has the detail): diff --git a/prompts/M9.md b/prompts/M9.md index 61d883e..ebc1784 100644 --- a/prompts/M9.md +++ b/prompts/M9.md @@ -4,9 +4,9 @@ You are an Opus implementation agent on Kvitt (repository `/Users/jhag/projects/ Read PLAN-M9.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. -Numbers you may use (from the orchestrator): +Numbers you may use (from the orchestrator), for proposed entries in your report only; you never edit `SPEC.md` or `docs/decisions.md` (CLAUDE.md › Orchestration): - Next free decision id: {{NEXT_DECISION}} -- Spec version for any spec change: {{SPEC_VERSION}} +- Spec version a proposed spec change would get: {{SPEC_VERSION}} - Schema version for any schema change: {{SCHEMA_VERSION}} If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever the number would go and say so in the report. diff --git a/prompts/README.md b/prompts/README.md index d8a6687..a8fe1f3 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -1,8 +1,8 @@ # Start prompts -One ready-to-paste start prompt per plan (`PLAN-.md` at the repository root), plus `ORCHESTRATOR-prompt.md`. Before pasting, the orchestrator fills `{{NEXT_DECISION}}`, `{{SPEC_VERSION}}`, `{{SCHEMA_VERSION}}` (disjoint ranges for agents that run in parallel, CLAUDE.md › Numbering) and `{{ORCHESTRATOR_NOTES}}` (answers to the plan's open points, phone assignment, overrides). `U2.md` also takes `{{OWNER_PICK_AND_COMMENTS}}`. U1 and U2 run in the owner's own Claude session on branch `agent-ux`. +One ready-to-paste start prompt per plan (`PLAN-.md` at the repository root), plus `ORCHESTRATOR-prompt.md`. Before pasting, the orchestrator fills `{{NEXT_DECISION}}`, `{{SPEC_VERSION}}`, `{{SCHEMA_VERSION}}` (disjoint ranges for agents that run in parallel, CLAUDE.md › Numbering) and `{{ORCHESTRATOR_NOTES}}` (answers to the plan's open points, phone assignment, overrides). `U2.md` also takes `{{OWNER_PICK_AND_COMMENTS}}`. U1 and U2 run in the owner's own Claude session in `~/orca/workspaces/kvitt/UX-sprint` on branch `UX-sprint`. The numbers are for proposed entries in agents' reports: implementation agents never edit `SPEC.md` or `docs/decisions.md`; a docs agent records what the orchestrator accepts (CLAUDE.md › Orchestration). -Dependencies follow SPEC §11.1 and §11.2 (v0.2, D-001). Status as of 2026-10-17: M0 done, M1 landed (`c6df2ab`). +Dependencies follow SPEC §11.1 and §11.2 (v0.3). Status as of 2026-10-17: M0 done, M1 landed (`c6df2ab`), M2c-dev landed (`c885aba`); M2a and M2b in flight; U1 in the owner's session. The plans' open points are resolved in SPEC v0.3 and recorded in each plan's §4. ## Dependency order and waves @@ -14,10 +14,10 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7 | 0 | U1 UX directions | `U1.md` | M0 | everything in waves 0 to 2 (owner's session) | | 1 | M2a Server skeleton and `kvitt-domain` | `M2a.md` | M1 | M2b, M2c-dev, U1/U2 | | 1 | M2b Android skeleton and `:domain` | `M2b.md` | M1 | M2a, M2c-dev, U1/U2 | -| 1 | M2c-dev Infra for development | `M2c-dev.md` | M0 (its health check needs M2a's image) | M2a, M2b, U1/U2 | +| 1 | M2c-dev Infra for development | `M2c-dev.md` | M0 (its health check needs M2a's image) | done (`c885aba`) | | 1 | U2 UX design spec | `U2.md` | U1 and the owner's pick | M2a, M2b, M2c-dev, wave 2 | -| 2 | M3 Server core API | `M3.md` | M2a | M4 (see coupling below), M5, M6 | -| 2 | M4 Server sync, audit, push, scheduler | `M4.md` | M2a | M3, M5, M6 | +| 2 | M3 Server core API | `M3.md` | M2a | M4 (M4 lands after M3), M5, M6 | +| 2 | M4 Server sync, audit, push, scheduler | `M4.md` | M2a; lands after M3 (D-015) | M3, M5, M6 | | 2 | M5 Android data layer | `M5.md` | M1, M2b | M3, M4, M6 | | 2 | M6 Design system | `M6.md` | U2 approved, M2b, the §9.7 colour spec change | M3, M4, M5 | | 3 | M7a Feature onboarding | `M7a.md` | M6 | M7b to M7f, M9 | @@ -34,8 +34,9 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7 ## Constraints the orchestrator must keep in mind -- **M3 and M4 are coupled** through recurring generation (E-28 generates a due period; §9.5 logic, audit writer and push queue are M4's). Decide the interface before starting both (PLAN-M3 open point a, PLAN-M4 open point b), or land M4's generation function first. -- **M2a and M2b need the stub-file decisions** in their notes before starting (PLAN-M2a open points a to f, PLAN-M2b open points a to g): they create files in directories §14.7 gives to later milestones. -- **M7a to M7f** need M2b's feature convention (Hilt or not, navigation callbacks) and M6's shared components (ledger rows, expense form fields) settled first. +- **M3 and M4** (D-015): M2a provides `next_occurrence_date`, the `AuditWriter`/`PushQueue` traits with no-op implementations and `tasks::start`; M3 writes the recurring generation function and calls the traits; M4 replaces the implementations, its scheduler calls M3's function, and M4 lands after M3. +- **M2a and M2b** create shell files and modules in later milestones' paths (D-004); ownership passes on when they land. +- **M7a to M7f** follow the feature convention of SPEC §6.2 (D-017) and take the ledger rows and expense editor fields from M6's `:core:design` (D-019). +- **M6** starts only after the §9.7 colour change from U2's proposal has landed in SPEC (§11.1). - **Phone:** M2b (one `installDebug`), M5 (only if instrumented tests), M8 and M10 use the phone; only one at a time, named in the notes. -- **Outside the repository:** M2c-dev (gcloud, Firebase, secrets in the main checkout), M2c-pi (SSH to the Pi, Gitea, Drive), M8 (dev server on the Mac), M10 and M11 (production) need the owner's allowance in the notes. +- **Outside the repository:** M2c-dev (D-022, done), M2c-pi (the Kvitt Pi, the Home Assistant Pi, Gitea, Drive: allowances requested at the start of that sprint, D-023), M8 (dev server on the Mac), M10 and M11 (production) need the owner's allowance in the notes. diff --git a/prompts/U1.md b/prompts/U1.md index e9fc953..f40e729 100644 --- a/prompts/U1.md +++ b/prompts/U1.md @@ -7,11 +7,11 @@ Read PLAN-U1.md and follow it; CLAUDE.md rules apply. SPEC.md wins over both. Yo Numbers (normally none are needed for UX work): next decision {{NEXT_DECISION}}, spec version {{SPEC_VERSION}}, schema version {{SCHEMA_VERSION}}. If a value is still a `{{…}}` placeholder, write `TODO(spec)` wherever a number would go. Setup and limits: -- Work on branch `agent-ux` in the worktree `.claude/worktrees/agent-ux`: from the main checkout, `git worktree add .claude/worktrees/agent-ux -b agent-ux main`, then `git reset --hard main` inside it. Do not use any other branch or worktree (an old `UX-sprint` branch exists elsewhere; ignore it). +- Work on branch `UX-sprint` in the worktree `~/orca/workspaces/kvitt/UX-sprint` (the owner's separate session); `git rebase main` before reporting. Do not use any other branch or worktree. - Commit only files under `docs/ux/`. Edit nothing else, SPEC.md and CLAUDE.md included. - Deliver 2 or 3 directions per SPEC §13.4 phase 1: HTML in a 411 dp phone frame, the six screens S-4, S-7, S-5, S-9, S-11, S-8 in light and dark, Swedish copy, realistic data for Jacob and Anna, PNG exports, and `docs/ux/directions/README.md` with the comparison and your recommendation. -- No external requests except Google Fonts for a font that may be bundled in an Android app. No placeholder text. +- No external requests except Google Fonts. The app bundles no font and uses the Material 3 default typeface (D-020). No placeholder text. - Stop and ask before installing any tool (for example a headless browser for PNG export). -- Commit on `agent-ux`, `git rebase main`. Never merge, push or remove the worktree. +- Commit on `UX-sprint`, `git rebase main`. Never merge, push or remove the worktree. -Report: end with the report described in PLAN-U1.md §10: branch, head commit hash, files produced with PNG counts, fonts and licences, your recommendation in one line, open questions and spec gaps with section numbers, and the owner's next step ("Review 2-3 directions, pick one, give comments") with the PNG paths. +Report: end with the report described in PLAN-U1.md §10: branch, head commit hash, files produced with PNG counts, typography, your recommendation in one line, open questions and spec gaps with section numbers, and the owner's next step ("Review 2-3 directions, pick one, give comments") with the PNG paths. diff --git a/prompts/U2.md b/prompts/U2.md index 141ca3e..449753b 100644 --- a/prompts/U2.md +++ b/prompts/U2.md @@ -10,10 +10,10 @@ The owner's pick and comments on the U1 directions: {{OWNER_PICK_AND_COMMENTS}} Setup and limits: -- Work on branch `agent-ux` in `.claude/worktrees/agent-ux`. If that worktree still exists (U1 not landed), continue there and `git rebase main`; otherwise, from the main checkout, `git worktree add .claude/worktrees/agent-ux -b agent-ux main`, then `git reset --hard main` inside it. +- Work on branch `UX-sprint` in `~/orca/workspaces/kvitt/UX-sprint` (the owner's separate session); `git rebase main` before reporting. - Commit only files under `docs/ux/`. Edit nothing else; the §9.7 colour change is a proposal file, the orchestrator changes SPEC.md. -- Deliver `docs/ux/design-spec.md` (tokens, components, every screen S-1 to S-22 with every §13.3 state or "not applicable"), `docs/ux/strings.csv` (`key,en,sv,notes`, every string), `docs/ux/screens/` PNGs, `docs/ux/colour-tokens-proposal.md`. -- Gate: every string key has `en` and `sv`; every screen has all states of §13.3. -- Stop and ask before installing any tool. Commit on `agent-ux`, `git rebase main`. Never merge, push or remove the worktree. +- Deliver `docs/ux/design-spec.md` (tokens, components, every screen S-1 to S-22 with its list of §13.3 states, each specified or "not applicable"), keys prefixed by feature module (D-017), `docs/ux/strings.csv` (`key,en,sv,notes`, every string), `docs/ux/screens/` PNGs, `docs/ux/colour-tokens-proposal.md`. +- Gate: every string key has `en` and `sv`; every screen has every state `design-spec.md` lists for it. +- Stop and ask before installing any tool. Commit on `UX-sprint`, `git rebase main`. Never merge, push or remove the worktree. -Report: end with the report described in PLAN-U2.md §10: branch, head commit hash, gate self-check lines, how each owner comment was addressed, the proposed colour values, fonts and licences, open questions and spec gaps with section numbers, and the owner's next step ("Approve the design spec"). +Report: end with the report described in PLAN-U2.md §10: branch, head commit hash, gate self-check lines, how each owner comment was addressed, the proposed colour values, typography, open questions and spec gaps with section numbers, and the owner's next step ("Approve the design spec").