SPEC v0.6: M4 decisions (D-035 to D-039), HANDOFF rewritten

- D-035 unusable mutation_id/entity_type/entity.id fails the whole push with 400; other shape errors per mutation.
- D-036 0 mutations is batch_too_large ("outside 1 to 100").
- D-037 never-synced delete runs full step-8 validation; current on every rejection; created_at kept; canonical ids.
- D-038 FCM retries: one send plus three retries (1/5/25 s), 401 drops the token, other 4xx not retried.
- D-039 pruning strictly older than 90 days; tasks::start returns AppState with the push queue, SqlAuditWriter default.
- Clarifications: step 8 order, negative since, pull read transaction, FCM JWT kid and cache, token clearing only if
  unchanged, unusable credentials, scheduler clock jumps, rotate-epoch exit codes, /i/ empty code, image build needs
  a C toolchain and CA roots (dev image rebuilt 2026-10-18).
- PLAN-M4 pruning wording, prompts/README status, HANDOFF at bd6f10a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 07:33:46 +02:00
parent 15056dc7df
commit d9dd5848fe
5 changed files with 61 additions and 61 deletions
+5 -5
View File
@@ -39,11 +39,11 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-032 | 2026-10-17 | Rate-limit keying (§8.6): per device by the hash of the bearer token, no database lookup; a 401 also counts against the client IP; tokens never accepted are refused once the IP is over its limit; `KVITT_TRUSTED_PROXY` re-resolved at most every 60 s; the client IP is the last `X-Forwarded-For` entry from the trusted proxy; `serve` must use `into_make_service_with_connect_info` (being added by M4). | M3 report: keying and proxy handling were unspecified; the limiter must not cost a database query. | SPEC §8.6, §10.4, §11.3 |
| D-033 | 2026-10-17 | Resume rule (§9.5): `last_generated_period` moves forward only when the latest past period is at or after `start_period`, so resuming a not-yet-started template keeps its start. | M3 report: the v0.4 rule would skip a future start on resume. | SPEC §9.5 |
| D-034 | 2026-10-17 | Storage normalisation: strings stored trimmed, `notes` included; PNG and WebP transparency flattened onto white; `fcm_token` non-empty or null (`""` is `invalid_request`); revoking a device clears its `fcm_token`; archive or unarchive in the state already held returns the row unchanged with no new `server_version`; a new category's `sort_order` is the maximum over non-deleted categories plus 10. | M3 report: details the spec left open. | SPEC §8.6, §9.6, AC-3.5, AC-4.4, AC-7.3, E-17, E-20, E-21, E-25, E-26 |
| D-035 | 2026-10-17 | Reserved for M4 (in flight). | | |
| D-036 | 2026-10-17 | Reserved for M4 (in flight). | | |
| D-037 | 2026-10-17 | Reserved for M4 (in flight). | | |
| D-038 | 2026-10-17 | Reserved for M4 (in flight). | | |
| D-039 | 2026-10-17 | Reserved for M4 (in flight). | | |
| D-035 | 2026-10-18 | A mutation whose `mutation_id`, `entity_type` or `entity.id` is unusable (not a canonical lowercase hyphenated UUID, or an `entity_type` other than `expense` or `settlement`) fails the whole `POST /sync` with 400 `invalid_request`, checked before the transaction; every other shape error is a per-mutation `rejected` / `validation_failed` (§14.4.4 step 3). | M4 report (`bd6f10a`): without those keys no per-mutation result (keyed by `mutation_id`, `entity_type`, `entity_id`) can be formed or stored in `sync_mutations`. | SPEC §14.4.4 |
| D-036 | 2026-10-18 | `POST /sync` with 0 mutations is 422 `batch_too_large`; the §14.3.2 meaning reads "Number of mutations in `POST /sync` outside 1 to 100". | M4 report (`bd6f10a`): §9.6 said 1 to 100 but §14.3.2 named only "more than 100". | SPEC §9.6, §14.3.2, §14.4.4 |
| D-037 | 2026-10-18 | A delete of a never-synced row (§14.4.4 step 7, no stored row) runs the full step-8 validation (field rules, domain rules, references), so the stored tombstone satisfies the schema. Every rejection carries the stored row or `null` as `current`, at steps 3 and 4 too. Step 11 keeps the stored `created_at` (a pushed one is ignored on update). Pushed ids must be canonical lowercase hyphenated UUIDs; another spelling of a valid UUID is not accepted. | M4 report (`bd6f10a`): v0.5 validated only references for such a tombstone, left `current` open at steps 3 and 4, and did not say whether `created_at` may change. | SPEC §14.4.3, §14.4.4 |
| D-038 | 2026-10-18 | FCM retry is one send plus up to three retries after 1 s, 5 s, 25 s (four sends at most) on 429, 5xx and network errors. On 401 the cached access token is dropped and the message is not retried; other 4xx are not retried. Retries run inside the single sender task, blocking it up to 31 s per device; accepted for two users. | M4 report (`bd6f10a`): "3 attempts with delays 1 s, 5 s, 25 s" was ambiguous (three delays need four sends) and 4xx handling was open. | SPEC §14.6 |
| D-039 | 2026-10-18 | `sync_mutations` pruning deletes rows whose `received_at` is strictly older than 90 days (a row exactly 90 days old is kept); PLAN-M4's "90 days and older" corrected. Seam change to D-015: `tasks::start` returns the `AppState` with the real push queue installed and `serve` builds the router from it; `AppState::new` defaults to `SqlAuditWriter`; `NoopAuditWriter` is removed; `NoopPushQueue` stays for push disabled. | M4 report (`bd6f10a`): "older than 90 days" (D-021) and PLAN-M4 disagreed at the boundary; the push queue must exist before the router is built. | SPEC §6.3, §14.1, §14.4.9, §14.6; PLAN-M4; server/README.md |
| D-040 | 2026-10-17 | A push refused as a whole (400 `invalid_request`, 413 `payload_too_large`, 409 `household_incomplete`, 422 `batch_too_large`) counts as a batch with no progress: the cycle goes on to pull and records `last_error_code`; 5xx and 429 retry with backoff; 401 and 426 stop. (Proposed by M5 as D-030, renumbered.) | M5 report: §14.4.5 covered only network errors, 5xx and 401. | SPEC §14.4.5 |
| D-041 | 2026-10-17 | `:data`'s `DataModule` provides implementations only; the interface bindings live in `DataBindingsModule` (`@DisableInstallInCheck`), which `:app` includes for release so the debug fakes do not duplicate bindings. (M5's D-031, renumbered.) | M5 report: Hilt fails on duplicate bindings when debug binds fakes. | SPEC §6.2, §11.3 (M8); PLAN-M8 |
| D-042 | 2026-10-17 | Room tests run on Robolectric with the framework SQLite driver; migration tests build from the exported schema JSON rather than `room3-testing`. (M5's D-032, renumbered.) | M5 report: no device needed for the gate. | SPEC §6.2 |