diet-guard/docs/meal-schedule.md
Krzysztof kuhy Rudnicki ed6d283ff8 Document the configurable meal schedule
The rules that are expensive to rediscover: integer-only arithmetic and why
(Python and Dart round differently), the count clamp and why a duplicate slot
hour is worse than it looks, the last+2h cutoff and its midnight clamp, and
the required-argument decision in _slots.py / slot.dart.

Split into docs/meal-schedule.md rather than inlined -- CLAUDE.md hit 276
lines against the 250 cap, the same split the Flutter rules already use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159b4E5hh512jYUeaJVNxfM
2026-08-16 15:03:59 +02:00

52 lines
2.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# The meal schedule (`_meal_schedule.py` / `meal_schedule.dart`)
_Split out of `CLAUDE.md` to keep it under the repo's 250-line cap._
The user sets a first meal hour, a last meal hour, and how many meals fall
between them; the intermediate checkpoints are derived by even division.
`MealSchedule(8, 20, 5)` → `08:00, 11:00, 14:00, 17:00, 20:00`. The default
`MealSchedule(8, 20, 4)` reproduces the hours that used to be hardcoded, so an
existing install sees no change until it is edited.
Four rules, each load-bearing:
- **Integer arithmetic only** — see the `Do NOT` entry below. The shared
test-vector table plus an identical 0..23 × 0..23 × 2..6 sweep in
`test_meal_schedule.py` and `meal_schedule_test.dart` is what catches a
cross-language divergence before it reaches a device.
- **Slots stay whole hours.** Changing that changes the on-disk log's `slot`
tag and the sync wire format, on both devices.
- **Counts are clamped to the window width** (`last - first + 1`), because a
narrow window would otherwise round two meals onto the same hour, and slot
hours are set members, dict keys *and* notification ids — a duplicate
silently drops a checkpoint. The two edit surfaces reject such input with a
message; `normalized()` clamps it, which is the defence against a peer's
corrupt sync data rather than the input contract.
- **The enforcement cutoff is `last + 2h`, clamped to midnight.** It is a
grace period for a late dinner, deliberately independent of meal count.
Unclamped, a 23:00 last meal gives 25 and `hour < cutoff` is vacuously
true — the window never closes.
`_slots.py` / `slot.dart` take the schedule as a **required** argument. A
default there would let a call site missed in a refactor keep deriving the old
fixed hours on one device only, which is the split brain that can lock the
user out; making it required lets mypy and `flutter analyze` enumerate the
sites. Callers resolve it at the impure edge through
`_meal_schedule_store.current_schedule()` / `MealScheduleService.current`.
The history is forward-only (`_meal_schedule_store.py` /
`meal_schedule_history.dart`), so switching from four meals to five does not
retroactively mark past days as having missed a checkpoint. It seeds the
default at `1970-01-01` **before** recording today's value — same ordering
requirement as `_budget_history.py`. It syncs as `sched:<YYYY-MM-DD>` fields
on the existing `budget` CRDT record, so devices predating the feature relay
them untouched.
Editable on both surfaces: the app's Settings screen
(`settings_meal_schedule.dart`) and the gate's History tab
(`_gatelock_scheduleedit.py`).
**Notification ids are slot hours.** `syncToSlots` must sweep the whole 0..23
id space, not the current schedule's slots — otherwise a schedule change
orphans the ids it no longer contains and they nag forever.