5e1be38364
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>
83 lines
9.0 KiB
Markdown
83 lines
9.0 KiB
Markdown
# 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.
|