Skip to content

Worked example

One complete run of the pipeline, from a real answers file to a finished menu, with the numbers shown at every step.

The answers are in mock/answers-noa-barlev.json and the menu is in menus/noa-barlev-2026-08-23.json, validated against the schema by scripts/validate-menu.ts. Both are in the repository; this page is the reasoning between them.

Step 1 — the digest

Who       52 y · female · Tel Aviv · product manager · seated most of the day
Screening PAR-Q: 4_condition Yes, 5_medication Yes (thyroid) — clearance
          confirmed. Conditions: high cholesterol, thyroid disorder.
          GI urgent: none. Disordered eating: No. Menses: Perimenopausal.
          Consents: all Yes.
Body      164 cm · 78 kg (measured) · BMI 29.0 · waist 92 · BP 132/84
Activity  Active · 3 sessions/wk · ~45 min · sedentary day · seated job
Goal      Lose body fat + Build muscle + Feel better + Improve health markers
Eating    First food 11:00 · last meal 21:30 · 4 occasions · water 1–2 L ·
          caffeine 4/day incl. on waking · FFQ: legumes Occasionally,
          plant proteins Never, pastries Daily, processed meat few×/wk
Limits    No pattern · lactose (milk only; yoghurt/labneh/hard cheese fine) ·
          no fish except tuna and salmon · no coriander, no aubergine ·
          cooking confidence 3 · 4 meals out/wk · kids eat differently
Symptoms  Bloating · wind · constipation · reflux · afternoon crash ·
          sleep 6–7 h, quality 3, wakes tired · stress 7
Gaps      No budget given · no body-fat measurement · no waist follow-up

Step 2 — the screen

Two PAR-Q triggers fired, so consent_clearance_confirm was required — and it is "Yes". Every stop condition checked; none fired.

sys_gi_urgent is "None of these", nut_medical_diet is "No", nut_disordered_history is "No", diabetes is absent, BP is 132/84 which is below the 180/110 stop. Clear.

Carried forward: perimenopausal → step 4. Thyroid → soy limit. Bloating, wind, constipation, reflux → step 7. Lactose, fish, coriander, aubergine → step 8. High cholesterol → no special framing. BP noted.

Step 3 — energy

BMR   = (10×78) + (6.25×164) − (5×52) − 161 = 1384 → 1380
TDEE  = 1380 × 1.55 = 2139
        (1.55: 3 sessions/wk; 45 min pulls neither way; sedentary day
         does not override the training band)
Goal  = "Lose body fat" + "Build muscle" → deficit wins → −15% → 1818
Floor = max(1380 × 1.1, 1400) = 1518 — not binding
TARGET = 1800 kcal/day

Step 4 — macros

Perimenopausal, so the menopause protocol sets protein from body weight: 3 sessions/week → 1.6 g/kg, plus 0.1 for the deficit → 1.7 g/kg. BMI 29.0 is under 30, so actual weight is used.

Protein  78 × 1.7 = 133 g → 532 kcal → 29.6%   ✓ inside 20–30%
Fat      27.5% of 1800    = 495 kcal →  55 g   ✓ inside 25–30%
Carb     1800 − 532 − 495 = 773 kcal → 193 g → 42.9%  ✓ at or above 35%
Per main meal: 30–40 g protein

Step 5 — portions

Fixed     veg 7 · fruit 2 · dairy 2
          → carb 89, protein 30, fat 6
Remaining carb 104 · protein 103 · fat 49

starch  = round(104/15) = 7   → carb 105, protein 21, fat 7
protein = round((103−21)/7)
        = round(82/7)   = 12  → protein 84, fat 12
fat     = round((49−7−12)/5)
        = round(30/5)   = 6   → fat 30

Totals  carb 194 (+1) · protein 135 (+2) · fat 55 (0)   ✓ all inside tolerance

7 starch · 12 protein · 6 fat · 7 vegetable · 2 fruit · 2 dairy

Step 6 — the schedule

Wakes 06:30, so the first meal is 08:00–08:30. Trains in the evening straight from work, so snack 2 sits before the session and dinner after it.

Slot Time Starch Protein Fat Veg Fruit Dairy Protein g kcal
Breakfast 08:00–08:30 2 2 2 1 1 30 445
Snack 1 11:00 2 1 1 14 175
Lunch 13:00–13:30 4 3 2 3 39 590
Snack 2 16:30 1 1 1 1 18 275
Dinner 18:30–19:00 0 4 1 3 34 260
Total 7 12 6 7 2 2 135 1745

