Back to Admin Dashboard

Food and Recipe Scoring Algorithm Reference

Part 1: The Average Score (1-8)

The Average Score provides a general nutritional rating for a food, independent of any user. It starts with a baseline score of 5 and is adjusted based on the following rules, applied to the food’s per-100g nutritional values. The final result is capped between 1 and 8.

Nutrient & Condition Points Rationale
Starting Score5A neutral baseline.
Fibre > 6g+2Excellent source of fiber.
Fibre > 3g+1Good source of fiber.
Protein > 20g+2Excellent source of protein.
Protein > 10g+1Good source of protein.
Sugars > 15g-2High in sugar.
Sugars > 5g-1Medium sugar content.
Saturated Fat > 5g-2High in saturated fat.
Saturated Fat > 1.5g-1Medium saturated fat.
Salt > 1.5g-2High in salt.
Salt > 0.3g-1Medium salt content.
Whole fruit, vegetable, nut or legume+1Matches the same idea Open Food Facts’ Nutri-Score uses — their “good stuff” bucket is fruit, veg, nuts and pulses, not just fibre and protein. Starchy tubers (potato, sweet potato, cassava/yuca, yam, taro) are deliberately excluded, exactly as Nutri-Score excludes them — nutritionally they behave like a starch, not a vegetable.

Part 0: What Gets Scored At All

Wholefoods and recipes are scored (base + personal). Cheat foods and supplements are never scored — neither layer runs for them. Cheats are packaged/processed items where nobody really knows what’s in them, so a nutrient score would be misleading — a short notice badge shows instead. Supplements carry no nutrient/calorie data at all and have their own separate detail-card path that never touches this file.

Part 1a: Recipe Scoring (Ingredient-by-Ingredient)

Recipes are scored differently from a single wholefood, because a recipe is really several wholefoods combined in different amounts. As of this update, a recipe is no longer blended into one “per 100g of the whole pot” number. Instead:

  1. Every ingredient is scored completely on its own — exact same Base Score and Personal Adjustment rules as a plain wholefood (Parts 1 and 2 below), using that ingredient’s own real per-100g nutrients and its own real wellness tags.
  2. Each ingredient’s score is then weighted by how much of the recipe’s total weight it actually makes up (its grams ÷ the recipe’s total grams).
  3. The weighted scores are added together for the recipe’s final Base Score and final Adjustment Value.

Result: a recipe that’s mostly chicken breast with a pinch of salt scores like chicken, not like an even blend of the two. A large ingredient drives the result; a trace ingredient barely moves it.

This only works because every ingredient in a recipe built via “Build It” or “Picture It” (recipes/recipeStudio.php) is guaranteed to be a real, matched wholefood carrying a barcode — any ingredient that doesn’t match a real catalog item is silently dropped at save time, never saved. Recipes saved before this matching existed may have no traceable ingredients at all; those cannot be scored this way and are flagged for the admin to rebuild or delete (see release_plans/SCHEDULER_MASTER_PLAN.md).

All of this lives in one shared file, pantry/food_score_calculator.php, used by both plain wholefoods and every recipe ingredient — one set of rules, not two.

Part 1b: The Colour Bands

The score circle’s colour comes from which of 5 bands the Base Score falls into. The middle “Okay” band is deliberately just the single value 5 — the exact untouched baseline, with no bonuses or penalties either way — rather than a wide range, so a small change like the fruit/veg bonus above is actually visible as a colour change instead of getting lost inside a big “amber” zone.

ScoreBand
8 or moreExcellent
6 – 7Great
Exactly 5Okay
3 – 4Below Average
2 or lessPoor

This band rule is shared (bsm_food_score_band() in pantry/food_score_calculator.php) by every place a score gets a colour — the food/recipe widget above and the meal-plan page — so the same score never shows as a different colour in two places. Each page is still free to use its own exact shades for a given band (the meal-plan page deliberately uses stronger, higher-contrast colours than the widget here).

Part 2: The Personalization Layer

This layer takes the Base Score and applies a series of adjustments based on the specific user’s health profile (“God JSON”). This generates the Adjustment Value (e.g., +2, -3) shown in the black badge. There are two paths depending on whether a food has been processed by the DB Tagger yet, described below. For a recipe, this runs once per ingredient (see Part 1a), not once for the whole dish.

Path A (Wholefoods): Tagger-Driven Scoring. Any food carrying precomputed wellness_factor_adopt/wellness_factor_avoid tags — written by the DB Tagger in wholefoods_suggested_crud.php‘s “Run DB Tagger” job — is scored using this path instead of the hand-coded rules in Path B. The user’s own currently-active wellness-factor tags (from wellness_synthesiser_db.php) are intersected against the food’s tags: any overlap in the food’s avoid tags applies a penalty, any overlap in its adopt tags applies a bonus.

