Skip to content

Output: the menu

One file: menus/<client-slug>-<YYYY-MM-DD>.json. Structured data, validated against the schema before it ships, and rendered by the client demo rather than read as-is.

Why JSON and not prose

A hand-written document can drift from what it claims: an option that has quietly stopped matching its slot's portion count, a disclaimer that got trimmed, a food that snuck back into an option after the client excluded it. Every one of those was a real failure mode of the Markdown version this schema replaced.

A schema turns most of that class of error into something that cannot be saved. menus/schema/menu.schema.json requires every option's food items to be individually typed with their own portion counts, so scripts/validate-menu.ts can add them up and refuse a file where they don't match the slot; it requires the disclaimer to equal an exact constant, so an edited one fails to validate; it requires every client-facing string to carry both an en and a he value, so a menu missing its Hebrew half is not a finished menu. None of that was enforceable when the output was Markdown.

The rendering — the actual page a client reads — is a separate concern, owned by the demo app, not by this file.

Why a framework and not seven named days

A seven-day plan is a stack of seven guesses. It specifies Tuesday's dinner without knowing what the client has in the fridge on Tuesday, and the first time that guess is wrong the whole document reads as broken — which is how meal plans end up in a drawer.

A framework specifies the structure — how many portions of what, in which slot, at roughly what time — and then offers three or four interchangeable options for each slot. The client picks. The structure holds regardless of which they pick, because every option in a slot carries the same portion counts. That is the whole design: the slot owns the numbers, the options own the food.

It also survives contact with section 7 of the questionnaire. A client with two intolerances and four dislikes loses one option out of four in a couple of slots, rather than losing Tuesday.

Required structure

Every menu file has these top-level parts. The schema is the authoritative definition; this is what each part is for.

Part Purpose
client, writtenDate, reviewWeeks Who this is for, and when to revisit it
dailyTarget The agent's working kcal/macro figures — not client-facing, kept so the arithmetic can be checked
dailyPortions The six-group daily total every slot's portions must sum to
slots The five meals, in fixed order, each with its portion counts and 3–4 options
drinking The hydration timing rules, as a list
atTheTable The mindful-eating rules, each flagged whether it survives the sensitivity filter
notes Personalisation the client would notice: intolerances honoured, symptoms accommodated, shift-work adaptation
assumptions Every place a missing answer was worked around — an empty array where there were none
disclaimer The scope disclaimer — schema-fixed, cannot be edited and still validate

A referral note — where step 2 stopped the run — is a different shape, type: "referral", with no slots at all. See the schema.

Rules the output must satisfy

  1. Every option in a slot carries that slot's portion counts. Not approximately, and not just as a total — every food item in the option carries its own portions, and scripts/validate-menu.ts adds them up. If the lunch slot is 2 starch / 4 protein / 2 fat / 3 vegetable, all four lunch options' items sum to exactly that.
  2. No starch or fruit in the dinner slot. Enforced structurally: the schema's dinner slot definition fixes portions.starch and portions.fruit to the constant 0. A menu with either does not validate.
  3. Portions are named in food terms as well as counted. Every foodItem carries a qty — the actual quantity, e.g. ½ cup cooked brown rice — not just its contribution to the six portion groups.
  4. No kcal figures in client-facing text, unless nut_tracked_before is "Currently tracking". dailyTarget.kcal exists in the file for the arithmetic; it is not rendered to the client.
  5. Nothing the client said they cannot or will not eat appears anywhere, including inside a food's name. This is checked in step 9 — the schema cannot know a client's exclusions, so this check is manual.
  6. Every assumption is listed in the assumptions array, in the client's own terms. An empty array is how a menu with no gaps says so.
  7. The disclaimer is present and unedited — schema-enforced via const.
  8. Every string has both languages. Enforced structurally: localizedText requires en and he, both non-empty.

What the output is not

  • Not a shopping list. Useful, out of scope, and it would double the length.
  • Not a recipe book. Options name dishes and quantities, not methods. "Rice and lentils with baked chicken" is an option; how to cook it is not.
  • Not a tracking sheet. The menu does not ask the client to record anything.
  • Not a progress document. It contains no targets, no timeline, no weights.
  • Not itself the client-facing page. That's rendered by the demo app from this data.

Language

Every client-facing string in the file is a localizedText object — { "en": "...", "he": "..." } — written per the menu style guide for English and its Hebrew counterpart. client.name is the one exception: it is the client's real name as given in client_full_name, in whatever script they wrote it, because it is data rather than authored copy.

Keep both languages' prose plain. The reader is not necessarily a fluent speaker of either, and the menu is a set of instructions rather than an essay:

  • Short sentences. One instruction per line.
  • Concrete nouns over categories — "half a cup of cooked lentils", not "a legume serving".
  • Household measures the client already owns: cups, tablespoons, teaspoons, slices. Grams only for meat, fish and cheese.
  • No idiom, no metaphor, no wordplay — hardest to carry across two languages, so avoided in both.

The full register and vocabulary rules, in both languages, are in the menu style guide.