SPEC v0.3: orchestrator resolutions of the plans' open points

Records D-004 and D-015 to D-025: shells and ownership, server seams
(router middleware, rate_limit layer, tasks::start, AuditWriter and PushQueue
traits, next_occurrence_date in kvitt-domain, M3 owns recurring generation,
M4 lands after M3), OpenAPI parsed-value equality, feature convention and
strings, empty fake scenario and resolveRange exception, shared ledger rows and
editor fields in :core:design, no bundled font, pruning at 04:00, development
infrastructure as M2c-dev built it, M2c-pi allowances, release keystore,
production import procedure. Plans and prompts close their open points;
HANDOFF rewritten to the real state; CLAUDE.md: implementation agents never
edit SPEC.md or docs/decisions.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 21:23:38 +02:00
parent c885aba313
commit 76ee957f41
45 changed files with 501 additions and 342 deletions
+26 -18
View File
@@ -10,38 +10,46 @@ Implement the sync protocol (pull and push), the append-only audit log with its
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`.
7. SPEC §11.2 (row M4), §11.3, §14.7, §14.8; §6.3 "Server seams (D-015)"; `docs/decisions.md` D-004, D-015, D-021.
8. Files: `contracts/examples/sync/*.json`, `contracts/examples/push/*.json`, `contracts/test-vectors/*.json`, the M2a code under `server/` (including the shells `src/tasks/mod.rs`, `src/audit/`, `src/push/`), M3's generation function in `src/api/recurring.rs` once M3 has landed, `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).
- `sync_mutations` rows older than 90 days are pruned daily at 04:00 Europe/Stockholm (DST-aware) (§14.1, §14.4.9, D-021).
- Seams (D-015): `serve` calls `tasks::start` in `src/tasks/mod.rs` (M2a's stub, now M4's); M4 starts the scheduler, the push sender and the pruning from it. M2a defined the traits `AuditWriter` (`src/audit/`) and `PushQueue` (`src/push/`) with no-op implementations; M4 replaces the implementations with the real writer and diff and the FCM queue, keeping the trait signatures (a change to them is a contract request). M3 and M9 call the traits.
- Recurring generation (D-015): the function that generates the due periods of one template is M3's, in `src/api/recurring.rs`; M4's scheduler calls it for every template with `deleted_at IS NULL` and `paused = 0`. M4 lands after M3: if M3 has not landed when the scheduler is written, write the loop against the signature in M3's report and rebase on M3 before the gate.
- 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).
- Scheduler: tokio task inside `kvitt-server`, runs at start and daily 06:00 Europe/Stockholm (DST-aware), and generates through M3's function (candidate periods, clamping, one transaction per candidate, split resolution, audit, `last_generated_period`, push are inside that function) (§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".
From SPEC §14.7 (v0.3): `kvitt-server`: `src/api/sync.rs`, `src/tasks/**`, `src/sync/**`, `src/audit/**`, `src/push/**`, `src/scheduler/**`, `src/cli/rotate_epoch.rs`, `tests/sync_*.rs`, `tests/scheduler_*.rs`, `tests/push_*.rs` (owner M4, shells from M2a; after landing: "later server plans").
Also from §14.7: M4 may add dependency lines to a crate's `Cargo.toml` (second to land resolves `Cargo.lock` on rebase) and one line per new module to `mod.rs` files it needs.
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").
Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries in the report.
Resolved open points (SPEC v0.3; the start prompt may repeat them):
- (a) `serve` calls `tasks::start` (`src/tasks/mod.rs`, created by M2a, owned by M4); everything M4 runs in the background starts there (D-015).
- (b) The generation function is M3's; M4's scheduler calls it; M4 replaces the `AuditWriter` and `PushQueue` implementations that M3 calls; M4 lands after M3 (D-015).
- (c) Pruning runs daily at 04:00 Europe/Stockholm (D-021).
## 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/audit/**`: the real `AuditWriter` implementation and the diff of §14.4.7, used by sync, M3's generation function and the importer.
- `src/push/**`: the real `PushQueue` implementation: queue, FCM v1 sender, OAuth token cache, recipient selection, `sync_batch` aggregation.
- `src/tasks/**`: `start` launching the scheduler, the push sender and the pruning.
- `src/scheduler/**`: the daily 06:00 Europe/Stockholm tick (and one run at start) calling M3's generation function for every active template; the daily 04:00 `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/scheduler_*.rs`: the scheduler cases of §9.5: run at start, next 06:00 tick computed across both DST changes, catch-up after downtime through M3's function, paused and deleted templates skipped; the 04:00 pruning (rows of 90 days and older removed, newer kept). The per-template generation cases are M3's tests.
- `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
@@ -51,15 +59,15 @@ Open points the orchestrator must settle in the start prompt's notes: (a) how `s
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).
7. Scheduler with an injectable clock, calling M3's generation function; tests for the scheduler cases.
8. `rotate-epoch`; pruning at 04:00.
9. Wiring through `tasks::start` (resolved 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".
From SPEC §11.2 (v0.3), verbatim: "Server gate; integration tests for every sync rule of §14.4 and the scheduler cases of §9.5 (run at start, daily 06:00 on DST days, catch-up through M3's generation function); 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.
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 scheduler case to a test name with no gaps.
## 8. HUMAN steps
From SPEC §11.2, verbatim: "none".
@@ -74,8 +82,8 @@ From SPEC §11.2, verbatim: "none".
- 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.
- How resolved points (a) to (c) were applied.
- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences.
- Contract requests, open questions, spec gaps (with section numbers).
## 11. Numbering