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:
+104
@@ -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.
|
||||
Reference in New Issue
Block a user