# M1 — Hydration (Reference Engine) · Session Card

> Planning artifact. A build session opens this card (after recalling the brain per doc 13 §6) to know exactly what its one cell builds, what must exist first, and when it is done. **This is the reference engine: it is built fully across every surface first, and every later engine (M2–M8) copies its shape.** Get the pattern right here — clean plugin purity, a thin-client/SSOT audit that passes, expand-contract migrations, ICU/RTL from day one — and the remaining engines' per-surface sessions become mechanical.

## 1. Purpose

Hydration tracks water intake in fixed categorical 250 ml units and paces it with a server-enforced cooldown and a daily cap, so the user builds a steady hydration habit relevant to kidney health and gout prevention. The engine is deliberately simple so it can serve as the archetype for the whole grid: a pure `core/` state machine registered as its own ToGO plugin (the SSOT for all cooldown/cap/day-boundary logic), fronted by REST endpoints, and rendered by seven thin clients that submit "log one unit" intents and draw the returned snapshot — never computing protocol logic locally. It embodies both Core Opinions: intake is **categorical** ("a unit of water was consumed," never a free-form ml quantity), and the engine **invents no medical certainty** (it never says "you are dehydrated"; any AI narrative over the history is pattern description ending in "Consult your doctor").

## 2. Engine parameters (exact — do not invent)

| Parameter | Value | Source of truth / notes |
|---|---|---|
| Unit size | **250 ml** | The only loggable increment. Categorical unit of logging, not a quantity. |
| Daily maximum (cap) | **5000 ml** (= 20 units) | Hard cap. Accumulation window (the "day") is an open question — see §4/Notes. |
| Entry cooldown | **30 seconds** | Minimum gap between two accepted entries; enforced server-side, transactionally. |
| Idempotency | **`Idempotency-Key` required on `POST`** | Same user + same key ⇒ original result replayed, no duplicate row. |
| Storage unit | `units` smallint, default `1` | 1 unit = 250 ml; canonical storage is units, never free-form ml. |
| Default goal | **2500 ml** | Legacy precedent (`GlassMl=250`, `MaxDailyMl=5000`, `LogCooldown=30s`, default goal 2500 ml). Goal, and its pacing, are **PENDING ratification** — not spec-fixed. |
| Goal pacing | Paced **linearly across the WORK WINDOW**, not the calendar day | Legacy precedent worth reusing; informs the day-boundary/anchor open question. **PENDING DESIGN** for the anchor source. |
| `source` enum | `web \| ios \| watchos \| android \| wearos \| extension \| desktop` | Finalized when the clients exist; default `'unknown'`. |
| Endpoints | `POST /api/v1/hydration/entries` · `GET /api/v1/hydration/today` · `GET /api/v1/hydration/entries?date=...` · (`DELETE .../entries/{id}` undo — window rules are an open question) | `/today` response shape is **PENDING DESIGN** (must carry whatever the ring/timer UI needs). |

Invariants: total is always a multiple of 250 and `0 ≤ total ≤ 5000`; no two accepted entries for one user are < 30 s apart (server clock); cooldown and cap are checked **only** server-side; on any ambiguity (clock skew, duplicate suspicion) the engine **declines to count** the entry rather than inflating the total.

## 3. Session table

Surface order (doc 13 §3/§4): **backend → web → mcp → chrome → apple → android → desktop.** The backend session must land before any client session. Apple and android bundle several device targets plus a sensor write-back and are sliced into a handheld session and a wearable+sensor session (doc 13 §2/§3 authorize slicing a too-big cell); the `NN` placeholder sequences the two. Replace `<date>` (e.g. `2026-09-05`) and `NN` (`01`, `02`, …) when the session is opened.

