SPEC v0.5: M3 and M5 decisions (D-026 to D-034, D-040 to D-043), HANDOFF rewritten

Records the decisions proposed by M3 (503cee5) and M5 (c28c52b). M5's proposals
D-030 to D-033 are renumbered D-040 to D-043; D-035 to D-039 are reserved for M4.
Adds the D-027 clock-skew re-stamp rule, M5's contract requests for M8 (PLAN-M8,
SPEC 11.3), the connect-info note for M4 (PLAN-M4), the picture size note (PLAN-M6),
and updates prompts/README status.

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-17 22:21:43 +02:00
parent 8f0e156b38
commit 853c038c58
7 changed files with 98 additions and 60 deletions
+18
View File
@@ -30,3 +30,21 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-023 | 2026-10-17 | M2c-pi: key-based SSH to the Kvitt Pi and the Gitea credentials are HUMAN steps; the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive are requested from the owner at the start of that sprint; the Gitea Actions workflow lives at `.gitea/workflows/`, owned by M2c-pi; secret files on the Pi (`ha_ssh_key`, `ha_known_hosts`, `rclone.conf`) mode 0600 owned by uid 1000, the uid of the `cert-sync` and `backup` containers; adding `gitea.lan` to `insecure-registries` is its own HUMAN step. | PLAN-M2c-pi open points (a) to (c). | SPEC §10.2, §10.3, §11.2, §11.3, §14.7; PLAN-M2c-pi |
| D-024 | 2026-10-17 | Release keystore: `keytool -genkeypair -v -keystore kvitt-release.jks -alias kvitt -keyalg RSA -keysize 4096 -validity 10000`, created by the owner and kept outside the repository (was `android/keystore/release.jks`); `local.properties` holds its absolute path. | §10.6 did not fix the key parameters (PLAN-M10 H-1); a keystore outside every checkout cannot be committed or deleted with a worktree. | SPEC §10.6, §11.2, §11.3; PLAN-M10 |
| D-025 | 2026-10-17 | Production Splitwise import (M11): a backup first; user ids resolved by display name from the database; CSVs copied to the Pi into `/opt/kvitt/import/`, bind-mounted read-only into `server` as `/import`; the import runs as `docker compose run --rm --entrypoint kvitt-import server ...` with the server stopped (`--entrypoint` because the image's entrypoint is `kvitt-server`); CSVs deleted afterwards. The category table stays at `server/crates/kvitt-import/data/splitwise_categories.csv`. | PLAN-M11 open points (a) to (c); PLAN-M9 table location. | SPEC §10.2, §10.8, §11.2, §11.3; PLAN-M2c-pi, PLAN-M9, PLAN-M11 |
| D-026 | 2026-10-17 | A 409 `cursor_ahead` on pull is treated as epoch recovery: cursor 0, epoch cleared, every local expense and settlement marked pending with a new `pending_mutation_id`, cycle restarted. | M5 report (`c28c52b`): a cursor ahead of the server means the server lost rows, as after a restore without `rotate-epoch`. | SPEC §14.4.2 |
| D-027 | 2026-10-17 | A `clock_skew` rejection gives the row a new `pending_mutation_id` and the row stays pending (the server stored the rejection under the old id and would replay it). Orchestrator addition: on the first sync attempt after a `clock_skew` rejection, the phone re-stamps `updated_at` of each such pending row to its current clock before pushing, so a corrected clock heals the row. The re-stamp is not in `:data` at `c28c52b`: contract request for M8 or a small `:data` follow-up (HANDOFF open work). | M5 report; without the re-stamp a row stamped by a wrong clock would stay rejected after the clock is fixed. | SPEC §14.4.5 |
| D-028 | 2026-10-17 | Periodic sync work is named `kvitt-sync-periodic` (policy `KEEP`); one-off work keeps `kvitt-sync` (`APPEND_OR_REPLACE`). | M5 report: WorkManager cannot share one unique name between periodic and one-off work. | SPEC §14.4.1, AC-15.3 |
| D-029 | 2026-10-17 | A REST call that fails on the network returns `OnlineResult.Failed("network", null)`; `network` is the one allowed code outside §14.3.2. | M5 report: the UI must tell "Could not reach the server" (§9.9) from `Offline` (call not attempted). | SPEC §14.2.4 |
| D-030 | 2026-10-17 | Picture version after a removal: E-10 deletes the JPEGs and leaves an empty `<v>-removed` marker in `pictures/<user_id>/`; the next upload uses the highest version on disk plus one, so caches keyed by (`user_id`, `picture_version`) never show a removed picture; the first upload is version 1. | M3 report (`503cee5`): `picture_version` is null after a removal, so the next version needs a record. | SPEC AC-4.4, §10.4, E-10 |
| D-031 | 2026-10-17 | Failed invite accept (AC-2.12): any 4xx answer other than 429 counts; after 10 failures further accepts from that IP are refused with `rate_limited` until 60 minutes after the first counted failure; windows are fixed from the first counted request. | M3 report: AC-2.12 did not define "failed" or the window. | SPEC AC-2.12, §8.6 |
| 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-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 |
| D-043 | 2026-10-17 | Two extra DataStore keys: the pairing status in `auth.preferences_pb`, and the installed version code that received a 426 in `prefs.preferences_pb` (syncing stops until the installed version changes). The token is encrypted with Keystore key `kvitt_token_key`, AES-256-GCM (as §8.2). (M5's D-033, renumbered.) | M5 report. | SPEC §8.2, §14.2.2, §14.4.5 |