# M4 — Trigger Families (nutrition) — Session Card

> Per-module session plan for the **Trigger Families** protocol engine (Engine 4). Read alongside `docs/plan/13-execution-modules-and-sessions.md` (the framework this card implements), `03-protocol-engines.md` §4 & §9, `04-backend-togo.md`, and the relevant platform PRD (05 web, 06 chrome, 07 apple, 08 android, 09 desktop, 11 mcp). One session builds one `(module × surface)` cell. Surface order is fixed: **backend → web → mcp → chrome → apple → android → desktop**, and the backend session MUST land before any client session for this module.

## 1. Purpose

Trigger Families is the categorical food/intake catalog that underpins the whole protocol. It is Engine 4, but it is really a **classification service + per-user exposure ledger**: every catalog item is strictly `Safe` or `Trigger-bearing`, and each trigger-bearing item belongs to one or more named trigger families — **Gout**, **IBS-GERD**, **Fatty Liver**. This module institutionalizes Core Opinion 1 (categorical, not quantitative): there is **no portion, gram, calorie, purine-milligram, glycemic, or risk-score math anywhere in the food path — ever**. It ships the `trigger-families` ToGO plugin plus the shared categorical `catalog_items` DB resource (which Engines 2 Caffeine and 3 GERD also consume via boolean membership flags), a classify/lookup endpoint, and the categorical verdict surfaces on each client (web meal-log + trigger checker, mcp `log_meal`/`check_trigger`, the chrome Contextual Trigger Scanner, and the native/desktop food-log screens). Legacy `nutrition.go` / `FoodRegistry.swift` lists and the `food_items` + `drinks` tables are **seed data only**, not logic to port.

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

| Parameter | Value |
|---|---|
| Item classification | Strictly binary: **Safe** / **Trigger-bearing** (no third quantitative state) |
| Trigger families | **Gout**, **IBS-GERD**, **Fatty Liver** |
| Quantities | **None. Ever.** No portion / gram / calorie / percentage / threshold / risk score in any food surface (Core Opinion 1) |
| Unclassified fail-safe | Unknown item ⇒ **no protocol judgment**: `unknown ≠ safe`, `unknown ≠ trigger`; shown as "unevaluated — consult your doctor if unsure". AI is **prohibited** from guessing a classification; curation is human. |
| Multi-family membership | The categorical model allows an item to belong to >1 family simultaneously (assumed true — **PENDING DESIGN confirmation**) |
| Shared-catalog flags | `caffeine_bearing` (consumed by Engine 2) and `gerd_whitelisted` = exactly water / chamomile / anise (consumed by Engine 3) live on the same `catalog_items` row |
| Classification enum | `unclassified \| safe \| trigger_bearing` |

**Catalog SQL sketch (planning artifact — every column NOT NULL with a default, per schema rule):**
```
catalog_items
  id                 uuid    NOT NULL DEFAULT gen_random_uuid()
  name_key           text    NOT NULL DEFAULT ''               -- ICU message key; no string concatenation (i18n rule)
  classification     text    NOT NULL DEFAULT 'unclassified'   -- enum: unclassified|safe|trigger_bearing
  family_gout        boolean NOT NULL DEFAULT false
  family_ibs_gerd    boolean NOT NULL DEFAULT false
  family_fatty_liver boolean NOT NULL DEFAULT false
  caffeine_bearing   boolean NOT NULL DEFAULT false            -- consumed by Engine 2 (Caffeine Block)
  gerd_whitelisted   boolean NOT NULL DEFAULT false            -- exactly water/chamomile/anise, consumed by Engine 3
```

**REST surface (names from `03` §4.6 — indicative, frozen by the backend session):**
`GET /v1/catalog/items` · `GET /v1/catalog/items/{id}` · `POST /v1/catalog/items/proposals` · `POST /v1/intake/logs` (shared entry point, fan-out via Microkernel hooks to Engines 2/3/4) · `GET /v1/engines/triggers/exposures` (per-user timeline by family) · `POST /triggers/scan { text, locale }` (chrome scanner → per-family `Safe | Trigger-bearing | Unknown`).

## 3. Sessions

Session ID pattern: `S-<date>-M4-<surface>-NN` (leave `<date>` and `NN` as placeholders for the session that opens the cell).