| Session ID | Surface | Scope (what this one session builds) | Entry deps | Done criteria (doc 13 §5) | Plan docs to read |
|---|---|---|---|---|---|
| `S-<date>-M1-backend-NN` | backend | Hydration ToGO plugin: pure `core/` state machine (Idle/Cooldown/Capped) + `plugin.go` kernel registration; `hydration_entries` schema (`id`, `user_id`, `units` smallint dflt 1, `logged_at` = server-received time, `source` dflt `'unknown'`, `idempotency_key` dflt `''` with unique `(user_id, idempotency_key)` for non-empty keys, `created_at`); the three endpoints; the `POST` transaction that enforces 30 s cooldown + 5000 ml cap under a **per-user serialization** (advisory lock or equivalent) so two concurrent posts cannot both pass; idempotency replay; engine unit tests encoding **250 / 5000 / 30 s** literally; integration test on real Postgres. | M0.1–M0.4 (kernel, auth, plugin framework, migration harness + error envelope) | Plugin registered on the kernel; migration is **expand-contract** and every column is **NOT NULL with a default**; endpoints return computed state; engine unit tests encode every spec constant (250/5000/30) literally and pass; integration test against real Postgres passes (idempotency replay, cooldown/cap **races**, Arabic-locale localized rejection code); **no protocol constant appears outside the engine's `core/` package**. | 03 §1; 04 §5–§9; 13 §5 |
| `S-<date>-M1-web-NN` | web | Hydration dashboard card + ring on `/engines/hydration`, rendering `GET /today` snapshot only; a "+250 ml" log intent wired through the `['engine','hydration',date]` query + log-water mutation; disabled/cooldown state driven purely by the snapshot. | M1 backend; M0.5 (app shell/auth), M0.6 (theme), M0.7 (i18n) | Surface renders `/today` state and submits log intents only; **thin-client/SSOT audit passes** (grep for `250`/`5000`/`30` as behavior finds nothing); both themes render per the color spec (dark neon-green `#00FF33` glow / light flat teal `#20A060`); Arabic RTL mirrors correctly incl. the progress ring; all strings ICU, none concatenated. | 03 §1; 05; 13 §5 |
| `S-<date>-M1-mcp-NN` | mcp | `log_water` tool (POST hydration entry, +250 ml, `Idempotency-Key` header) and `today_status` (GET today state, returned verbatim); CLI equivalents (`healthdebug log water`, `healthdebug status`). | M1 backend; M0.8 (MCP server/CLI skeleton — itself gated on M1 backend) | Each tool calls the real API with **personal-access-token** auth, returns server state verbatim, holds **no protocol logic and no AI keys**; the 30 s cooldown / 5000 ml cap remain the server's guards; excluded tools stay excluded (no `complete_break`). | 03 §1; 11; 04 §7; 13 §5 |
| `S-<date>-M1-chrome-NN` | chrome | MV3 popup with a "+250 ml" button (log intent) and a toolbar **badge showing hydration %** derived from the `/today` snapshot; minimal permissions (storage/alarms + one host, no content scripts); PAT in `chrome.storage.session`. | M1 backend; M0.2 (PAT auth) | Renders `/today` and submits log intents only; **thin-client/SSOT audit passes** (no cooldown/cap math in extension code); both themes + Arabic RTL correct; ICU strings, none concatenated; badge value comes from the server snapshot, never computed locally. | 03 §1; 06; 13 §5 |
| `S-<date>-M1-apple-NN` | apple (iOS handheld) | iOS hydration card rendering `/today`; "+250 ml" quick-log intent; PAT auth via TOGO. | M1 backend; M0.2 (PAT auth) | Renders `/today` and submits log intents only; **thin-client/SSOT audit passes** (Swift grep for 250/5000/30 as behavior finds nothing); both themes + Arabic RTL (mirrored ring) correct; ICU strings, none concatenated. | 03 §1; 07; 13 §5 |
| `S-<date>-M1-apple-NN` | apple (watch + widget + HealthKit) | WidgetKit ring widget; watchOS quick-log complication/screen; write **completed** hydration back to **HealthKit**. | This module's apple iOS session; **X3 sensors foundation** (HealthKit permission + client plumbing) | Renders `/today`-derived state and submits log intents only; write-back pushes to HealthKit without any client-side cap/cooldown logic; **thin-client/SSOT audit passes**; themes + RTL correct on watch and widget; ICU strings. *(If X3 HealthKit plumbing is not yet ready, the write-back is deferred and this session ends `partial` — see Notes.)* | 03 §1; 07; X3; 13 §5 |
| `S-<date>-M1-android-NN` | android (phone) | Phone hydration card rendering `/today`; "+250 ml" quick-log intent; **Glance** home-screen widget with the ring; PAT auth. | M1 backend; M0.2 (PAT auth) | Renders `/today` and submits log intents only; **thin-client/SSOT audit passes** (Kotlin grep for 250/5000/30 as behavior finds nothing); both themes + Arabic RTL (mirrored ring/widget) correct; ICU strings, none concatenated. | 03 §1; 08; 13 §5 |
| `S-<date>-M1-android-NN` | android (Wear + Health Connect) | **Wear Tile** quick-log; write **completed** hydration back to **Health Connect**. | This module's android phone session; **X3 sensors foundation** (Health Connect permission + client plumbing) | Renders `/today`-derived state and submits log intents only; write-back pushes to Health Connect with no client-side protocol logic; **thin-client/SSOT audit passes**; themes + RTL correct on Tile; ICU strings. *(If X3 Health Connect plumbing is not yet ready, write-back is deferred and this session ends `partial`.)* | 03 §1; 08; X3; 13 §5 |
| `S-<date>-M1-desktop-NN` | desktop | Electron tray quick-log with global shortcut **Ctrl+Alt+W** (+250 ml intent); reuses the web hydration card/ring via the shared `packages/ui` + `packages/api-client`; PAT in `safeStorage`. | M1 web (shared card); Electron shell that loads the web app (scaffolding precondition — see Notes) | Renders `/today` state (via reused web components) and submits log intents only; **thin-client/SSOT audit passes** (no protocol constants in Electron main/renderer glue); both themes + Arabic RTL inherited from the web card; ICU strings, none concatenated. | 03 §1; 09; 05; 13 §5 |

