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:
2026-10-17 21:05:34 +02:00
parent c6df2ab8f5
commit 5e1be38364
46 changed files with 2398 additions and 54 deletions
+104
View File
@@ -0,0 +1,104 @@
# PLAN-M2a: Server skeleton and `kvitt-domain`
## 1. Goal
Create the Rust Cargo workspace with its three crates, a running `kvitt-server` with config, database pool, schema migration, error type, auth extractor, every route of §7 registered (501 until implemented), `GET /health` and `GET /setup/status`, and a utoipa document equal to `contracts/openapi.yaml`. Implement `kvitt-domain` so that it passes every shared test vector, so M3, M4 and M9 can fill in their own files in parallel.
## 2. Read first
1. SPEC §6.3 Server (stack and crate table) and §6.1 System view.
2. SPEC §7 API outline.
3. SPEC §14.3 OpenAPI 3.1 outline (14.3.1 Common rules, 14.3.2 Error shape and error codes, 14.3.3 Wire objects, 14.3.4 Endpoints, 14.3.5 Examples as contract).
4. SPEC §14.1 SQLite schema v1 and its server-side rules.
5. SPEC §14.5 Domain rules as pure functions (14.5.1 Types, 14.5.2 Functions, 14.5.3 Error tokens, 14.5.5 Test-vector format).
6. SPEC §9.1 Splits, §9.3 Balances, §9.4 Settlement allocation proposal, §9.6 Limits and validation.
7. SPEC §8.2 Device tokens, §8.4 Authorization rules, §8.6 Rate limiting and input limits, §8.7 Privacy and logging, §8.9 Server hardening.
8. SPEC §10.3 Images, registry and build (the `server` image), §10.4 Server configuration and CLI.
9. SPEC §4 FR-20 Health and AC-1.5 (setup status).
10. SPEC §11.2 (row M2a), §11.3 (notes M1, M2a), §14.7 Module layout and ownership map, §14.8 Contract files and the change rule.
11. Files: `contracts/openapi.yaml`, `contracts/schema/server-v1.sql`, `contracts/test-vectors/*.json`, `contracts/examples/**`, `CLAUDE.md` (BUILD AND VERIFY, ARCHITECTURE RULES, ANTI-PATTERNS).
## 3. Already established
- Rust stable, edition 2024; axum on tokio multi-thread runtime with 2 worker threads; SQLite via sqlx, WAL, migrations embedded with `sqlx::migrate!`; serde; utoipa; tracing JSON lines to stdout; chrono + chrono-tz; uuid v7; image crate; reqwest with rustls; jsonwebtoken (§6.3). M2a needs only the subset it uses; later plans add the rest.
- Crates: `kvitt-domain` (library, no I/O, depends only on `serde`, `serde_json` for tests, `uuid`), `kvitt-server` (binary plus `lib.rs`), `kvitt-import` (binary, depends on `kvitt-domain` and `kvitt-server` as a library) (§6.3, CLAUDE.md Server layering).
- `migrations/0001_init.sql` is byte-identical to `contracts/schema/server-v1.sql`; a test asserts it (§14.1).
- Every connection sets `foreign_keys = ON`, `journal_mode = WAL`, `synchronous = NORMAL`, `busy_timeout = 5000` (§8.9). Every write transaction is `BEGIN IMMEDIATE`; `server_version` comes from `UPDATE sync_sequence SET value = value + 1 WHERE id = 1 RETURNING value` (§14.1).
- Error type covers exactly the 25 codes of §14.3.2 with their HTTP statuses and the JSON shape `{code, message, field}`; `not_implemented` (501) exists only during M2a to M4 (§14.3.2).
- Auth: bearer `kvt_` token, SHA-256 hex lookup, 401 `unauthorized` / `device_revoked`, `last_seen_at` and `app_version_code` updated at most once per hour per device; admin-only routes 403 `forbidden` for members (§8.2, §8.4).
- Every route of §7 is registered up front with a handler returning 501 `not_implemented`, and the utoipa `ApiDoc` lists them all, so M3 and M4 only fill in handler bodies in their own files (§11.2, §11.3).
- `GET /health` returns `{status, server_version, schema_version, db_ok}`, 503 `degraded` when `SELECT 1` fails, unauthenticated, under 100 ms on the Pi (FR-20, §14.3.4 E-1). `GET /setup/status` returns `{"initialized": bool}` (AC-1.5).
- Environment variables are exactly those of §10.4 with their defaults; `Europe/Stockholm` is a code constant. Subcommands are exactly `serve`, `bootstrap`, `pair-device`, `rotate-epoch`, `healthcheck`, `migrate` (§10.4); M2a implements `serve`, `migrate`, `healthcheck`.
- `server/Dockerfile`: stage 1 `rust:<stable>-bookworm` builds both binaries release `--locked` for `aarch64-unknown-linux-gnu` (no `target-cpu=native`); stage 2 `gcr.io/distroless/cc-debian12:nonroot` arm64, binaries in `/usr/local/bin/`, user 10001, entrypoint `kvitt-server`, command `serve` (§10.3).
- Every response carries `X-Kvitt-Server-Version`; authenticated endpoints require `X-Kvitt-Protocol: 1` else 426 `protocol_unsupported`; unknown request fields ignored; missing/wrong fields 400 `invalid_request`; path ids that are not UUIDs 404 `not_found`; optional response fields present as `null` (§14.3.1).
- Logs never contain tokens, invite codes, names, descriptions, notes, amounts or picture bytes; they log route templates, not raw paths (§8.7).
- No `unwrap()`/`expect()` outside `main` start-up, no `unsafe` (CLAUDE.md).
- M1 has landed (commit `c6df2ab`): `contracts/openapi.yaml` exists with 33 operations and 60 schemas, and the utoipa document must match it; differences may only be representational. M1's readings are now explicit in SPEC v0.2 (§14.3.1, §14.3.2 `not_implemented` on no operation, §14.3.4 E-1/E-3/E-12/E-17/E-31/E-33, §14.4.3 result key sets, §14.5.1, §14.5.2, §14.5.5); do not re-decide them.
- `contracts/openapi.yaml` may be changed by M2a only for representational differences that do not change meaning (key order, `$ref` naming), recorded in the report; anything semantic is a contract change (§11.3, §14.8).
## 4. Owned paths
From SPEC §14.7, verbatim:
- `server/Cargo.toml`, `server/rust-toolchain.toml`, `server/Dockerfile`, `server/crates/kvitt-domain/**` (owner M2a; after landing: "contract-change plans only for `kvitt-domain`; M3, M4 and M9 may add a dependency line to a crate's `Cargo.toml` (second to land resolves `Cargo.lock` on rebase)").
- `kvitt-server`: `migrations/`, `src/{main,lib,config,error,router,openapi,auth,time}.rs`, `src/db/**`, `src/wire/**`, `src/api/{mod,health,setup}.rs`, `src/cli/{mod,healthcheck}.rs` (owner M2a; after landing: "contract-change plans; M3/M4 may add one line per new module to `mod.rs` files they need").
- Exception granted by §11.3: representational (non-semantic) edits to `contracts/openapi.yaml`.
Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.
Open points the orchestrator must settle in the start prompt's notes (stop and ask if they are not settled): (a) the 501 stub handlers for E-3 to E-30, E-33 and E-31/E-32 belong in files §14.7 gives to M3 (`src/api/{invites,landing,me,pictures,devices,accounts,categories,recurring,proposal,apk}.rs`) and M4 (`src/api/sync.rs`), the subcommand stubs in `src/cli/{bootstrap,pair_device,rotate_epoch}.rs`, and the third crate in `server/crates/kvitt-import/**` (M9): whether M2a creates these files as stubs that ownership then passes on; (b) `server/Cargo.lock`, `server/crates/kvitt-server/Cargo.toml` and `server/crates/kvitt-import/Cargo.toml` are not in the ownership table; (c) names for M2a's own integration tests under `kvitt-server/tests/` (M3 has `core_*`, M4 `sync_*`, `scheduler_*`, `push_*`); (d) where the protocol-header check, body-size limits (§8.6) and the rate-limit layer that M3 writes in `src/ratelimit.rs` attach to `router.rs`; (e) how M4's scheduler and push sender get started by `serve` (`main.rs`/`lib.rs`) when M4 may only add `mod.rs` lines; (f) whether the OpenAPI equality test compares parsed values or text.
## 5. Deliverables
- `server/Cargo.toml`: workspace with members `crates/kvitt-domain`, `crates/kvitt-server`, `crates/kvitt-import`; shared lints (deny `unsafe_code`, clippy `unwrap_used`/`expect_used` outside tests).
- `server/Cargo.lock`: committed.
- `server/rust-toolchain.toml`: the Rust stable version chosen (see step 3).
- `server/Dockerfile`: the two-stage arm64 image of §10.3.
- `server/crates/kvitt-domain/Cargo.toml`, `src/lib.rs`, `src/split.rs`, `src/balance.rs`, `src/allocation.rs`, `src/settlement.rs`: types and functions of §14.5.1 and §14.5.2.
- `server/crates/kvitt-domain/tests/vectors.rs`: runs every case of the five Rust vector files in `contracts/test-vectors/`.
- `server/crates/kvitt-server/Cargo.toml`, `src/main.rs` (CLI parsing, start-up), `src/lib.rs` (public modules for `kvitt-import`), `src/config.rs` (§10.4 variables), `src/error.rs` (§14.3.2), `src/router.rs` (every route of §7, layers of §14.3.1), `src/openapi.rs` (utoipa `ApiDoc`), `src/auth.rs` (device and admin extractors, §8.2, §8.4), `src/time.rs` (fixed RFC 3339 form, `Europe/Stockholm` constant, injectable clock).
- `src/db/mod.rs`, `src/db/pool.rs` (pool with the §8.9 pragmas, `BEGIN IMMEDIATE` helper), `src/db/sequence.rs` (next `server_version`).
- `migrations/0001_init.sql`: byte copy of `contracts/schema/server-v1.sql`.
- `src/wire/{mod,user,device,invite,account,category,expense,settlement,recurring,audit,sync}.rs`: serde wire objects of §14.3.3 and §14.4 with utoipa schemas, arrays sorted as §14.3.3 says.
- `src/api/mod.rs`, `src/api/health.rs` (E-1), `src/api/setup.rs` (E-2).
- `src/cli/mod.rs` (subcommand dispatch, `serve`, `migrate`), `src/cli/healthcheck.rs`.
- 501 stubs for the remaining routes and subcommands, placed as the orchestrator decides (open point a).
- Integration tests in `kvitt-server/tests/` (names per open point c): OpenAPI document equals `contracts/openapi.yaml`; `0001_init.sql` equals `contracts/schema/server-v1.sql` byte for byte; every route returns 501 `not_implemented` with the error shape; E-1 and E-2 responses; auth extractor cases (missing, unknown, revoked token; member on admin route); protocol header 426; error JSON for every code serialises like `contracts/examples/errors/<code>.json`.
- `server/crates/kvitt-import/` minimal crate that builds (if open point a says M2a creates it).
## 6. Steps
1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m2a -b agent-m2a main`, then in `.claude/worktrees/agent-m2a`: `git reset --hard main`. No git-ignored files are needed; copy none. Run `tools/install-git-hooks.sh`.
2. Read §2 in order. Confirm `contracts/` exists on `main` (M1 landed); if not, stop and report.
3. Versions: "Rust stable" (§6.3) and `rust:<stable>-bookworm` (§10.3). Look up the current stable Rust release on the official Rust site (rust-lang.org / the Rust release blog) and the matching tag on the official Docker Hub `rust` image page; pin the same version in `rust-toolchain.toml` and the Dockerfile. Look up current stable versions of each crate you add on crates.io / docs.rs and the digest of `gcr.io/distroless/cc-debian12:nonroot`. Confirm current axum, sqlx and utoipa usage from their official docs (CLAUDE.md "Fast-moving APIs"). Record every version in the report.
4. Write `kvitt-domain` test-first: `tests/vectors.rs` loads `../../../contracts/test-vectors/{compute_split,compute_balances,propose_allocation,validate_settlement,over_settled_accounts}.json` and checks every case; then implement §14.5.2 with the error order of the enums. Run `cargo test -p kvitt-domain`.
5. Write `config.rs`, `time.rs`, `error.rs`, `db/**`, `migrations/0001_init.sql` (copy the bytes with `cp contracts/schema/server-v1.sql server/crates/kvitt-server/migrations/0001_init.sql`), `wire/**`.
6. Write `auth.rs` and `router.rs` with every route of §7, the §14.3.1 headers and limits, and stub handlers per open point (a).
7. Write `openapi.rs` with utoipa annotations on every handler and wire type; make the generated document equal `contracts/openapi.yaml` (comparison per open point f). Only representational changes to the YAML are allowed; list each in the report.
8. Implement E-1, E-2, `serve` (graceful SIGTERM drain up to 10 s, §10.4), `migrate`, `healthcheck`.
9. Write the integration tests of §5 with an in-memory or temp-file SQLite and an injected clock.
10. Write `server/Dockerfile`. Build it once locally only if no Gradle build is running (CLAUDE.md); record the image size. Do not push it anywhere.
11. Run the gate (§7) in `server/`. Commit on `agent-m2a`, `git rebase main`, rerun the gate. Never merge, push or remove the worktree.
## 7. Gate
From SPEC §11.2, verbatim: "Server gate (includes the OpenAPI match test, the migration identity test, the vector tests)".
Command, from `server/`: `cargo fmt --check && cargo clippy -- -D warnings && cargo test`.
Green means: all three exit 0 in this session; the `test result:` lines show 0 failed and include the OpenAPI match test, the migration identity test and the vector tests (each named in the report).
## 8. HUMAN steps
From SPEC §11.2, verbatim: "none".
## 9. Out of scope
- Handler bodies of E-3 to E-33 other than 501 stubs (M3: E-3 to E-30, E-33; M4: E-31, E-32).
- Invite landing page, pictures, rate-limit logic, `bootstrap`, `pair-device` (M3).
- Sync, audit, push, scheduler, `rotate-epoch`, `sync_mutations` pruning (M4).
- The Splitwise importer logic (M9).
- `infra/` compose files and image publishing (M2c-dev, M2c-pi).
- Any Android work (M2b and later).
- Semantic changes to any contract file.
## 10. Report
- Branch `agent-m2a`, head commit hash after rebasing on `main`.
- Gate output: fmt and clippy exit status, every `test result:` line, and the names of the OpenAPI match, migration identity and vector tests.
- Versions chosen (Rust, Docker base images with digests, every crate) with the official source.
- Representational changes made to `contracts/openapi.yaml`, one line each.
- How each open point (a) to (f) was resolved, as given in the start prompt.
- Numbering used (decision ids, spec version) or `TODO(spec)` occurrences.
- Contract requests, open questions, spec gaps (with section numbers).
- Docker image build result (size) if built.
## 11. Numbering
Decision ids, spec version and schema version are assigned by the orchestrator in the start prompt; use `TODO(spec)` if none was given.