diet-guard/docs/DOCS-meal-schedule.md
Krzysztof kuhy Rudnicki 03e14e17f3
Some checks failed
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Adopt the four markdown namespaces and gate them
Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md /
TODO*.md so a future session can tell a live task from a finished
record without reading the file to find out.

TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted
when the work lands. Enforced by the md-naming pre-commit hook and CI
workflow, both delegating to the shared gate in kuhyx/utils so this
repo's idea of a valid name cannot drift from every other repo's.

Includes the patch bump this repo's bump-patch-version hook applies to
app/pubspec.yaml on every commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
2026-08-24 15:57:20 +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.