Deep dive: nutritional estimation
The @gram-lang/analyzer is capable of automatically computing the macronutrient and micronutrient profile of any recipe.
However, because nutritional data is highly sensitive and impacts dietary choices, Gram takes a strict, mathematically conservative approach to estimating nutrition. This page explains that logic.
The calculation flow
Section titled “The calculation flow”- Mass Standardization: Before any nutritional calculation can occur, the Analyzer must first convert every single ingredient in the recipe into a standard mass in grams. (See Mass Standardization).
- Database Lookup: The Analyzer queries your
ingredients.yamldatabase for thenutritionblock of each ingredient. The database values must always represent the nutrients per 100g of the raw ingredient. - Proportional Scaling: The Analyzer scales the per-100g database values to match the actual mass used in the recipe.
- Aggregation: The scaled values for all ingredients are summed to calculate the Total Recipe Nutrition.
- Serving Bases v1.1.0: The Total Recipe Nutrition is then expressed on the bases a cook actually reads. If the recipe’s frontmatter defines
portions: 4, it is divided by 4 to giveperPortion. It is also divided by the mass it was computed from to giveper100g— which needs noportionsat all, so an unportioned recipe still gets a standardized figure.
Transparent partial data reporting
Section titled “Transparent partial data reporting”The most important philosophy of Gram’s nutritional engine is: never hide the estimate, but never let the user mistake it for the truth.
If you are cooking a meal with 10 ingredients, and only 9 of them have nutritional data in your database, silently displaying the sum of those 9 ingredients would risk being read as a complete, precise total when it actually understates the real caloric content.
Rather than withholding the total outright, Gram surfaces it alongside the information needed to judge its reliability:
- The
total(andperPortion, ifportionsis set, andper100g) is always computed and returned from whatever data is available — it is never blanked out. - A
coverageratio is always included (e.g.0.9for “90% of ingredients have data”), computed as the share of mass-bearing ingredients that had anutritionblock. - A
warningslist flags exactly which ingredients are missing from the database, missing theirnutritionblock, or have an unresolvable mass — so the gap is attributable, not just a vague caveat.
The reference HTML renderer reflects this directly: it always shows the nutrition panel whenever there is any data (or any warning) to show, with a Coverage: X% badge next to it. This gives the cook a genuinely useful number immediately, while making it clear at a glance how much of the recipe that number actually accounts for — rather than replacing a “90% accurate estimate” with nothing at all.
What “per 100 g” means here v1.1.0
Section titled “What “per 100 g” means here ”Gram models no cooking loss. There is no evaporation, no reduction, no bake loss — the yield field in the ingredient database is about trimming waste (peels, stems, bones), not about what happens in the pan.
So per100g means per 100 g of the raw assembled mixture: the sum of the ingredient masses that actually contributed macros. For a stew that reduces by a third or a loaf that bakes off 12% of its water, that understates the finished dish against the label on a supermarket product.
Rather than hide that, the analyzer reports the denominator it used:
massStatus is the same three-way signal the mass metrics use, and it changes how the figure should be read:
precise— every ingredient’s mass was known exactly.estimated— some masses came from a density or a unit weight, so the denominator is approximate. Renderers show it as~830 g.incomplete— some mass couldn’t be resolved at all, so the denominator is a lower bound and the density is therefore an over-estimate, not an under-estimate. Renderers show it as>830 g.
The denominator is the mass of the ingredients that contributed macros, not the recipe’s totalMass. Those two can legitimately differ — an ingredient with a known weight but no nutrition data has mass but no calories — and dividing one by the other would silently understate the density in exactly that case.
Renderers say which of the two they mean, because at full coverage the denominator is the recipe’s raw weight and at partial coverage it isn’t:
coverageis 1 → “Based on 830 g of ingredients, before cooking”coverageis below 1 → “Based on the 830 g with nutrition data, before cooking”
The second wording is what stops a 830 g figure from reading as the weight of a recipe that actually weighs more.
Where the numbers come from
Section titled “Where the numbers come from”Gram doesn’t ship a reference nutrition/density database — these values are specific to each product, region, and country, so only you can really vouch for what’s actually in your kitchen. gram db enrich can propose values via AI, but every proposal goes through an interactive review before it’s written, and anything accepted without editing is tagged # [LLM] in ingredients.yaml so it stays visibly unverified.
For an in-depth look at the design decisions and reasons behind abandoning bundled reference tables like CIQUAL or the USDA, read the dedicated devlog: Why Gram generates your ingredient database instead of bundling one.
Handling modifiers
Section titled “Handling modifiers”Gram’s syntax modifiers impact nutritional calculations in specific ways:
- Optional Ingredients (
?): Ingredients marked as optional (e.g.,@?whipped cream) are excluded from the base nutritional totals. The Analyzer assumes the most conservative dietary baseline. - Alternatives (
|): When a recipe provides alternatives (e.g.,@butter{50g} | @oil{40g}), the Analyzer only calculates the nutrition for the first (preferred) option. It does not average them. - Composite Ingredients (
<@): The nutrition of a composite ingredient is calculated purely from its children, each looked up independently in the database by its own id (e.g.,lemon juiceandlemon zestare two distinct entries with different macros). There is no fallback to the parent’s own nutritional data — if a child ingredient has nonutritionblock, it is treated the same as any other missing ingredient: it contributes to theMISSING_MACROSwarning and lowerscoverage, rather than being estimated from the parent.