diff --git a/.gitignore b/.gitignore index 38f082c..017aceb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ config.toml .DS_Store spa/node_modules/ -spa/dist/ \ No newline at end of file +spa/dist/ +.claude/ \ No newline at end of file diff --git a/CODE_STYLE.md b/CODE_STYLE.md index 48e4f72..a751c34 100644 --- a/CODE_STYLE.md +++ b/CODE_STYLE.md @@ -215,20 +215,15 @@ Constants are only for true invariants that never change. Tunable values belong ## Comments -Zero comments unless explaining **why** something non-obvious is done. +No comments. Not even to explain why — a comment is noise that drifts out of date while the code moves on. -```rust -// YES — explains a non-obvious constraint -// BLAKE3 hash, not SHA-256, because iroh uses BLAKE3 for content addressing -let hash = blake3::hash(&audio_data); +When something non-obvious needs saying, put it somewhere that cannot rot silently: -// NO — restates what the code does -// Create a new track with the given metadata -let track = Track::new(title, artist_id, duration, hash); +- **A name.** Rename the function, the variable, or the type until the reason is visible in the code. +- **A test.** A constraint worth a comment is worth a test that fails when someone breaks it. `media_is_resolved_before_the_cascade_runs` outlives any note explaining why the order matters. +- **An ADR.** Architectural reasoning belongs in `docs/adr/`, where it is versioned and discoverable. -// NO — references the task/ticket -// Added for thesis requirement §3.2 -``` +This applies to doc comments too. ## Dependencies @@ -240,24 +235,19 @@ let track = Track::new(title, artist_id, duration, hash); - Adapters: free to pull in platform crates (`axum`, `sqlx`, `iroh`, `nats`, etc.) - Bootstrap: wiring only -## Generics Over Trait Objects +## Trait Objects for Ports -Use generics (static dispatch) for ports, not `dyn Trait`: +Ports are `dyn` behind `Arc`. Static dispatch is not worth the ergonomic cost here — use cases hold their dependencies as trait objects: ```rust -// YES — zero-cost, monomorphized -pub struct StreamTrackUseCase { - transport: T, - repository: R, -} - -// NO — heap allocation, dynamic dispatch overhead -pub struct StreamTrackUseCase { - transport: Box, - repository: Box, +pub struct Deps { + pub entries: Arc, + pub events: Arc, } ``` +Prefer a closed enum over `dyn` only where exhaustiveness is the point — when adding a variant must force every consumer to handle it (`MetricKind`, `CorrelationStrategy`). That is a decision about compiler-enforced coverage, not about dispatch cost. + ## Testing ### Coverage Targets @@ -283,6 +273,16 @@ crates/domain/ └── track_test.rs ``` +### Fakes Must Not Be Kinder Than Production + +A fake that is more forgiving than the real adapter turns a test into a lie: it passes whichever way the code is written, which is worse than having no test. Two rules follow. + +**A fake must reproduce the constraints the database enforces.** SQLite removes dimension rows through `ON DELETE CASCADE`, so `InMemoryStore`'s cascade clears the dimension stores registered with it via `cascades_to`. Without that, the ordering requirement in `delete_entries_by_date_range` — resolve media *before* the cascade, or every blob is orphaned — could not be tested at all. + +**A fake must be able to fail.** Every port whose failure is meant to degrade rather than propagate needs a fake that refuses: `FakeMediaStorage::refusing_to_delete`, `FakeNowPlaying::failing`, `FakeRecordingLookup::failing`, `FakeWeatherLookup::failing`, `RefusingRejectionTrace`, `RefusingApiTokenStore`. A best-effort path with no failing test is a path that has never run. + +When a claim cannot be observed through the fake, move the test to where the real thing runs rather than asserting it against the fake's own behaviour. + ### Test Naming Tests read as specifications: diff --git a/CONTEXT.md b/CONTEXT.md index d5825b2..52b2290 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,9 +7,13 @@ A personal mood tracking journal. Users log how they feel throughout the day, ta ### Core **MoodEntry**: -A single mood record — the aggregate root. Every MoodEntry has exactly one Mood and belongs to exactly one User. May optionally include Activities, Content, photos, and voice memos. Multiple MoodEntries per day are allowed. +A single mood record — the aggregate root. Carries exactly one Mood, the instant it was logged, and nothing else. Belongs to exactly one User. Everything else attaches as EntryDimensions. Multiple MoodEntries per Date are allowed. _Avoid_: Log, journal entry, record, mood log +**EntryDimension**: +An optional aspect of a MoodEntry, stored independently of it — Content, Activities, photos, voice memos, weather, location, song. Each kind is self-contained: it owns its own type, its own storage, and its own validation, and knows nothing about the others. A MoodEntry is complete without any of them. +_Avoid_: Facet, attribute, extra, metadata, attachment + **Mood**: One of five discrete states representing how the user feels, mapped to a 1–5 ordinal scale: Awful (1), Bad (2), Meh (3), Good (4), Rad (5). The ordering is a domain truth — Rad is better than Good. Required on every MoodEntry. _Avoid_: Feeling, emotion, state, score @@ -29,11 +33,131 @@ _Avoid_: Group, section, type ### People **User**: -A registered account identified by username and email. Has a role (Admin or User) and an optional timezone for analytics display. Owns their own Activity catalog, MoodEntries, and Reminders. +A registered account identified by username and email. Has a role (Admin or User) and a Timezone. Owns their own Activity catalog, MoodEntries, and Reminders. _Avoid_: Account, member, profile +**Timezone**: +The IANA zone a User lives their days in. It is what turns an instant into a Date, so anything day-shaped — the calendar, a streak, a DailyMetric — is unanswerable without it. Clients set it from the platform; a User who somehow has none is told to set one rather than being given a silently wrong answer. +_Avoid_: TZ, offset, locale, region + ### Scheduling **Reminder**: A per-user notification schedule. Each Reminder defines an `Option