feat(event): an event keeps at least one person #258

Merged
nalum merged 1 commit from feat/last-attendee-guard into main 2026-08-26 18:37:40 +00:00
Owner

Implements the server half of #257: the last attendee cannot leave — leaving an event, or one occurrence of it, is a delete when you are the only person on it.

  • domain.RemoveMember returns the new ErrLastMember (FailedPrecondition, code LAST_MEMBER) when a removal would empty the guest list; the message names delete as the way out. RemoveUser and LeaveOccurrence inherit the guard through that one seam, and on a still-derived slot the refusal lands before the split is minted, so a refused leave costs no permanent detach.
  • Update's guest-list rewrite applies the same verdict inline — it may not be the path that empties a list RemoveUser refuses to empty. Create is untouched: a Birthday starts with nobody on it by design.
  • ADR-0036 gains the #257 amendment (and its old last-person trade-off paragraph is struck): ownerless events stay admin-only, the state is prevented from arising instead, and the accidental-join consequence is recorded — deleting the child row re-derives the slot, which is the right undo.
  • Coverage: domain unit tests, service specs for all three doors (refusal + rollback pinned), and integration specs including the two-attendee leave and the sole-person refusal.

Stacked on #209 (the branch sits on the current stack tip). Design: calendar handoff §0.1 + acceptance checklist, ruled in #257.

🤖 Generated with Claude Code

Implements the server half of #257: the last attendee cannot leave — leaving an event, or one occurrence of it, is a delete when you are the only person on it. - `domain.RemoveMember` returns the new `ErrLastMember` (`FailedPrecondition`, code `LAST_MEMBER`) when a removal would empty the guest list; the message names delete as the way out. `RemoveUser` and `LeaveOccurrence` inherit the guard through that one seam, and on a still-derived slot the refusal lands before the split is minted, so a refused leave costs no permanent detach. - `Update`'s guest-list rewrite applies the same verdict inline — it may not be the path that empties a list `RemoveUser` refuses to empty. `Create` is untouched: a Birthday starts with nobody on it by design. - ADR-0036 gains the #257 amendment (and its old last-person trade-off paragraph is struck): ownerless events stay admin-only, the state is prevented from arising instead, and the accidental-join consequence is recorded — deleting the child row re-derives the slot, which is the right undo. - Coverage: domain unit tests, service specs for all three doors (refusal + rollback pinned), and integration specs including the two-attendee leave and the sole-person refusal. Stacked on #209 (the branch sits on the current stack tip). Design: calendar handoff §0.1 + acceptance checklist, ruled in #257. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(event): an event keeps at least one person
All checks were successful
check / commits (pull_request) Successful in 25s
check / go (pull_request) Successful in 2m55s
check / report (pull_request) Successful in 15s
check / web (pull_request) Successful in 4m36s
cf942b30cf
The #257 ruling: the last attendee cannot leave — leaving an event, or
one occurrence of it, is a delete when you are the only person on it.
Without this the last owner's leave orphans the row into the admin-only
state ADR-0036 accepts for household dates, silently and one tap at a
time; refusing at the seam makes the way out explicit instead.

The guard lives in domain.RemoveMember so every remover shares it:
RemoveUser (the parent list), LeaveOccurrence (the materialised child's
own list — a refusal lands before the split is minted, so a refused
leave costs no permanent detach), and Update's guest-list rewrite gets
the same verdict inline. Create stays free to mint attendee-less events
(every Birthday is one); the rule is that a list never returns to empty,
not that it never starts there.

FailedPrecondition, LAST_MEMBER, wording that names delete as the exit.
Widening: none — this narrows. Design: calendar handoff §0.1 and the
acceptance checklist ('The last attendee cannot leave'), ruled in #257.

Test report

Suite Tests Result Skipped
Unit 1440 ✅ pass 1
Integration 133 ✅ pass —

Coverage: 27.0%

