Diet is not special code. It is the catalog, Templates, an item field that carries an amount, and Readings, configured. The Food & drink module (dietModule in packages/core/src/modules.ts) builds it, for the demo profiles and for setup alike: the Food catalog, the Diet trackable, a Meal routine, Daily Calories, and on request Daily Caffeine, Daily Alcohol and three drinks (Coffee, Beer, Wine), filed under the Nutrition tag (Modules).
Foods
A Food is an item of the Food catalog, whose fields every food carries: Calories (kcal), Protein, Carbs, Fat, Caffeine (shown in mg), Alcohol, and Food type, a Category. Each number field is a name, a label, a dimension and a display unit, and its values are stored in the dimension’s base unit, like every measured value (Units & Dimensions); field names carry no units. The basis is the gram, so values are stored per gram and shown per 100 g by default (Display basis). One schema serves solids and drinks: a beer is a food with Alcohol filled, so Daily Calories counts it, which a separate drinks catalog would not without declaring calories twice.
A value is known, a guess, or unknown. A blank value is unknown, never 0, except Caffeine and Alcohol, which read 0 when absent (absent: zero) since most foods have none. Each value has one sign, picked beside it in the food form (signs): Known, About (~), At least (≥, “800+ calories” on a menu) or At most (≤, “<1 g” on a label, which a scanned label sets). A day’s total that includes a food with no figure is at least, shown ≥; the signs of what goes into it combine by one rule (combineSigns), so About anywhere reads ~ and At least with At most reads ~ too. A day whose foods all lack the figure has no total for it, rather than a total of 0. An entry saved with no food, a meal whose food was left empty, is not refused: what it held is unknown, so its day’s totals read as at least (≥). The marks travel with the number into derived trackables, cards and charts, and minimum and estimated days stay in analysis: nearly every day is a minimum for most people, so leaving them out would leave nothing, and a visualization of several trackables says how many of its days were either. An entry keeps only which food and how much, and a food’s values are read when totals are worked out, so correcting a food corrects every day it was eaten. A food is one recipe: a changed recipe is a new food.
A number either scales with the amount or describes the food. Calories, protein and caffeine scale: 200 g holds twice what 100 g does, and a day adds them up. A number that describes the food itself, such as a glycemic index, is marked Same at any amount (per: item on its field, read through isPerItem in packages/core/src/value-schema.ts): it is taken as it is wherever an amount of the food is logged or lent to a food based on it. A day reads it by its field’s own Reading, Mean unless the field says (dayReadingOf), and a food without one is left out of that reading rather than making it a minimum. Chad’s Food catalog carries one, Glycemic index, on the foods he eats that have carbohydrate, and his Daily glycemic index reads its mean each day.
Typical foods are thirteen ordinary foods whose every value is a guess (a healthy meal out, a restaurant meal, a pastry or baked good, fruit), in the group “Typical, for a quick guess” (packages/core/src/typical-foods.ts). The module installs them and Import offers them, so a day can be logged roughly before any real food is entered. A typical food is also a ready food to base a new one on, for a food whose real figures are not known yet.
A food can be based on another (based_on), with an amount between them said at what the numbers are per: “100 g of this ≈ 1 serving of that”, stored per one basis unit. Picking the other food starts the amount at the same weight when both are weighed (100 g ≈ 100 g), the same count when both are counted (1 serving ≈ 1 serving), and 100 g to a serving across the two. Each value is the food’s own where it has one, else the other food’s scaled by the amount, always as a guess here even where it is real there, else unknown. A chain reads through (based on B, which is based on C, reads C through B), and a loop is refused on save. Correcting the other food moves every food based on it, and every day those were eaten. The link stays when both foods have their own numbers, since it also says what kind of food this is. Deleting a food others are based on first copies its values, scaled by each one’s amount and marked as guesses, into each of them, so nothing silently becomes unknown (packages/core/src/catalog-repository.ts). A value that was only a minimum on the deleted food stays a minimum (≥) on each food that took it, until a value is typed over it. Based on is not a recipe: a recipe is a template whose ingredients are logged, each counted; based on says “like”, and what it lends is a guess.
To fill in is a list on the catalog page of your own foods (yours and one-offs) with a value unknown or not Known, its own or lent by the food it is based on, each named with its sign; a field that reads 0 when absent is not owed (components/catalog/ToFillIn.tsx).
Alcohol is typed as % ABV when the amount is a volume, through the field’s density, ethanol’s 0.789 g/ml. Daily Alcohol counts standard drinks, a unit of its own of 13.45 g of alcohol, Canada’s standard drink (STANDARD_DRINK_GRAMS in packages/core/src/modules.ts), and can be read in grams instead. Other countries count a standard drink differently (the US 14 g, Australia 10 g, a UK unit 8 g); the count here is always Canada’s.
Where foods come from
- Bundled public data: a curated subset of USDA FoodData Central’s Foundation Foods, which are public domain.
pnpm run db:gen-foods(scripts/db/gen-foods.ts) shapes it intoscripts/db/foods.generated.json: protein, fat and carbs per gram; energy in kcal from the analysed energy (nutrient 208), else Atwater Specific (958), else Atwater General (957); up to eight household servings with their gram weights from the food’s portions, leaving out RACC and unnamed portions; foods with no macros dropped; ordered by FDC id so the bundle is stable. The full bundle is 354 foods; its starter set is 133, grouped for the picker (packages/core/src/foods.starter.json); how a bundle becomes a starter set and reaches a profile is in Catalog Entities. The app bundles this curated set rather than a downloadable food database, so it works offline and stays small; the foods you actually eat are added as you go. - Your own foods, defined once and reused.
- One-offs, for a meal out (below).
A recipe is a template; a meal is an Event
- A recipe is a Template on Diet: Chad’s Egg & Cheese is 100 g of Egg and 30 g of Cheddar. Logged, it is a Diet entry holding an entry for each, editable first, so each food counts as itself in every total: how much of the week’s calories came from eggs counts the egg in the sandwich. A pot cooked once and eaten over days is a recipe weighed once made, and a bowl is a portion of it, each food taking its share (Templates).
- A meal is an Event: the Meal holds the foods eaten at one sitting as they are logged, each an ordinary Diet entry, an Egg & Cheese among them.
Logging food
A Diet entry is one food and its amount: {food, quantity, unit?, base}, where base is the amount in the food’s basis units, frozen when it is logged so that a later change to a unit cannot rewrite it (Units & Dimensions). An entry that holds its food as entity_id rather than under food reads the same (packages/core/src/entry-object.ts). Diet is asked once a day in the Rundown, answered by any food (The Rundown). It is not asked at breakfast, lunch and dinner, since when someone eats is their own pattern: meals can be added to the Rundown as labelled items, and read afterwards through a time window in a derived trackable (“Ate before 17:00”).
The food is picked in the catalog’s own item picker, with the foods logged recently first and Diet’s templates above the list (The catalog page and picking an item). Picking a food fills in what a person would log: its usual amount, else one of its serving where it declares one, else 100 of a measured basis, else 1; never 1 g (pickDefaults in components/entries/log-defaults.ts). Under the amount, Usual amounts offers its amounts as chips, a tap each, the one being logged pressed: those pinned first, then the amounts it was logged with most, the latest first among equals, up to four. An amount is usual from its second time, so a 207 g weighed once is not offered; a unit the food no longer has is never offered (usualAmounts). Pin (the amount) keeps the amount being logged among them whatever the history says, and Unpin lets it go; the pins are kept on the trackable, by food, in its capture’s pinned_amounts, so they sync with it. They are units and a quantity, never a second concept such as a preset (Units & Dimensions). While the amount is typed, a preview shows what it comes to, reading the catalog’s own number fields, so it shows macros here and another catalog’s numbers there with no diet code (components/entries/AmountRow.tsx). From the log form, Edit (the food)‘s numbers opens the food in a dialog; saving it refreshes the resolver, the day totals and the derived series. On the phone, Scan barcode picks a saved food with that barcode or opens the item form filled from the lookup (Reading a Label).
Not in the catalog
“Not in the catalog…” opens the catalog’s own item form, titled “Not in Food”, with the name already typed and a “Save to my food catalog for next time” switch; off, the food is saved as a one-off. With a weight on its serving line it is stored per gram with the serving as a unit of that weight; without one, its basis is the serving and it is counted in servings. A one-off stays among the foods recently logged, so yesterday’s lunch out is one click to log again.
The item form and the serving line
One item form (components/catalog/CatalogEntityForm.tsx) serves the catalog page and the log dialog. No value is required; a food with none is what To fill in lists. Each number is typed in the unit the label prints it in, such as 160 mg of caffeine or 960 kJ of energy, from its field’s dimension, and the app converts it to what is stored. Its sections are named by glossary terms, Units and Basis unit.
The serving line. Numbers are typed per what the label prints: “Per [2] [cookies] weighing [34] g”, or “measuring” for a volume (components/catalog/ServingLine.tsx, serving.ts). The app makes a unit of the shared-out weight (a cookie of 17 g), named in the singular, stores per gram, and logging opens on 1 cookie. “100 g” makes no unit; a cup with a weight becomes a cup of that weight; several of a measured unit with a weight (“250 ml (258 g)”) becomes a unit named for the amount printed, 250 ml of 258 g, with ml beside it, so logging opens on the 250 ml and never on 1 ml; a word with no weight counts the food in that word, or in ml for a drink. A size in ml is not turned into grams, since nothing gives the food a density, so a bar sized in ml is counted in ml with the bar a unit. The form reads the result back (“1 cookie is 17 g, and logging starts at 1”). Plurals are made singular, with a list of food words whose plural is “-ies” from “-ie” (cookie, brownie, pie).
Units in plain words. “1 [sleeve] is [12] [cookies]” or “is [204] g”, the unit’s code made from its word. A unit keeps its stored code while its word is unchanged, since entries name it by code.
Based on sits in the same form, as a picker of another food and the amount between them.
A label can be read into the form from its barcode or a photo: see Reading a Label.
Daily totals
Two surfaces, one arithmetic: each entry’s base times the food’s value per basis unit, or the value itself for a number that describes the food (containedOf in packages/core/src/reading-days.ts). Both read what is stored, with no second computation path, so they always agree.
- Daily Calories, Daily Caffeine and Daily Alcohol are Derived Trackables, one reference each: Diet’s entries mapped to a value and summed by day. They chart, correlate and feed visualizations like any other trackable.
- The day’s totals are the Day visualization of a trackable whose item field carries an amount in the item’s units: every number field of its catalog read for the day as
dayReadingOfsays, summed where it scales with the amount and by its own Reading where it describes the food, with the entries behind them, each opening its entry (components/home/TodayTotals.tsx). The catalog’s first number field in declared order is what a row is worth (calories for a food). The totals list each field on its own, since a sum of calories and milligrams of caffeine means nothing.
A day’s sign, ≥, ≤ or ~, is carried beside each evaluated series through formulas, windows, tags and zones (DayMark, evaluateWithMarks), a subtraction flipping its direction, and shown on Home’s cards, the day’s totals, the log preview and the catalog.
Related
Catalog Entities · Templates · Events · Entry · Reading a Label · Frictionless Capture · Units & Dimensions