6dce4c73a9
Records the decisions proposed in the M2a (fdf2093) and M2b (fad6fc9) reports: D-005 OpenAPI equality normalisations and CommonRules, D-006 bp_sum_not_10000 token, D-007 per-crate no-unwrap deny, D-010 Roborazzi, D-011 IncompleteCustomRangeException, D-012 Room 3 with M2b's entities and 1.json as a contract file, D-013 JDK 25 Robolectric flags, D-014 failOnNoDiscoveredTests (D-008, D-009 unused). Clarifications in §6, §8.2, §8.6, §9.6, §10.3, §10.4, §11, §14.2, §14.3, §14.5, §14.7, §14.8 and AC-11.7. PLAN-M5 builds on the existing entities; v0.4 readings added to PLAN-M3, M5, M7b to M7f. HANDOFF rewritten: next free D-040, spec v0.5, server schema 2, Room schema 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
14 KiB
14 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-005 | 2026-10-17 | The OpenAPI equality test (server/crates/kvitt-server/tests/openapi_contract.rs) parses both documents and compares values after eight normalisations: key order ignored; local $ref inlined, discriminator mappings included; description and example keywords dropped; nullable type arrays equal oneOf with null, nested oneOf flattened and compared as sets; const equals a single-value enum; required: false and deprecated: false dropped, path parameters always required; parameters keyed by (in, name); each operation compared on its effective security. The document-wide rules of §14.3.1 and §14.3.2 live in one place, the CommonRules modifier in src/openapi.rs. |
M2a report (fdf2093): utoipa's output differs from the hand-written YAML only in form; D-016 needed the exact list. |
SPEC §11.3, §14.8 |
| D-006 | 2026-10-17 | The §14.5.3 token for a wrong basis-point sum is bp_sum_not_10000. The Rust variant SplitError::BpSumNot10000 carries an explicit #[serde(rename = "bp_sum_not_10000")], because rename_all = "snake_case" gives bp_sum_not10000. The Kotlin side must emit the same token. |
M2a report: the §14.5.1 snippet would not produce the token the vectors expect. | SPEC §14.5.1, §14.5.3 |
| D-007 | 2026-10-17 | The no-unwrap/expect rule for server non-test code is enforced per crate with #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] at each crate root. |
M2a report: a rule the gate checks instead of one reviewers remember. | CLAUDE.md (Architecture rules) |
| D-008 | 2026-10-17 | Unused (reserved for M2a and M2b; neither needed it). | ||
| D-009 | 2026-10-17 | Unused (reserved for M2a and M2b; neither needed it). | ||
| D-010 | 2026-10-17 | Screenshot tests use Roborazzi 1.76.0 on Robolectric 4.17 with @Config(sdk = [36]); roborazzi.test.verify=true in android/gradle.properties makes check compare with the committed goldens; re-record with tools/gradle-serial.sh recordRoborazziDebug. |
M2b report (fad6fc9): AGP's Compose Preview Screenshot Testing plugin was still alpha. |
SPEC §6.2, §11.2, §11.3; android/README.md |
| D-011 | 2026-10-17 | resolveRange with CUSTOM and a null start or end throws IncompleteCustomRangeException (subclass of IllegalArgumentException) declared in :domain; callers map it to validation_failed. ALL_TIME with the earliest expense after today gives today..today. Completes D-018. |
M2b report: D-018 left the name to M2b. | SPEC §11.3, §14.5.4 |
| D-012 | 2026-10-17 | Room 3 (androidx.room3 3.0.3, KSP only, SQLiteDriver with sqlite-bundled; migrations take an SQLiteConnection) instead of Room 2. M2b created the Room entities and KvittDatabase version 1 in :data; the exported schema android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json is a contract file (§14.8). M5 builds on the existing entities; changing them or the exported schema is a contract change. |
M2b report: §14.2 named Room without a major version; M2b chose Room 3 and created the entities and version 1 with the skeleton, so §11.2 and PLAN-M5 had to follow. | SPEC §6.2, §11.2 (M2b, M5), §11.3, §14.2, §14.2.3, §14.7, §14.8; PLAN-M5 |
| D-013 | 2026-10-17 | Android unit tests run with --add-exports java.base/jdk.internal.access and --add-opens java.base/java.io on JDK 25 for Robolectric, set in build-logic for every Android module. |
M2b report: needed by Robolectric on the pinned JDK 25. | SPEC §6.2 |
| D-014 | 2026-10-17 | Gradle's failOnNoDiscoveredTests is off only for modules without src/test (the shells) and on everywhere else. |
M2b report: the shells have no tests yet; in every other module a test task that discovers nothing must fail the gate. | SPEC §6.2 |
| 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: IncompleteCustomRangeException, D-011); 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 |