Updated by the check workflow · commit 9eb74cdc74

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1440 | ✅ pass | 1 | | Integration | 133 | ✅ pass | — | **Coverage:** 27.0% <sub>Updated by the check workflow · commit 9eb74cdc74e4bc52e7b08e112bc71b002ce245e4</sub>
nalum force-pushed feat/last-attendee-guard from cf942b30cf
All checks were successful
check / commits (pull_request) Successful in 25s
check / go (pull_request) Successful in 2m55s
check / report (pull_request) Successful in 15s
check / web (pull_request) Successful in 4m36s
to c6058fe4ec
All checks were successful
check / commits (pull_request) Successful in 25s
check / go (pull_request) Successful in 2m57s
check / web (pull_request) Successful in 4m29s
check / report (pull_request) Successful in 5s
2026-08-25 23:42:04 +00:00
Compare
nalum force-pushed feat/last-attendee-guard from c6058fe4ec
All checks were successful
check / commits (pull_request) Successful in 25s
check / go (pull_request) Successful in 2m57s
check / web (pull_request) Successful in 4m29s
check / report (pull_request) Successful in 5s
to 52ff758ea4
All checks were successful
check / commits (pull_request) Successful in 31s
check / go (pull_request) Successful in 2m54s
check / web (pull_request) Successful in 4m26s
android / build (pull_request) Successful in 7m8s
check / report (pull_request) Successful in 10s
android / report (pull_request) Successful in 3s
2026-08-26 03:45:31 +00:00
Compare
Author
Owner

