Files
kvitt/docs/decisions.md
T
jacobh2 76ee957f41 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>
2026-10-17 21:23:38 +02:00

10 KiB

Decisions

One entry per decision that changes or refines SPEC.md. Numbered, dated, never deleted: a revised decision gets a new entry and the old one a strike-through plus a pointer. The spec is updated in the same change.

# Date Decision Why Where it lives
D-001 2026-10-17 Development-first deployment. During development the server runs locally on the owner's M4 Mac (Docker compose infra/dev/compose.yaml or cargo run) and debug builds pair against it over the home Wi-Fi. M2c splits into M2c-dev (Mac compose stack, Firebase setup, local backup job; runs now) and M2c-pi (Pi provisioning, cert-sync, router forward 8443, Gitea registry and Gitea Actions, Drive backups; after M8, before M10). The registry is Gitea at http://gitea.lan (plain HTTP, home Wi-Fi only); the owner provides an example Gitea Actions workflow later. OQ-1 answered; OQ-3 now blocks only M2c-pi. Owner wants to develop against a local server and defer the Pi. SPEC §10 (intro, §10.1, §10.3, §10.7), §11.1, §11.2, §11.3, §12, §14.7; PLAN-M2c-dev, PLAN-M2c-pi
D-002 2026-10-17 Backup retention on the Pi: local snapshot files are deleted as soon as their upload to Drive is confirmed (rclone lsf), not after 30 days. Drive keeps everything. Owner's choice; the Pi's disk is small and Drive is the archive. SPEC §10.9 step 5; PLAN-M2c-pi
D-003 2026-10-17 Splitwise CSV format confirmed from two real exports (OQ-2 answered). English variant header Date,Description,Category,Cost,Currency,Amelie Bennich,Jacob Hagstedt (group "hemma", account "Hemma"); Swedish variant header Datum,Beskrivning,Kategori,Kostnad,Valuta,Jacob Hagstedt,Amelie Bennich (general group, account "Vardag", about 1750 rows). Member columns differ in order, so the importer reads member names from the header. ISO dates, point decimal, double-quoted fields with commas, trailing empty line then Total balance / Totalsumma. Payment category Payment / Betalning. All rows SEK. Swedish category names are mapped through a committed table (unknown names to other, counted as a warning). The partner's display name is Amelie. The CSV files stay in export/ (git-ignored), never committed or quoted. Real exports differ by Splitwise UI language. SPEC §10.8, §12; PLAN-M9 (mapping notes), PLAN-M11
D-004 2026-10-17 Shells and ownership. M2a creates shell files in M3, M4 and M9 paths (501 handlers, stub subcommands bootstrap, pair-device, rotate-epoch, a kvitt-import crate whose main.rs prints usage); M2b creates a module shell for every module of §6.2 with one empty entry composable per feature. The later milestone owns each shell from the moment it lands and writes the body. M2a owns server/Cargo.lock, every Cargo.toml, main.rs, lib.rs, config, serve and migrate, router.rs and its own tests (any tests/*.rs without the core_, sync_, scheduler_, push_ prefixes); later milestones may add dependency lines to Cargo.toml. PLAN-M2a and PLAN-M2b open points (a), (b), (c): the skeletons need files in paths §14.7 gave to later milestones. SPEC §11.2, §11.3, §14.7; PLAN-M2a, PLAN-M2b, PLAN-M3, PLAN-M4, PLAN-M6, PLAN-M7a to M7f, PLAN-M9
D-015 2026-10-17 Server seams. The X-Kvitt-Protocol 426 check, the X-Kvitt-Server-Version header and the §8.6 body limits are layers in router.rs (M2a). M3's rate limit is a layer in src/api/rate_limit.rs (replacing src/ratelimit.rs) attached at one line in router.rs that M2a marks. M2a creates src/tasks/mod.rs with a start function called from serve; M4 owns src/tasks/**. M2a implements next_occurrence_date and the day clamping in kvitt-domain (Rust only, src/recurring.rs) and defines the traits AuditWriter (src/audit/) and PushQueue (src/push/) with no-op implementations; M4 replaces the implementations, M3 and M9 call the traits. Filled in by the docs agent: M3 writes the per-template recurring generation function in src/api/recurring.rs (E-28, E-29 and M4's scheduler call it), M3 tests the §9.5 generation cases, M4 the scheduler cases, and M4 lands after M3. PLAN-M2a (d), (e); PLAN-M3 (a), (c); PLAN-M4 (a), (b): M3 and M4 both need §9.5 logic, audit and push while running in parallel. SPEC §6.3, §9.5, §11.1, §11.2, §14.5, §14.7; PLAN-M2a, PLAN-M3, PLAN-M4, PLAN-M9
D-016 2026-10-17 The OpenAPI equality test compares parsed values, not text. Representational edits to contracts/openapi.yaml (key order, $ref naming, description wording) need no spec bump or decision entry; they are listed in the plan report. PLAN-M2a open point (f); utoipa output differs from hand-written YAML in form only. SPEC §11.3, §14.8; CLAUDE.md (Contracts are versioned); PLAN-M2a
D-017 2026-10-17 Feature convention. Each feature exposes one composable entry point per screen taking a hiltViewModel()-provided ViewModel and navigation as plain lambdas; features never reference other features or the NavHost; :app owns the NavHost; repositories are injected through the :core:model interfaces, bound in :app to :data (fakes only in test and debug-preview source sets). The §14.5.4 analytics types live in :core:model, package se.hagstedt.kvitt.model.analytics. Strings: :app holds only the app name, :core:design the shared strings (the notification copy and channel name included), each feature its own; lint MissingTranslation is an error in every module; keys are prefixed by the feature name (§14.7 wins over §13.4, which said "by screen"); :core:design keys never start with a feature name. PLAN-M2b open points (c), (e), (f); M7a to M7f need one convention fixed in build-logic. SPEC §6.2, §13.4, §14.5.4, §14.7; PLAN-M2b, PLAN-M6, PLAN-M7a to M7f, PLAN-M8, PLAN-U2
D-018 2026-10-17 The empty fake scenario: household complete (two named users, no pictures), no accounts, exactly the ten default categories, no expenses, settlements, templates, rejections or pending changes. resolveRange with CUSTOM and a null start or end throws a typed subclass of IllegalArgumentException declared in :domain (named by M2b); the caller maps it to validation_failed. PLAN-M2b open point (g) and the resolveRange signalling left open in v0.2. SPEC §6.2, §11.3, §14.5.4; PLAN-M2b
D-019 2026-10-17 Shared UI needed by two features is built in :core:design by M6: the ledger rows (expense and settlement rows, used by S-5, S-12 and the pending list of S-16) and the expense editor fields (used by S-7 and S-19). Features never copy each other's components. PLAN-M6 open point (a); feature modules cannot depend on each other. SPEC §6.2, §11.2, §11.3; PLAN-M6, PLAN-M7b, M7c, M7e, M7f
D-020 2026-10-17 No bundled font: the app uses the Material 3 default typeface. No licence step. PLAN-M6 open point (b); avoids a font licence clearance. SPEC §6.2, §11.2, §13.4; PLAN-M2b, PLAN-M6, PLAN-U1, PLAN-U2
D-021 2026-10-17 sync_mutations pruning runs daily at 04:00 Europe/Stockholm (DST-aware), deleting rows whose received_at is older than 90 days. PLAN-M4 open point (c); the spec said only "daily". SPEC §14.1 (rules text, not the DDL), §14.4.9, §11.2; PLAN-M4
D-022 2026-10-17 Development infrastructure (M2c-dev, landed c885aba). No new CLI installs: Firebase project and Android apps through the Firebase Management REST API with a gcloud access token, no Firebase CLI; rclone belongs to M2c-pi. The agent may write git-ignored secrets, google-services.json included, under infra/secrets/; M8 copies it into android/app/ with infra/dev/copy-google-services.sh. Phone health check against the dev server is a HUMAN step. Gate: docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config; shellcheck $(find infra -name '*.sh'). As built: dev stack without Caddy or TLS, bind-mounted data folder, the whole infra/secrets/ mounted read-only at /secrets (a missing key file means push disabled), env file infra/dev/.env.example (the Pi's stays infra/env.example for M2c-pi), kvitt-server healthcheck used by compose, backups with sqlite3 .backup (§10.9). The server must start when KVITT_TRUSTED_PROXY does not resolve. Firebase on kvitt-1337 with apps se.hagstedt.kvitt and se.hagstedt.kvitt.debug; service account kvitt-fcm@kvitt-1337.iam.gserviceaccount.com with roles/firebasecloudmessaging.admin (roles/firebasemessaging.admin does not exist). Owner's tools already on the Mac; M2c-dev report. SPEC §10 intro, §10.4, §10.7, §11.2, §11.3, §14.7; PLAN-M2c-dev, PLAN-M8
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