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>
9.0 KiB
PLAN-M4: Server sync, audit, push, scheduler
1. Goal
Implement the sync protocol (pull and push), the append-only audit log with its field diff, sync_mutations idempotency and pruning, the FCM HTTP v1 sender, the recurring-expense scheduler and rotate-epoch. Every rule of §14.4 and every case of §9.5 has an integration test.
2. Read first
- SPEC §14.4 Sync protocol v1: 14.4.1 Principles, 14.4.2 Pull, 14.4.3 Push, 14.4.4 Server processing of one mutation, 14.4.6 Rejection reasons, 14.4.7 Audit diff, 14.4.8 Epoch recovery and re-pairing, 14.4.9 Limits and timing (14.4.5 for the phone's expectations).
- SPEC §14.6 Push payload v1.
- SPEC §9.5 Recurring generation, §9.1 Splits (server check), §9.4 settlement validity, §9.6 Limits and validation, §9.10 Other edge cases.
- SPEC §4 FR-9 Recurring expenses, FR-12 Audit log and timeline, FR-13 Push notifications, FR-15 Offline and sync.
- SPEC §14.1 SQLite schema v1 and server-side rules; §14.3.3 Wire objects; §14.3.4 E-31, E-32.
- 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. - SPEC §11.2 (row M4), §11.3, §14.7, §14.8.
- Files:
contracts/examples/sync/*.json,contracts/examples/push/*.json,contracts/test-vectors/*.json, the M2a code underserver/,CLAUDE.md.
3. Already established
- Pull: 409
cursor_aheadwhensince> current sequence; lowestserver_version > sinceacross the seven tables viaUNION ALL, tombstones included, parent with children counts as one row;limitdefault 500, max 1000;next_since= highest version in page orsince;has_more; every array present and sorted byserver_version;epoch,server_time(§14.4.2, §9.6). - Push: 1 to 100 mutations (
batch_too_large), body ≤ 1 MiB; whole request in oneBEGIN IMMEDIATE, oneSAVEPOINTper 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 insync_mutations); pushes enqueued after commit; results in request order (§14.4.3, §14.4.4). - Server recomputes shares with
kvitt-domainand rejects mismatchesvalidation_failed; modedefaultbp only checked to sum to 10000 (§9.1). - Audit diff fields, order and value types per §14.4.7;
createlists every field witholdnull,updateonly differing fields,deleteonlydeleted_at;client_at= pushedupdated_at(delete:deleted_at); scheduler and importer use server now for both; entries never updated or deleted (§14.4.7, AC-12.1). sync_mutationsrows older than 90 days are pruned daily (§14.1, §14.4.9).- Push: data-only FCM v1 message per target device,
android.priorityhigh,ttl86400s, 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 onePOST /sync→ onesync_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;
UNREGISTEREDor 404 clears the device'sfcm_token; disabled with a start-up warning when the credentials file is missing; OAuth2 via service-account JWT (RS256) athttps://oauth2.googleapis.com/token, scopehttps://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, modedefaultresolves the account's current ratio at generation, auditcreatewithsourcerecurring,last_generated_periodbump, pushrecurring_expense_created; unique index makes it idempotent; pause/resume and edit rules (§9.5, AC-9.2 to AC-9.6). rotate-epochsetsmeta.sync_epochto a newlower(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".
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").
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_batchaggregation.src/scheduler/**: recurring generation of §9.5, daily 06:00 Europe/Stockholm tick,sync_mutationspruning.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/push_*.rs: payloads for every type againstcontracts/examples/push/, recipients,sync_batchthreshold, retries and delays, token clearing onUNREGISTERED/404, queue overflow, disabled without credentials, all against a local stub HTTP server.
6. Steps
- Worktree. From the main checkout
/Users/jhag/projects/kvitt:git worktree add .claude/worktrees/agent-m4 -b agent-m4 main, then in.claude/worktrees/agent-m4:git reset --hard main. No git-ignored files are needed (the FCM key is not used; tests use a stub). Runtools/install-git-hooks.sh. - Read §2 in order. Confirm M2a has landed; if not, stop and report.
- Versions: crates added (reqwest with rustls, jsonwebtoken, chrono-tz, a stub HTTP server for tests if not axum itself) at latest stable from crates.io / docs.rs; confirm the FCM HTTP v1 request and error format and the OAuth2 service-account flow from Google's official Firebase documentation; record each.
- Audit writer and diff first, with unit tests for each §14.4.7 rule.
- Push processing (§14.4.4) step by step, a test per step and per rejection reason; then pull (§14.4.2) with paging tests.
- Push sender with the stub server tests.
- Scheduler with an injectable clock; tests for every §9.5 case.
rotate-epoch; pruning.- Wiring into
serveper open point (a). - Run the gate (§7). Commit on
agent-m4,git rebase main(resolveCargo.lockif 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".
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.
8. HUMAN steps
From SPEC §11.2, verbatim: "none".
9. Out of scope
- REST endpoints E-2 to E-30 and E-33, landing page, pictures, rate limiting,
bootstrap,pair-device(M3). - The importer (M9); real FCM sends to devices (M8, M10);
infra/; Android. - Changing the sync protocol or push payload (contract change).
10. Report
- Branch
agent-m4, head commit hash after rebasing onmain. - 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. - Contract requests, 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.