Two base fixes joined this PR's foot after CI caught them overnight: the field-row title-editing goldens now record with a frozen caret (MotionDurationScale zero — the blink phase was a CI coin toss), and the bundled Eagrai Symbols face now covers + U+FF0B by aliasing it onto DejaVu's plus glyph (the calendar's add affordances fell through to host fonts, which diverged between the dev box and the runner). Every branch in the stack re-recorded its calendar page goldens against the bundled glyph.

Two base fixes joined this PR's foot after CI caught them overnight: the field-row title-editing goldens now record with a frozen caret (MotionDurationScale zero — the blink phase was a CI coin toss), and the bundled Eagrai Symbols face now covers + U+FF0B by aliasing it onto DejaVu's plus glyph (the calendar's add affordances fell through to host fonts, which diverged between the dev box and the runner). Every branch in the stack re-recorded its calendar page goldens against the bundled glyph.

Android test report

Suite Tests Result Skipped
Unit (debug) 188 ✅ pass 0

Coverage: 9.6% of lines

Updated by the android workflow · commit 9eb74cdc74

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | 188 | ✅ pass | 0 | **Coverage:** 9.6% of lines <sub>Updated by the android workflow · commit 9eb74cdc74e4bc52e7b08e112bc71b002ce245e4</sub>
nalum force-pushed feat/last-attendee-guard from 52ff758ea4
All checks were successful
check / commits (pull_request) Successful in 31s
check / go (pull_request) Successful in 2m54s
check / web (pull_request) Successful in 4m26s
android / build (pull_request) Successful in 7m8s
check / report (pull_request) Successful in 10s
android / report (pull_request) Successful in 3s
to ddacd9938c
Some checks failed
check / commits (pull_request) Successful in 28s
check / go (pull_request) Successful in 3m11s
check / web (pull_request) Successful in 4m29s
android / build (pull_request) Successful in 7m18s
android / report (pull_request) Has been cancelled
check / report (pull_request) Has been cancelled
2026-08-26 04:32:25 +00:00
Compare
Author
Owner

Correction to the note above: the U+FF0B alias was abandoned — the identical Chrome-for-Testing build on the runner still fell back to a host font while every local configuration honored the alias, so it could not be trusted. The stack instead writes the three plus strings with ASCII '+' (Atkinson carries it, bundled, pixel-identical on CI), the fonts subset stays its original eight glyphs, and the divergence from the demo's + is recorded in the day-sheet web commit. The frozen-caret fix stands.

Correction to the note above: the U+FF0B alias was abandoned — the identical Chrome-for-Testing build on the runner still fell back to a host font while every local configuration honored the alias, so it could not be trusted. The stack instead writes the three plus strings with ASCII '+' (Atkinson carries it, bundled, pixel-identical on CI), the fonts subset stays its original eight glyphs, and the divergence from the demo's + is recorded in the day-sheet web commit. The frozen-caret fix stands.
nalum force-pushed feat/last-attendee-guard from ddacd9938c
Some checks failed
check / commits (pull_request) Successful in 28s
check / go (pull_request) Successful in 3m11s
check / web (pull_request) Successful in 4m29s
android / build (pull_request) Successful in 7m18s
android / report (pull_request) Has been cancelled
check / report (pull_request) Has been cancelled
to 959af841b6
All checks were successful
check / commits (pull_request) Successful in 35s
check / go (pull_request) Successful in 2m56s
check / web (pull_request) Successful in 4m24s
android / build (pull_request) Successful in 7m32s
check / report (pull_request) Successful in 5s
android / report (pull_request) Successful in 4s
2026-08-26 04:56:37 +00:00
Compare
nalum force-pushed feat/last-attendee-guard from 959af841b6
All checks were successful
check / commits (pull_request) Successful in 35s
check / go (pull_request) Successful in 2m56s
check / web (pull_request) Successful in 4m24s
android / build (pull_request) Successful in 7m32s
check / report (pull_request) Successful in 5s
android / report (pull_request) Successful in 4s
to b6961010e7
Some checks failed
check / commits (pull_request) Successful in 30s
check / go (pull_request) Successful in 2m59s
check / web (pull_request) Successful in 4m31s
android / build (pull_request) Successful in 7m11s
android / report (pull_request) Has been cancelled
check / report (pull_request) Has been cancelled
2026-08-26 05:40:44 +00:00
Compare
nalum force-pushed feat/last-attendee-guard from b6961010e7
Some checks failed
check / commits (pull_request) Successful in 30s
check / go (pull_request) Successful in 2m59s
check / web (pull_request) Successful in 4m31s
android / build (pull_request) Successful in 7m11s
android / report (pull_request) Has been cancelled
check / report (pull_request) Has been cancelled
to 48cd494eaf
All checks were successful
check / commits (pull_request) Successful in 23s
check / go (pull_request) Successful in 2m56s
check / web (pull_request) Successful in 4m21s
android / build (pull_request) Successful in 6m54s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 4s
2026-08-26 06:16:34 +00:00
Compare
Author
Owner

Restructured per review: the four suite-hardening commits (frozen caret for the field-row goldens, the 24px page-comparator allowance, the scroll-to-top pin, the webhook spec draining to its own delivery) fix defects that predate this stack, so they moved onto the base branch (docs/park-handoff-bundles) rather than riding this PR. This PR is now the guard and its ADR alone. Each wave's golden re-records stay inside the PR whose change required them (#260, #262, #265).

Restructured per review: the four suite-hardening commits (frozen caret for the field-row goldens, the 24px page-comparator allowance, the scroll-to-top pin, the webhook spec draining to its own delivery) fix defects that predate this stack, so they moved onto the base branch (docs/park-handoff-bundles) rather than riding this PR. This PR is now the guard and its ADR alone. Each wave's golden re-records stay inside the PR whose change required them (#260, #262, #265).
nalum force-pushed feat/last-attendee-guard from 48cd494eaf
All checks were successful
check / commits (pull_request) Successful in 23s
check / go (pull_request) Successful in 2m56s
check / web (pull_request) Successful in 4m21s
android / build (pull_request) Successful in 6m54s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 4s
to 9eb74cdc74
Some checks failed
check / commits (pull_request) Successful in 12s
check / go (pull_request) Successful in 2m56s
android / build (pull_request) Successful in 7m9s
check / web (pull_request) Successful in 4m36s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 4s
check / commits (push) Has been cancelled
check / go (push) Has been cancelled
check / report (push) Has been cancelled
check / web (push) Has been cancelled
tag / tag (push) Has been cancelled
2026-08-26 07:03:44 +00:00
Compare
nalum changed target branch from docs/park-handoff-bundles to main 2026-08-26 18:37:37 +00:00
nalum merged commit 9eb74cdc74 into main 2026-08-26 18:37:40 +00:00
nalum deleted branch feat/last-attendee-guard 2026-08-26 18:37:41 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eagraiclainne/app!258
No description provided.