| Session ID | Surface | Scope | Entry deps | Done criteria (doc 13 §5) | Plan docs to read |
|---|---|---|---|---|---|
| `S-<date>-M4-backend-NN` | backend | `trigger-families` plugin (pure `core/` classifier state machine + `plugin.go`) + `catalog_items` DB resource; classify/lookup endpoints (`GET /v1/catalog/items[/{id}]`, `POST /v1/catalog/items/proposals`); wire `POST /v1/intake/logs` fan-out so each intake writes a per-user exposure record `{item, families[], evaluated_as: safe\|trigger, engines_notified[]}`; `GET /v1/engines/triggers/exposures`; `POST /triggers/scan` categorical taxonomy lookup; seed the catalog from legacy `nutrition.go`/`FoodRegistry.swift` lists + `food_items`/`drinks` as **seed data** (not logic). | M0.1, M0.2, M0.3 (plugin framework + hook bus), M0.4 (expand-contract migration harness) | Plugin registered on the kernel; migration is expand-contract and every column NOT NULL with a default; endpoints return computed categorical state (never quantities); engine unit tests encode Safe/Trigger-bearing + the three families + unclassified fail-safe literally and pass; integration test against real Postgres passes (idempotent re-log replay, multi-family membership, unclassified → no-judgment); no protocol constant/family label appears outside the engine's `core/` package. | 03 §4 & §9, 04, 12 (§seed rows) |
| `S-<date>-M4-web-NN` | web | Trigger Families dashboard card at `/engines/triggers`: meal/food log control (categorical pick only) + trigger checker showing the backend's per-family verdict; renders Safe / Trigger-bearing / unevaluated verbatim; exposure timeline by family. | M0.5, M0.6, M0.7 (web shell + theme + i18n); **M4 backend** | Renders the trigger endpoint / `/today` state and submits log intents only; thin-client/SSOT audit passes (no food categorization, no quantity/threshold math in client — grep for family labels/constants finds nothing); both themes render per the strict color spec; Arabic RTL mirrors correctly (incl. any progress/timeline bar); strings are ICU, none concatenated (family labels never concatenated into sentences). | 05 §1–§7, 03 §4 |
| `S-<date>-M4-mcp-NN` | mcp | `log_meal` tool → logs a food item and returns its Trigger Family classification (Gout / IBS-GERD / Fatty Liver / Safe); `check_trigger` tool → answers Safe / Trigger-bearing only for a named food/ingredient. Idempotency-Key on `log_meal`. | M0.8 (mcp server + CLI skeleton); **M4 backend** | Each tool calls the real API with personal-access-token auth, returns server state verbatim, holds no protocol logic and no AI keys; tool schema descriptions state outputs are behavioral-tracking data, not medical advice; no quantity ever enters a tool input/output; excluded tools stay excluded (no diagnosis-shaped tool). | 11, 03 §4 |
| `S-<date>-M4-chrome-NN` | chrome | Contextual Trigger Scanner: `chrome.contextMenus` entry (`contexts:["selection"]`, no content script) → service worker `POST /triggers/scan { selectionText, locale }` → render per-family categorical verdict (`Safe \| Trigger-bearing \| Unknown`). Extension only ever sees the categorical result. | **M4 backend** (`/triggers/scan`); M1 chrome (extension shell + TOGO auth + service worker/context-menu infra) | Surface renders server verdict and submits the selection intent only; thin-client/SSOT audit passes (no categorization, no quantity/score in extension code); both themes per spec; Arabic RTL on the result surface, ICU strings (`en`+`ar` context-menu label), none concatenated; unknown/ambiguous text shown as the server's "unknown" as-is (extension never guesses); **AI text-extraction-vs-classification boundary respected — AI may only help extract candidate item text, never assign a family (see gotchas)**. | 06 §2, §3.2, §6, 03 §4 |
| `S-<date>-M4-apple-NN` | apple | iOS food-log + trigger surface: `Safe` / `Trigger-bearing` labels (Gout / IBS-GERD / Fatty Liver) from API; categorical pick logging; relevant watch/widget copy shows labels only. | **M4 backend**; M1 apple (workspace scaffold, `HDAPIClient`, TOGO auth session, theme/ICU-RTL pipeline) | Surface renders API state and submits log intents only; thin-client/SSOT audit passes (no local food categorization on any Apple surface incl. Siri/CarPlay/watch/widget); both themes per spec; Arabic RTL mirrors; ICU strings, none concatenated; no diagnosis/quantity phrasing anywhere. | 07 §engine-responsibility table, 03 §4 |
| `S-<date>-M4-android-NN` | android | Kotlin phone food-log + categorical verdict display (Gout / IBS-GERD / Fatty Liver vs Safe); Glance/Wear copy shows labels only; no local categorization. (Health Connect nutrition write-back belongs to X3, not here.) | **M4 backend**; M1 android (suite scaffold, API client/model, TOGO auth, theme/ICU-RTL) | Surface renders API verdict and submits log intents only; thin-client/SSOT audit passes (no categorization/quantity in phone/Wear/Auto/Glance code); both themes per spec; Arabic RTL mirrors; ICU strings, none concatenated; en/ar string parity. | 08 §engine-responsibility table, 03 §4 |
| `S-<date>-M4-desktop-NN` | desktop | Electron food-log + trigger surface, reusing the web M4 components inside the desktop shell; renders categorical verdict only. | **M4 backend**; **M4 web** (component reuse); M1 desktop (Electron shell) | Surface renders server state and submits log intents only; thin-client/SSOT audit passes (reused web components carry no protocol logic); both themes per spec; Arabic RTL mirrors; ICU strings, none concatenated. | 09, 05 (reused components), 03 §4 |

