Files
kvitt/PLAN-M4.md
T
jacobh2 5e1be38364 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>
2026-10-17 21:05:34 +02:00

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

  1. 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).
  2. SPEC §14.6 Push payload v1.
  3. SPEC §9.5 Recurring generation, §9.1 Splits (server check), §9.4 settlement validity, §9.6 Limits and validation, §9.10 Other edge cases.
  4. SPEC §4 FR-9 Recurring expenses, FR-12 Audit log and timeline, FR-13 Push notifications, FR-15 Offline and sync.
  5. SPEC §14.1 SQLite schema v1 and server-side rules; §14.3.3 Wire objects; §14.3.4 E-31, E-32.
  6. 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.
  7. SPEC §11.2 (row M4), §11.3, §14.7, §14.8.
  8. Files: contracts/examples/sync/*.json, contracts/examples/push/*.json, contracts/test-vectors/*.json, the M2a code under server/, CLAUDE.md.

3. Already established

  • Pull: 409 cursor_ahead when since > current sequence; lowest server_version > since across the seven tables via UNION ALL, tombstones included, parent with children counts as one row; limit default 500, max 1000; next_since = highest version in page or since; has_more; every array present and sorted by server_version; epoch, server_time (§14.4.2, §9.6).
  • Push: 1 to 100 mutations (batch_too_large), body ≤ 1 MiB; whole request in one BEGIN IMMEDIATE, one SAVEPOINT per 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 in sync_mutations); pushes enqueued after commit; results in request order (§14.4.3, §14.4.4).
  • Server recomputes shares with kvitt-domain and rejects mismatches validation_failed; mode default bp only checked to sum to 10000 (§9.1).
  • Audit diff fields, order and value types per §14.4.7; create lists every field with old null, update only differing fields, delete only deleted_at; client_at = pushed updated_at (delete: deleted_at); scheduler and importer use server now for both; entries never updated or deleted (§14.4.7, AC-12.1).
  • sync_mutations rows older than 90 days are pruned daily (§14.1, §14.4.9).
  • Push: data-only FCM v1 message per target device, android.priority high, ttl 86400s, 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 one POST /sync → one sync_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; UNREGISTERED or 404 clears the device's fcm_token; disabled with a start-up warning when the credentials file is missing; OAuth2 via service-account JWT (RS256) at https://oauth2.googleapis.com/token, scope https://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, mode default resolves the account's current ratio at generation, audit create with source recurring, last_generated_period bump, push recurring_expense_created; unique index makes it idempotent; pause/resume and edit rules (§9.5, AC-9.2 to AC-9.6).
  • rotate-epoch sets meta.sync_epoch to a new lower(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_batch aggregation.
  • src/scheduler/**: recurring generation of §9.5, daily 06:00 Europe/Stockholm tick, sync_mutations pruning.
  • 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 against contracts/examples/push/, recipients, sync_batch threshold, retries and delays, token clearing on UNREGISTERED/404, queue overflow, disabled without credentials, all against a local stub HTTP server.

6. Steps

  1. 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). Run tools/install-git-hooks.sh.
  2. Read §2 in order. Confirm M2a has landed; if not, stop and report.
  3. 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.
  4. Audit writer and diff first, with unit tests for each §14.4.7 rule.
  5. Push processing (§14.4.4) step by step, a test per step and per rejection reason; then pull (§14.4.2) with paging tests.
  6. Push sender with the stub server tests.
  7. Scheduler with an injectable clock; tests for every §9.5 case.
  8. rotate-epoch; pruning.
  9. Wiring into serve per open point (a).
  10. Run the gate (§7). Commit on agent-m4, git rebase main (resolve Cargo.lock if 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 on main.
  • 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.