Lunch is the largest slot at 34%; dinner is the smallest main at 15%, which also suits the reflux. The fat allocation is deliberately light at dinner and heavier at breakfast, where the two tablespoons of ground flaxseed have to fit.

The lunch-to-dinner gap is 5½ hours — over the ordinary 3–5 h rule, and covered by the evening-training exception. Snack 2 at 16:30 sits in the second half of it.

Steps 7–8 — food

Four options per slot. The constraints that actually shaped them:

Constraint Effect
Lactose — milk only No milk to drink anywhere. Dairy portions are yoghurt, kefir and labneh
No fish except tuna and salmon Those two only; the exception in the free text is what made salmon available at all
No coriander, no aubergine Absent from every option
Thyroid Soy not needed — the phytoestrogen requirement is met by flaxseed, so no soy limit had to be spent
Perimenopausal Cruciferous in lunch options 2 and 4 and dinner option 1; 2 tbsp ground flaxseed daily, split across breakfast and snack 1
Bloating, wind, low legume baseline The fibre ramp — see below
Constipation Flaxseed daily, water to the top of the range
Reflux Dinner light, low fat, finished by 19:00
Cooking confidence 3 One involved option per main slot; the rest assemble
4 meals out/week Lunch option 3 is orderable anywhere
Kids eat differently Lunch options 1 and 4 are a variation on one pot

The fibre finding

A representative day of this menu delivers roughly 45 g of fibre — well above the 25–30 g target, because a build that is wholegrain throughout with seven vegetable portions simply gets there.

Against nut_ffq showing legumes "Occasionally" and plant proteins "Never", and bloating and wind both "Yes", that is too much too fast. So the ramp binds, and it binds inside the portion counts rather than against them: half a cup of tinned rinsed legumes rather than a cup, cruciferous cooked rather than raw, rice and sweet potato preferred over oats for the first fortnight. That is written into the menu as a first-weeks instruction with a date to revisit, not as a permanent feature.

This is the case that made step 7 grow a section on overshooting the target. The first draft only handled coming in under it.

Step 9 — the check

bun scripts/validate-menu.ts menus/noa-barlev-2026-08-23.json counts every option's items back against its slot mechanically — the same check a human count-back does, run on every option rather than a sample. The first draft of this file failed it once: lunch option 1 came to 1 fat portion against a slot of 2. Adding a teaspoon of olive oil fixed it. Nineteen of twenty options were right; the twentieth read perfectly well and was wrong, which is exactly why this check is mechanical rather than a read-through — and why it is now the validator's job rather than a human's.

The searches for excluded foods, in both languages, returned only the notes mentions — milk named in order to say it is out, coriander named in order to say to order without it. The dinner slot's portions.starch and portions.fruit are both 0, which the schema would refuse to validate otherwise.

Final reconciliation, exactly as scripts/validate-menu.ts computes it from dailyPortions:

carb 194 g (target 193) · protein 135 g (target 133) · fat 55 g (target 55)
slot counts sum exactly to 7 · 12 · 6 · 7 · 2 · 2
main meals at 30, 39 and 34 g protein

Both languages, one file

client.name is "נעה בר-לב" — her name exactly as she gave it in client_full_name, in the script she wrote it in. That field is data, not authored copy, so it is not translated — see the output contract. Every other string in the file — every food name, every quantity, every note, the disclaimer — is a localizedText, written in English and Hebrew together, per the style guide.

Nothing about the arithmetic changes with the language. The portion counts, the schedule and the exclusions are the same numbers regardless of which half of a localizedText is being read.

What the menu says that the arithmetic does not

The two changes the client will actually notice are not in any of the numbers above: breakfast moves from 11:00 to 08:15 and acquires 30 g of protein, and dinner moves from 21:30 to 18:30 and loses its starch. Both are in notes, stated as changes with the reason attached, because a client who is told what changed and why will follow it and a client handed a table will not.

The other two personas

mock/answers-tomer-adler.json is the opposite build — vegan, 34, five sessions a week, muscle gain. It exercises the surplus branch, the dairy group dropping to zero and the rebuild from step 5, and the B12 limitation note.

mock/answers-yael-stern.json is a stop. Three independent flags fire: a sys_gi_urgent tick for blood in the stool, insulin-managed type 2 diabetes, and a prescribed therapeutic diet. The correct output is menus/yael-stern-2026-08-23.json with type: "referral" — no slots, no dailyPortions, just the four bilingual fields the schema defines for a stop — ordered GI first. It is in the repository as the case that proves step 2 is not decorative.