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>
14 KiB
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
- SPEC §6.3 Server (stack and crate table) and §6.1 System view.
- SPEC §7 API outline.
- 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).
- SPEC §14.1 SQLite schema v1 and its server-side rules.
- 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).
- SPEC §9.1 Splits, §9.3 Balances, §9.4 Settlement allocation proposal, §9.6 Limits and validation.
- 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.
- SPEC §10.3 Images, registry and build (the
serverimage), §10.4 Server configuration and CLI. - SPEC §4 FR-20 Health and AC-1.5 (setup status).
- 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.
- 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 onserde,serde_jsonfor tests,uuid),kvitt-server(binary pluslib.rs),kvitt-import(binary, depends onkvitt-domainandkvitt-serveras a library) (§6.3, CLAUDE.md Server layering). migrations/0001_init.sqlis byte-identical tocontracts/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 isBEGIN IMMEDIATE;server_versioncomes fromUPDATE 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, 401unauthorized/device_revoked,last_seen_atandapp_version_codeupdated at most once per hour per device; admin-only routes 403forbiddenfor members (§8.2, §8.4). - Every route of §7 is registered up front with a handler returning 501
not_implemented, and the utoipaApiDoclists them all, so M3 and M4 only fill in handler bodies in their own files (§11.2, §11.3). GET /healthreturns{status, server_version, schema_version, db_ok}, 503degradedwhenSELECT 1fails, unauthenticated, under 100 ms on the Pi (FR-20, §14.3.4 E-1).GET /setup/statusreturns{"initialized": bool}(AC-1.5).- Environment variables are exactly those of §10.4 with their defaults;
Europe/Stockholmis a code constant. Subcommands are exactlyserve,bootstrap,pair-device,rotate-epoch,healthcheck,migrate(§10.4); M2a implementsserve,migrate,healthcheck. server/Dockerfile: stage 1rust:<stable>-bookwormbuilds both binaries release--lockedforaarch64-unknown-linux-gnu(notarget-cpu=native); stage 2gcr.io/distroless/cc-debian12:nonrootarm64, binaries in/usr/local/bin/, user 10001, entrypointkvitt-server, commandserve(§10.3).- Every response carries
X-Kvitt-Server-Version; authenticated endpoints requireX-Kvitt-Protocol: 1else 426protocol_unsupported; unknown request fields ignored; missing/wrong fields 400invalid_request; path ids that are not UUIDs 404not_found; optional response fields present asnull(§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()outsidemainstart-up, nounsafe(CLAUDE.md). - M1 has landed (commit
c6df2ab):contracts/openapi.yamlexists 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.2not_implementedon 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.yamlmay be changed by M2a only for representational differences that do not change meaning (key order,$refnaming), 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 forkvitt-domain; M3, M4 and M9 may add a dependency line to a crate'sCargo.toml(second to land resolvesCargo.lockon 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 tomod.rsfiles 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 memberscrates/kvitt-domain,crates/kvitt-server,crates/kvitt-import; shared lints (denyunsafe_code, clippyunwrap_used/expect_usedoutside 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 incontracts/test-vectors/.server/crates/kvitt-server/Cargo.toml,src/main.rs(CLI parsing, start-up),src/lib.rs(public modules forkvitt-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(utoipaApiDoc),src/auth.rs(device and admin extractors, §8.2, §8.4),src/time.rs(fixed RFC 3339 form,Europe/Stockholmconstant, injectable clock).src/db/mod.rs,src/db/pool.rs(pool with the §8.9 pragmas,BEGIN IMMEDIATEhelper),src/db/sequence.rs(nextserver_version).migrations/0001_init.sql: byte copy ofcontracts/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 equalscontracts/openapi.yaml;0001_init.sqlequalscontracts/schema/server-v1.sqlbyte for byte; every route returns 501not_implementedwith 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 likecontracts/examples/errors/<code>.json. server/crates/kvitt-import/minimal crate that builds (if open point a says M2a creates it).
6. Steps
- 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. Runtools/install-git-hooks.sh. - Read §2 in order. Confirm
contracts/exists onmain(M1 landed); if not, stop and report. - 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 Hubrustimage page; pin the same version inrust-toolchain.tomland the Dockerfile. Look up current stable versions of each crate you add on crates.io / docs.rs and the digest ofgcr.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. - Write
kvitt-domaintest-first:tests/vectors.rsloads../../../contracts/test-vectors/{compute_split,compute_balances,propose_allocation,validate_settlement,over_settled_accounts}.jsonand checks every case; then implement §14.5.2 with the error order of the enums. Runcargo test -p kvitt-domain. - Write
config.rs,time.rs,error.rs,db/**,migrations/0001_init.sql(copy the bytes withcp contracts/schema/server-v1.sql server/crates/kvitt-server/migrations/0001_init.sql),wire/**. - Write
auth.rsandrouter.rswith every route of §7, the §14.3.1 headers and limits, and stub handlers per open point (a). - Write
openapi.rswith utoipa annotations on every handler and wire type; make the generated document equalcontracts/openapi.yaml(comparison per open point f). Only representational changes to the YAML are allowed; list each in the report. - Implement E-1, E-2,
serve(graceful SIGTERM drain up to 10 s, §10.4),migrate,healthcheck. - Write the integration tests of §5 with an in-memory or temp-file SQLite and an injected clock.
- 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. - Run the gate (§7) in
server/. Commit onagent-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_mutationspruning (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 onmain. - 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.