A1. Tier-Based Point Weighting (Path A)

Every wellness-factor code encodes its own priority tier as a number prefix — T5_VERIFIED_LACTOSE is tier 5, T30_SYS_LACTOSE is tier 30, and so on. Lower tier numbers mean higher evidentiary confidence (5 = lab/monitor verified, 10 = blood biomarker derived, 30 = onboarding-survey risk score), so avoid penalties get bigger as the tier number gets smaller — mirroring the original -4/-3/-2/-1 deal-breaker scale below. Tiers 40 and up (mind states, neurotype traits, coaching preference, goals) aren’t clinical risk tiers, so they get a flat, gentle weight instead.

Tag’s TierMeaningAvoid PenaltyAdopt Bonus
≤ 5Lab/monitor verified condition-4+2
≤ 10Blood biomarker-derived risk-3+2
≤ 30Onboarding-survey risk score-2+1
40+Mind state, neurotype, coaching preference, or goal-1+1

Every matched tag adds its own penalty or bonus independently, so a food can rack up several adjustments at once if it trips multiple wellness factors the user actually has — unlike Path B’s fixed, single-shot condition checks below.

A2. Fallback: Legacy Hand-Coded Scoring (Path B)

Path B is the fallback for any individual wholefood (or, for a recipe, any individual ingredient) the DB Tagger hasn’t processed yet. Cheats and supplements never reach either path — they aren’t scored at all (see Part 0 above).

B1. Major Penalties (Deal-Breakers)

ConditionPoints
User has "celiac_status": "Severe" AND food has "gluten": true-4
User has "lactose_status": "High" or "Severe" AND food has "lactose": true or "dairy": true-3
User has "ibs_status": "Severe" AND food has "fodmap": true or "fodmaps": true-2

Note: the lactose check used to only match "High", silently skipping the worse "Severe" band entirely. It now matches both, matching the correction made to the diabetes check below.

B2. Weighted Health Risk Penalties

ConditionPoints
User has "bp_high": "Yes" AND food Salt > 0.3g/100g-1
User has "diabetes_status": "High" or "Severe" AND food Sugars > 5g/100g-1

B3. Goal-Oriented Bonuses

ConditionPoints
User has "weight_objective": "loss" AND food Fibre > 6g/100g+1
User has "weight_objective": "loss" AND food Protein > 15g/100g+1
User has "muscle_objective": "1" AND food Protein > 15g/100g+1

B4. Targeted Nutrient Bonuses

ConditionPoints
User’s protocol includes “Potassium” AND food Potassium ≥ 250mg/100g+1
User’s protocol includes “Magnesium” AND food Magnesium ≥ 25mg/100g+1

Final Score Formula: The final “Match Score” shown to the user is simply: (Average Score + Total Adjustment Value), capped between 1 and 10. This is identical for both Path A and Path B — only how the Adjustment Value itself is calculated differs.

Nutrient Level Definitions (per 100g)

This section details the specific thresholds used to determine if a nutrient level is considered Low, Medium, or High. These values drive the Traffic Light colors and the points awarded in the scoring algorithm.

Official UK Traffic Light System

Nutrient Low (Green) Medium (Amber) High (Red)
Fat (Total) ≤ 3.0g > 3.0g to ≤ 17.5g > 17.5g
Saturated Fat ≤ 1.5g > 1.5g to ≤ 5.0g > 5.0g
Sugars ≤ 5.0g > 5.0g to ≤ 22.5g > 22.5g
Salt ≤ 0.3g > 0.3g to ≤ 1.5g > 1.5g

Internal Scoring Thresholds

Nutrient Low (0 points) Good Source (+1 point) High Source (+2 points)
Protein ≤ 10g > 10g to ≤ 20g > 20g
Fiber ≤ 3g > 3g to ≤ 6g > 6g

Micronutrient “Source Of” Thresholds

These values define the minimum amount needed per 100g for a food to be considered a “source of” that micronutrient, based on providing 15% of the UK/EU Nutrient Reference Value (NRV). The personalization algorithm uses these as a baseline.

Nutrient “Source Of” Threshold Basis (15% of NRV)
Iron> 2.1 mgNRV = 14mg
Magnesium> 56 mgNRV = 375mg
Potassium> 300 mgNRV = 2000mg
Vitamin C> 12 mgNRV = 80mg
Vitamin D> 0.75 mcgNRV = 5mcg
Vitamin B12> 0.37 mcgNRV = 2.5mcg

Note on Energy & Carbohydrates: Energy (kcal) and Carbohydrates (g) are not included in the threshold tables as their values are highly contextual. A “high” value is not inherently negative and depends entirely on the user’s daily energy needs, goals, and activity level.