**Session count: 9.**

## 4. Notes & gotchas

- **Legacy precedents worth reusing (candidates, not spec):** `hydration.go` shipped `GlassMl=250`, `MaxDailyMl=5000`, `LogCooldown=30s` — these match the fixed spec constants, so reuse them verbatim. It also had a **default goal 2500 ml paced linearly across the WORK WINDOW (not the calendar day)** and statuses `on_track / slightly_behind (deficit ≤ 500) / dehydrated / goal_reached`. The goal, its pacing, and those status bands are **PENDING ratification** (not spec-fixed) — carry them as the proposed default and flag any use.
- **Concurrency (the load-bearing test):** the cooldown/cap read-then-insert must run under per-user serialization (transactional advisory lock or equivalent) so two simultaneous `POST`s cannot both pass the checks. The invariant (no double-accept) is fixed; the mechanism is an implementation choice. The **cooldown-race integration test** (two concurrent posts ⇒ exactly one accepted) is mandatory and is the pattern every later engine copies.
- **Idempotency:** unique constraint on `(user_id, idempotency_key)` for non-empty keys; a replayed key returns the original result and inserts no new row. Retention window is an implementation decision — document it with the endpoint.
- **Fail-safe:** on any ambiguity (clock skew, suspected duplicate, late offline-watch sync) the engine **declines to count** rather than inflating totals; conflicting late entries are rejected or flagged, never silently merged.
- **Thin-client/SSOT audit is a gate, not a suggestion:** every client session must pass a grep proving no `250 / 5000 / 30` (nor any cooldown/cap/window math) lives in client code. Display strings mentioning these numbers come from the backend snapshot or ICU messages interpolated from API data.
- **Desktop shortcut divergence:** the greenfield tray quick-log uses **Ctrl+Alt+W** (per this module brief). Legacy Electron used `Ctrl/Cmd+Shift+W` — do **not** copy the old binding.
- **HealthKit / Health Connect write-back** depends on the **X3 sensors** foundation (permission groups + client plumbing). X3 runs alongside the apple/android engine sessions; if its plumbing is not ready when the wearable+sensor session runs, ship the render/quick-log and mark the write-back a partial/open thread rather than inventing local sensor logic.

### PENDING DESIGN / open questions (do not invent answers)
- **Day-boundary / cap-reset anchor + timezone/DST policy** for the 5000 ml daily cap and the work-window goal pacing — an open question that must be settled before the schema freeze.
- **Multi-unit per entry:** whether one API entry may carry > 1 unit, or strictly one unit per entry (cooldown rate-limits logging). Spec is silent; depends on the UI logging control.
- **Violation model:** reject-at-API vs. accept-and-flag (the plan assumes accept-and-flag). Violation alert rendering is PENDING DESIGN.
- **Visuals PENDING DESIGN:** the `/today` response shape (must carry what the ring/timer needs); the cooldown/disabled-button neon-`#00FF33`-glow behavior; the hydration ring itself; refresh/animation cadence per surface (web, watch, badge, widget/Tile); **Human Node status visual (PENDING DESIGN)**; history pagination/range shape.
- **Undo/edit window** for a mistaken entry (`DELETE .../entries/{id}`) and its effect on already-evaluated cap/cooldown state.

## 5. Handoff reminder

At the end of **every** session in this module, follow doc 13 §6: write **one `session-handoff` memory** to the `healthdebug` brain (`retain` the content in the §6 template, then `memory/edit` to attach metadata) with a unique `source_ref: handoff/M1-hydration/<surface>/<n>` and `metadata` `{ project:"healthdebug", category:"session-handoff", era:"greenfield", status:"current", surface:"<surface>", module:"M1", next:"<module+surface>" }`; then **update the single build-ledger memory** (`source_ref: build-ledger`, edited in place) so one recall shows the whole frontier and the recommended next session. Any architectural decision or new open question also gets its own classified memory and, if it changes the plan, an edit to the relevant plan doc.
