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.