## 4. Notes & gotchas

- **Shared catalog = cross-engine dependency (coordination flag).** `catalog_items` carries `caffeine_bearing` (Engine 2 / M2) and `gerd_whitelisted` (Engine 3 / M3) on the same row. Module order is M1→M2→M3→M4, but the catalog Engine 2/3 read from is owned here in M4. Do not restructure the module order silently — instead the M4 backend session must expose these membership flags (already in the schema sketch), and M2/M3 backends must consume them rather than hardcoding food lists. If M2/M3 landed before M4, confirm whether they stubbed membership; reconcile in the handoff. **Flag any resolution as an architecture-decision memory.**
- **Legacy is seed, not logic.** `nutrition.go` uses lowercased **substring matching that errs toward flagging** (ibs_gerd: whole eggs/falafel/deep-fried/raw onion/raw garlic/cheddar/yellow cheese; gout: red meat/liver/duck/beans/lentils/legumes; fatty_liver: refined sugar/honey/nutella/jam/white flour/mixed carbs; plus safe lists). Mine the **item lists and `name_ar` values** as a ready seed dataset (with admin-CRUD precedent), but **do not port the substring-matching algorithm** as protocol logic — classification is explicit per-item catalog membership, not fuzzy string matching.
- **AI text-extraction-vs-classification boundary (chrome — OPEN QUESTION / PENDING DESIGN).** Whether `/triggers/scan` is a pure server-side taxonomy lookup or may use the BYOK AI proxy is a backend Phase 1 decision (`06` §6.2, open Q #12). The hard invariant either way: **AI may only help extract candidate item text from the highlighted selection; it must NEVER assign or guess a Safe/Trigger-bearing verdict or a family.** Classification is human-curated catalog lookup only. The extension only ever sees the finished categorical result.
- **Unclassified fail-safe is load-bearing.** Free-text the catalog does not know is `Unclassified` and yields **no judgment** — never silently "safe". Surface it as "unevaluated — consult your doctor if unsure".
- **Reclassification audit honesty.** Historical exposure records keep the classification **as evaluated at log time**; the recomputation policy on later reclassification is an **open question** — do not invent one.
- **Violation UX (PENDING DESIGN).** Whether a categorically disallowed intake is rejected or accepted-and-flagged is unsettled (`03` §0.5 assumes accept-and-flag). Alert rendering is PENDING DESIGN.
- **Catalog curation ownership (OPEN QUESTION).** Who curates/seeds the catalog (admin team / clinical advisors / future doctor dashboard) is not defined.
- **Core Opinion 2.** Family membership is protocol categorization, never "this food caused your flare." Any AI-adjacent copy is behavioral pattern analysis that must terminate in "Consult your doctor" — placement/wording PENDING DESIGN.
- **i18n.** Item names via ICU message keys; never concatenate family labels into sentences in code; full Arabic RTL incl. mirrored timeline/progress bars.

## 5. Handoff reminder

At session end, write **one** `session-handoff` memory to the `healthdebug` brain (`retain`, then `memory/edit` to attach metadata — retain cannot set metadata) using the doc 13 §6 content structure, with a unique `source_ref: handoff/M4-trigger-families/<surface>/<n>` and `metadata { project:"healthdebug", category:"session-handoff", era:"greenfield", status:"current", surface:"<surface>", module:"M4", title:"Handoff <session-id>", tags:[...], 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. See `docs/plan/13-execution-modules-and-sessions.md` §6 and `docs/brain/BRAIN-INDEX.md`.
