Plans and start prompts for every milestone; SPEC v0.2
PLAN-<id>.md for U1, U2, M1, M2a, M2b, M2c-dev, M2c-pi, M3, M4, M5, M6, M7a-M7f, M8, M9, M10, M11, each with the eleven fixed sections, and a start prompt per plan in prompts/ plus prompts/README.md (dependency order and parallel waves). SPEC v0.2: owner answers D-001 (development-first, M2c split into M2c-dev and M2c-pi, Gitea registry), D-002 (local backups deleted once on Drive), D-003 (Splitwise CSV format, English and Swedish variants, category mapping), OQ-1, OQ-2, OQ-4 answered; M1's readings of ambiguous contract points stated explicitly in §14.3 to §14.5. HANDOFF §2 lists the plans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+93
@@ -0,0 +1,93 @@
|
||||
# PLAN-M3: Server core API
|
||||
|
||||
## 1. Goal
|
||||
Implement the REST surface of the server other than sync: invites and pairing, profile and pictures, devices, accounts, categories, recurring templates, the settlement proposal, APK serving, the invite landing page, rate limiting, and the `bootstrap` and `pair-device` subcommands. Every endpoint is covered by integration tests for each error code it can return.
|
||||
|
||||
## 2. Read first
|
||||
1. SPEC §7 API outline.
|
||||
2. SPEC §14.3 OpenAPI 3.1 outline: 14.3.1 Common rules, 14.3.2 Error shape and error codes, 14.3.3 Wire objects, 14.3.4 Endpoints (E-2 to E-30, E-33), 14.3.5 Examples as contract.
|
||||
3. SPEC §4 FR-1 Server bootstrap, FR-2 Invites and pairing, FR-3 Devices and revocation, FR-4 Profiles, FR-5 Household gate, FR-6 Expense accounts, FR-7 Categories, FR-9 Recurring expenses (AC-9.1, AC-9.7, AC-9.8), FR-11 AC-11.8, FR-16 AC-16.1.
|
||||
4. SPEC §8.2 Device tokens, §8.3 Invite codes, §8.4 Authorization rules, §8.5 Transport (`X-Forwarded-For`), §8.6 Rate limiting and input limits, §8.7 Privacy and logging.
|
||||
5. SPEC §9.4 Settlement allocation proposal, §9.5 Recurring generation (resume rule, `next_occurrence_date`, due-period generation on create), §9.6 Limits and validation, §9.7 Icon and colour tokens, §9.10 Other edge cases.
|
||||
6. SPEC §10.4 Server configuration and CLI (`bootstrap`, `pair-device`), §10.6 APK hosting and release (`latest.json`, file name pattern).
|
||||
7. SPEC §14.1 SQLite schema v1 and server-side rules; §14.5.2 Functions (`propose_allocation`).
|
||||
8. SPEC §11.2 (row M3), §11.3, §14.7, §14.8.
|
||||
9. Files: `contracts/openapi.yaml`, `contracts/examples/**`, the M2a code under `server/` (router, error, auth, wire, db), `CLAUDE.md` (BUILD AND VERIFY, ARCHITECTURE RULES, ANTI-PATTERNS).
|
||||
|
||||
## 3. Already established
|
||||
- Routes, the utoipa `ApiDoc`, error type, auth extractors, wire objects, DB pool and sequence exist from M2a; M3 fills in handler bodies in its own files (§11.3).
|
||||
- `bootstrap`: no users → cancel any pending `admin_setup`, create a new one, print code, link `<KVITT_PUBLIC_URL>/i/<CODE>`, expiry, exit 0; users exist → `already initialized` on stderr, exit 2 (AC-1.1 to AC-1.3, §10.4). `pair-device --user <id>`: 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=<urlencoded KVITT_PUBLIC_URL>&code=<CODE>`, code in groups of 4, one sentence of instructions; no validity hint, no personal data; any 1 to 32 character code gets 200, else 404 HTML (AC-2.5, E-4).
|
||||
- Rate limits: unauthenticated 60/min per IP, authenticated 600/min per device, 429 `rate_limited` with `Retry-After`; more than 10 failed accepts per IP in 60 minutes → `rate_limited` for the rest of the window; in memory; `X-Forwarded-For` trusted only from the Caddy peer (`KVITT_TRUSTED_PROXY`) (§8.6, AC-2.12, §8.5, §10.4).
|
||||
- Pictures: JPEG/PNG/WebP up to 5 MiB and 8000 × 8000, EXIF orientation applied, centre-crop square, JPEG q85 at 512 and 128, metadata stripped, `picture_version` incremented, files `pictures/<user_id>/<v>-512.jpg` and `-128.jpg`, older versions deleted after success; E-11 headers `ETag`, `Cache-Control: private, max-age=31536000, immutable`, `size` 128|512 default 512 (AC-4.4, §10.4, E-9 to E-11).
|
||||
- Household gate: account create, template create and any expense or settlement mutation → `household_incomplete` below two users (AC-5.2).
|
||||
- Accounts: names unique among non-deleted, case-insensitive after trim (Unicode lowercase) via `name_key`; delete only when unreferenced (`account_not_empty`), soft delete; ratio change affects only later expenses (FR-6).
|
||||
- Categories: icon and colour validated against §9.7 lists; new `sort_order` = max + 10; `name: null` resets a default name, `category_is_default` on a non-default; delete blocked by use (`category_in_use`) or default key (`category_is_default`); no uniqueness (FR-7, E-23 to E-27).
|
||||
- Recurring templates: E-28 generates an already-due period before responding; E-29 `split_mode` and `splits` together; resume does not backfill; `next_occurrence_date` per §9.5 (§14.3.4, §9.5).
|
||||
- Every REST write that changes a synced row takes a new `server_version` in a `BEGIN IMMEDIATE` transaction and returns the row in wire form (§7, §14.1).
|
||||
- E-33 uses `kvitt-domain::propose_allocation` over non-deleted rows, `exclude_settlement_id` honoured (AC-11.8, E-33).
|
||||
- APK: `GET /apk/latest` reads `apk/latest.json` on each request; `GET /apk/files/{file_name}` only for `^kvitt-[0-9]+\.apk$` that exists, `application/vnd.android.package-archive`, `Content-Length` (§10.6, E-5, E-6).
|
||||
- `last_seen_at`/`app_version_code` update at most hourly (§8.2); devices never deleted (AC-3.5).
|
||||
|
||||
## 4. Owned paths
|
||||
From SPEC §14.7, verbatim: `kvitt-server`: `src/api/{invites,landing,me,pictures,devices,accounts,categories,recurring,proposal,apk}.rs`, `src/ratelimit.rs`, `src/cli/{bootstrap,pair_device}.rs`, `tests/core_*.rs` (owner M3; after landing: "later server plans").
|
||||
Also from §14.7: M3 "may add a dependency line to a crate's `Cargo.toml` (second to land resolves `Cargo.lock` on rebase)" and "may add one line per new module to `mod.rs` files they need".
|
||||
|
||||
Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.
|
||||
|
||||
Open points the orchestrator must settle in the start prompt's notes: (a) E-28's due-period generation, E-29's resume rule and `next_occurrence_date` use §9.5 logic, the audit writer and the push queue, which §14.7 places in M4's `src/scheduler/**`, `src/audit/**` and `src/push/**` while M3 and M4 run in parallel: which plan writes the shared generation function and in which order they land; (b) E-2 `GET /setup/status` is listed for M3 in §11.2 but implemented by M2a (`src/api/setup.rs`); M3 only tests it; (c) how the rate-limit layer and body limits attach to M2a's `router.rs`; (d) the landing page text (one sentence of instructions, English and Swedish) is not in §13 or `docs/ux/strings.csv`: who writes the Swedish copy.
|
||||
|
||||
## 5. Deliverables
|
||||
- `src/api/invites.rs`: E-3, E-12, E-13, E-14.
|
||||
- `src/api/landing.rs`: E-4 HTML in sv and en.
|
||||
- `src/api/me.rs`: E-7, E-8.
|
||||
- `src/api/pictures.rs`: E-9, E-10, E-11.
|
||||
- `src/api/devices.rs`: E-15, E-16, E-17.
|
||||
- `src/api/accounts.rs`: E-18 to E-22.
|
||||
- `src/api/categories.rs`: E-23 to E-27.
|
||||
- `src/api/recurring.rs`: E-28 to E-30.
|
||||
- `src/api/proposal.rs`: E-33.
|
||||
- `src/api/apk.rs`: E-5, E-6.
|
||||
- `src/ratelimit.rs`: §8.6 limits and AC-2.12.
|
||||
- `src/cli/bootstrap.rs`, `src/cli/pair_device.rs`: FR-1 subcommands.
|
||||
- `tests/core_*.rs`: one or more integration test files per endpoint group covering success and every error code listed for each endpoint in §14.3.4, responses compared with `contracts/examples/`.
|
||||
|
||||
## 6. Steps
|
||||
1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m3 -b agent-m3 main`, then in `.claude/worktrees/agent-m3`: `git reset --hard main`. No git-ignored files are needed; copy none. Run `tools/install-git-hooks.sh`.
|
||||
2. Read §2 in order. Confirm M2a has landed (`server/crates/kvitt-server/src/router.rs` exists on `main`); if not, stop and report.
|
||||
3. Versions: any crate added (for example image decoding, HTML templating, a rate limiter) at its latest stable version from crates.io / docs.rs; confirm current API usage from official docs; record each.
|
||||
4. Build the test harness first in `tests/core_common.rs` or equivalent (temp SQLite, injected clock, seeded two-user household helper, request helper that sends `X-Kvitt-Protocol: 1`).
|
||||
5. Implement in this order, each with its tests before moving on: `bootstrap` and `pair-device`; E-3 accept (all three kinds) and E-12 to E-14; E-7, E-8; E-15 to E-17; E-18 to E-22; E-23 to E-27; E-28 to E-30 (per open point a); E-33; E-9 to E-11; E-5, E-6; E-4; rate limiting (per open point c).
|
||||
6. For each endpoint, test every error code §14.3.4 lists for it, plus `household_incomplete` where AC-5.2 applies and `forbidden` for member on admin routes.
|
||||
7. Compare response bodies with `contracts/examples/<E-id>-<name>/response.json` by JSON value (ids and timestamps normalised as the examples allow).
|
||||
8. Check logs from one test run contain no token, code, name, description, note or amount (§8.7).
|
||||
9. Run the gate (§7). Commit on `agent-m3`, `git rebase main` (resolve `Cargo.lock` if M4 landed first), rerun the gate. Never merge, push or remove the worktree.
|
||||
|
||||
## 7. Gate
|
||||
From SPEC §11.2, verbatim: "Server gate; integration tests per endpoint and per error code listed for it".
|
||||
Command, from `server/`: `cargo fmt --check && cargo clippy -- -D warnings && cargo test`.
|
||||
Green means: all three exit 0 in this session; `test result:` lines show 0 failed; the report contains a table endpoint × error code → test name with no empty cell for E-2 to E-30 and E-33.
|
||||
|
||||
## 8. HUMAN steps
|
||||
From SPEC §11.2, verbatim: "none".
|
||||
|
||||
## 9. Out of scope
|
||||
- E-31, E-32 sync, audit writer and diff, push sender, scheduler, `rotate-epoch`, `sync_mutations` pruning (M4), except what open point (a) assigns.
|
||||
- Router, error type, auth extractor, wire types, migrations (M2a; contract requests only).
|
||||
- The importer (M9); `infra/` (M2c-dev, M2c-pi); any Android work.
|
||||
- Semantic changes to `contracts/**`.
|
||||
|
||||
## 10. Report
|
||||
- Branch `agent-m3`, head commit hash after rebasing on `main`.
|
||||
- Gate output: fmt and clippy exit status, every `test result:` line.
|
||||
- The endpoint × error code → test table.
|
||||
- Versions of crates added, with sources.
|
||||
- How open points (a) to (d) were resolved.
|
||||
- Numbering used (decision ids, spec version) or `TODO(spec)` occurrences.
|
||||
- Contract requests (anything needed in M2a's files), open questions, spec gaps (with section numbers).
|
||||
|
||||
## 11. Numbering
|
||||
Decision ids, spec version and schema version are assigned by the orchestrator in the start prompt; use `TODO(spec)` if none was given.
|
||||
Reference in New Issue
Block a user