health score

This commit is contained in:
2026-09-29 22:40:36 +05:30
parent f9fb405974
commit fb859ecda1
21 changed files with 1590 additions and 703 deletions

115
docs/NUTRITION_API.md Normal file
View File

@@ -0,0 +1,115 @@
# Nutrition and health score — the app contract
`GET /live/api/v1/mob/products/getproductbyvariant?tenantid=&productid=&variantid=`
Each product in `details[]` may now carry two extra keys. Both come from the
catalogue-intelligence service (`mcp.nearle.ai.in`) — the same records behind the
health score card in the console — fetched server-side, so the app needs no
second host, no second failure mode, and no copy of the rules below.
---
## nutrition
```json
"nutrition": {
"per": "100g",
"servingsize": "1 mini (11 g)",
"items": [
{ "name": "Energy", "value": 545, "unit": "kcal" },
{ "name": "Protein", "value": 7.5, "unit": "g" }
]
}
```
- **Absent when unknown.** Not `null`, not `{}`. A missing key means "we do not
know", never "this food has no nutrition".
- `items` is never empty when `nutrition` is present.
- `per` is `"100g"` for everything the service returns today. `servingsize` is
often absent — show the basis only when it is there.
- `value` may be a decimal. `unit` is free text and may be absent.
- Rows appear only when the service stated them. A null field is omitted; a
stated zero is kept, because "no fibre" is a fact and a dash is not.
## healthscore
```json
"healthscore": {
"score": 65,
"band": "good",
"label": "Healthy",
"positives": ["Good source of protein (7.5 g per 100 g)."],
"cautions": ["High in saturated fat (14.4 g per 100 g)."],
"diettags": ["High Fiber", "Vegetarian"],
"allergens": [],
"allergensunconfirmed": true,
"caveat": "Matched to a reference product with 61% confidence — treat these figures as a guide.",
"source": { "label": "openfoodfacts", "url": "https://..." }
}
```
- **Absent when there is nothing safe to show** — unscored, not food, or no
record at all. All three read as "not rated yet".
- `band` is one of `excellent` | `good` | `fair` | `poor`, for styling. `label`
is what a shopper reads. Use the label; do not re-derive it.
- `score` is 0–100, already rounded.
- `positives`, `cautions` and `diettags` are sentences the service wrote for a
person. Render as given.
### Two rules the app MUST honour
**`caveat`, when present, has to be on screen.** It means the underlying source
match was weak — most are; the service matches down to 0.32 confidence. A
nutrition table presented as fact on a 61% match is a claim the data does not
support.
**`allergensunconfirmed: true` means an empty `allergens` list must NOT be
rendered as "contains none".** Say "not confirmed — check the packet". Silence
standing in for "none" is the one failure here that can put somebody in hospital.
A declared allergen is always sent and must always be shown.
---
## Why the judgement is server-side
The service returns a raw number and, from this endpoint, no band. Deciding which
band, whether the match is strong enough to state plainly, and whether the
product is even food is a set of rules that already exists in the console. Two
implementations would drift and disagree about the same product on two screens.
The edibility guard is the sharpest of them. The upstream per-product endpoint is
**not** gated for it: on 4 Sep 2026 it rated Godrej Hit insecticide 80/100 with
`data_status: "verified"`, and soap and shampoo both scored 37.5. Those records
now read "unavailable", and the guard stays — this tenant sells soap and
toothpaste beside its biscuits.
## Coverage today
Measured 29 Sep 2026 against tenant 1147: **6 of 15 catalogue-linked products
have nutrition**, and fewer have a score. The Patanjali ghee this work started
from has neither.
Test with **product 7101, Balaji Wafers Simply Salted** — a full panel and a
65/100 score.
```sh
go run ./scratch/nutritionproof # three real products
go run ./scratch/nutritionproof <brand> <image_id> # any product
```
## Known bad data upstream
Balaji Wafers reports `sodium_mg: 0.967` — under 1 mg per 100 g, for salted
crisps, where 500–900 mg is normal. The `Salt` figure of 0.002 g is wrong the
same way. It looks like a grams/milligrams mix-up at the source.
Fiesta passes the value through as given rather than scaling it: silently
"correcting" a food label is how wrong data becomes invisible. It will look wrong
in the app until the agent team fixes the unit.
## Configuration
`NUTRITION_BASE=https://mcp.nearle.ai.in/api`, in `.env`. Unset means neither key
is ever sent and nothing else changes. Lookups are cached six hours, capped at
three seconds, and every failure costs that product its panel rather than the
response.