Every surface with the status read from the source, not from the plan. Where a document and the code disagreed, the code won and the document was corrected. The two newest features — nutrient contributions and multi-meal logging — get the depth they deserve, because they are the two the competitive field does not have.
Shipped in production Partial built but not fully delivered Gap planned, not built By design deliberately absent
Tapping any nutrient band reveals which of the window's meals fed that nutrient, and roughly how much, relative to each other. Day or last-7-days. The single most strategically valuable thing in the product — and the highest-risk copy surface in it.
UsageMeter reservation. Features that make a product feel smarter and cost less are rare.| Risk | What the code does | Status |
|---|---|---|
| Attribution is one word away from blame. "71% of your fat came from the pesto pasta" is an accusation if the copy implies fault. | Rows are ranked by magnitude and never labelled — no "largest contributor", no "worst". Each row uses the meal's own AI-generated title plus its own dominating-ingredient chips. The bar carries the category colour, which identifies the nutrient and never signals status. | Handled |
| The percentages will not add up. Meals with no landed value for a nutrient would silently distort every share. | build_contributions excludes them from the ranking and counts them in meals_without_value instead of fabricating a zero. The panel renders an explicit calm line: "some meals have no value for this nutrient yet". |
Handled |
| The math must be honest about what is being shared. Day ranges aggregate by root-sum-square, so uncertainties do not add and a share of a range is meaningless. | Shares are computed on midpoints only, and the module says so in its own docstring: this answers "which meal drove this", never "how uncertain is the day". The honest range stays owned by the RSS aggregation path. | Handled |
| It must not become a per-meal grade. The unit of judgment in emealia is the day; a tappable per-meal ranking on every nutrient is a meal scoreboard. | The list is capped at 8 with everything beyond collapsed into "+ N more meals" — bounded without hiding the total, matching the existing top-5 dominating-ingredient pattern. | Handled |
The share never appears as text. It is rendered as the width of a bar and nothing else; the meal's absolute amount is gated behind the showNumbers preference. So expanding a band on Today shows a picture, not a percentage — the main daily view stays numbers-free even when the explanation is open.
band.contributions.rowLabel reads "{meal}, contributed {share} of {nutrient}" with a rounded whole percent. Sighted users get a calm bar; screen-reader users get "43%" — a two-significant-figure share of an estimate carrying ±25% error. Consider an approximate register ("about a third", "a small part of") for parity, which is also the honest register.| Layer | Artefact |
|---|---|
| Core (pure) | core/nutrient_contributions.py — ranking, share math, rest bucket, missing-value counting |
| Use case | features/build_nutrient_contributions.py — clock → window → one store read → pure ranking |
| Boundary | DailyViewStore.meal_contributions() + the SQLAlchemy integration |
| API | GET /nutrients/{nutrient}/contributions — period day or week |
| Frontend | useNutrientContributions.ts (lazy, per-screen cache keyed nutrient|period, a failed load resolves to empty — never an error wall) and NutrientContributionPanel.vue, shared by Today's DayCard and All-details' NutrientDetailRow |
| Bonus | The panel also surfaces the user's own related dietary-preference chips for that nutrient — a profile fact, so it stays useful before anything is logged |
One free-text message describing several eating occasions produces several drafts. No mode switch — a single meal is simply N = 1.
Nobody in the field does this. The category's answer to entry fatigue is templates and repetition: MyNetDiary autologs routine meals, Lifesum saves meal combos, Cal AI re-logs remembered meals, everyone has favourites. All of these speed up repeating yesterday. None lets you describe an unrepeated day in one go. Cal AI's entire founding story is that manual logging is tedious; the industry answered with faster single entries, not fewer entries.
| Decision | Resolution |
|---|---|
| Credit accounting | One credit per AI call — the batch costs one, not one per extracted meal. It rewards the behaviour we want and makes the 30-credit trial feel generous. The anti-farming property is preserved by the segment cap and the per-user limits, not by credit arithmetic. |
| Meal-type inference (B2, resolved 2026-07-27) | The AI fills meal_type only from an explicit textual cue in that segment ("for breakfast…", "dinner was…"). Inferring type from food content is forbidden — "eggs → breakfast" is a guess. No cue → other. Always user-confirmable. |
| Never auto-save a batch | Splitting and estimating in one prompt is harder than estimating one dish, and boundary errors are likely ("…and a coffee" — separate meal or part of breakfast?). A batch produces N drafts on one review screen where each card can be re-typed or discarded before saving. |
| Segment cap | 8. Beyond that the first 8 are kept and one counted violation is flagged — the log never fails. _MEAL_ESTIMATE_MAX_TOKENS is derived from the cap so the two cannot drift, after a real incident where a flat ceiling truncated a single-meal response. |
| N = 1 must not change | A single-occasion text renders pixel-identically to the previous single-meal preview — no multi-meal chrome, no "Save all" wording change. Snapshot-asserted. |
"Meals per active user per day" (target ≥1.5 → 2.0) becomes partly a function of this feature rather than of engagement. The north star is already weekly active loggers with ≥4 logging days — keep it there and treat meals/day as a diagnostic, not a goal.
| Feature | Status | Detail |
|---|---|---|
| Free-text AI estimation | Shipped | Claude Haiku 4.5, structured tool output, versioned prompt snippets per locale with snapshot fixtures |
| Multi-meal segmentation | Shipped | Up to 8 occasions from one text, one credit, N drafts |
| Structure validation + one retry | Shipped | Cosmetic violations drop the field and keep the meal; nutrition failures retry once with the reason fed back |
| Plausibility validation | Shipped | Hard per-nutrient caps plus macro-energy coherence (±50%). Includes the "5000 kcal apple" regression test. |
| Language enforcement | Shipped | App locale controls output language even when the meal text is in another language; one retry on mismatch |
| Soft fallback, never an error | Shipped | After two validation failures: a low-confidence, editable fallback with calm bilingual copy and a needs_user_confirmation status |
| Prompt-injection guard | Shipped | HTML stripped, entities decoded, whitespace normalised, capped at an admin-configurable 1200 chars, wrapped in <meal_input> so hostile strings stay data |
| Portion size + product breakdown | Shipped | Canonical unit and portion normalisation (EN + DE spellings), structured amounts, merged ingredient list with dominating markers |
| Backdating | Shipped | Up to 7 days, no future dates, applies to a whole batch |
| Re-estimate | Shipped | Once per segment, then a defined disabled state |
| Offline logging | Shipped | Queue plus auto-commit when connectivity returns |
| Photo logging | By design | Post-launch Plus only. Non-negotiable #4. |
| Barcode / food database | By design | Never. See what we will not build. |
| AI-estimation cost optimisation | Gap | Epic 21: 1 of 8 stories done. Relevant because AI cost > €0.08/user/month is a kill condition. |
| Feature | Status | Detail |
|---|---|---|
| Band system, five calm states | Shipped | A versioned rule table returns an opaque state key; the API never sends a translation key. Category fill never changes with status — the text label is the only status signal. |
| No-failure guarantee, regression-locked | Shipped | Story 2.4: every one of the 2⁴ band geometries classifies to a calm state, the classifier never escalates, and an EN + DE copy deny-list proves no state reads as a failure |
| Personalised band set | Shipped | Star any of the 32 tracked nutrients onto Today, up to 10; starred pin first |
| Nutrient contributions | Shipped | See above |
| Today ↔ This-week tabs | Shipped | A rolling 7-local-day window (distinct from the calendar week Statistics uses) |
| All-details, 32 nutrients | Shipped | Top section plus explicit Vitamins and Minerals sections — never one generic "micros" block. Honest ranges, a calm "rougher estimate" sublabel on fuzzy values, and a last-7-days tab. |
| Qualitative day level | Partial | The pure function exists and is fully tested but is deliberately not wired into the daily view — it lands with History dots and Statistics. An unlogged day returns none, never "very light". |
| Daily tip in the suggestion slot | Gap | The component and the demo copy exist; nothing is delivered into it. See below. |
| Feature | Status | Detail |
|---|---|---|
| Day context tags (mood, hunger, energy) | Shipped | Eight controlled tags: energetic, calm, focused, stressed, tired, hungry, craving_sweet, bloated. Optional, per day, replace-semantics. |
| Activity context | Shipped | Manual and coarse, wellbeing-first. Shifts the ideal zone slightly; never a balance to spend. The full activity epic (MET estimation, net view) is deliberately post-launch. |
| Weight tracking | Shipped | Consent-gated schema, profile entry, a weight-protocol page, and full inclusion in GDPR export and erasure. No goal line, no countdown. |
| History | Shipped | Calendar with day dots, search, favourites, meal memory for confirmed repeat dishes |
| Statistics | Shipped | Daily, weekly and monthly spans from daily_summaries and period_summaries; gaps render as gaps, never as zeros |
| Mood ↔ nutrient correlation | By design | Never. Log it, show it as context, never assert causality. |
Mood data plausibly falls under GDPR Art. 9 (health data) on the same logic as body data. It belongs in the consent ledger with its own consent type, erasable independently, and must be excluded from any AI context not covered by that consent. This is launch-blocker-adjacent, not a nice-to-have.
GenerateDailySuggestion, the Anthropic suggest_daily path, the prompt guardrails, and the complete eligibility policy — energy threshold (35% of the 7-day median, floor 300 kcal), the local time gates (10:00 earliest, 20:00 fallback, 23:00 cutoff), and the frequency guards (≥3 logging days all-time, ≤1 tip/day) — are all built and tested. Story 4.5 — persistence and delivery — is not. No tip reaches a user.
The consequences are specific:
The recommended scope is not a coach. It is one sentence that names something you already did and extends it — built on the contribution ranking, selected by a deterministic rule table, and phrased (at most) by a model. That is cheap, on-voice, auditable under AI Act Art. 50, structurally incapable of shaming, and differentiated in a way a bigger coaching engine cannot copy without changing its own frame. The twenty rules →
| Feature | Status | Detail |
|---|---|---|
| Anonymous session + pre-signup demo | Shipped | Hardcoded data, zero AI calls, zero cost |
| Onboarding | Shipped | Age gate, goals (the only required step), body params, coaching params, dietary preferences, guided demo |
| Credit trial + Stripe | Shipped | All 6 stories. 30 AI credits, no card up front, granted at the first real estimation; Stripe Tax for EU VAT and US sales tax. |
| Auth | Partial | 7 of 15 done, 4 todo, 7 deferred or removed. The documents disagree on whether email/password exists — see the gap list on Current state. |
| GDPR export + erasure | Partial | 4 of 5 done; the remainder is blocked on a human dependency |
| Security hardening | Partial | 10 of 13 done |
| Medical / general-wellness disclaimer | Blocker | Research and draft EN + DE copy exist; legal counsel sign-off (H10) does not. Launch blocker. |
| EU AI Act Art. 50 disclosure | Gap | Enforceable since 2 August 2026. emealia complies in substance — every estimate is labelled an estimate and shown as a range — but the explicit disclosure placement at onboarding and at the meal-input surface does not exist. |
| Observability | Partial | 2 of 4. Needed to detect the cost kill condition. |
| PWA install, update, offline | Shipped | 5 of 6 |
| Localisation EN + DE | Shipped | Full parity; DE is adaptation, not translation |
Absence here is a position, not a backlog. Stating it plainly is part of the product.
| Absent | Reason |
|---|---|
| Barcode scanner, food database | Non-negotiable #4 — a race emealia would lose while abandoning its own axis |
| Streaks, badges, leaderboards | Non-negotiable #6 and the ED evidence base |
| Red, anywhere in nutrition feedback | Non-negotiable #1 |
| Goal-weight countdown | A number as a verdict with a failure state built in |
| Earned calories from exercise | Food-as-currency; the worst mechanic in the category |
| Apple Health / Health Connect sync | Native-only platform APIs, structurally unavailable to a PWA. Garmin, Fitbit, Withings, Strava and Polar are feasible over OAuth if ever wanted — and would refine the activity factor only, never credit the energy band. |
| GLP-1 tooling | Medical claim surface, wrong market, capital wins |
| Household / multi-eater accounts | Low differentiation, high cost |
Ranked by differentiation against cost. The top four are already true and simply unclaimed.
| Opportunity | Differentiation | Cost | Verdict |
|---|---|---|---|
| Claim eating out, shared dishes and cuisine breadth — free text handles "the chicken tikka masala at the Indian place" and "we split a big pan of…" natively; an LLM handles Turkish, Levantine, Vietnamese or Nigerian home cooking far better than a US/EU-centric crowdsourced database | High | ~zero | Do now — already true, never said. In Germany the cuisine point reaches a large population no competitor addresses. |
| Publish the guilt-design comparison rubric | High | Low | Do now — creates the axis instead of losing on theirs |
| Ship the deterministic tip library | Medium-high | Low | Do now — lands before the AI coaching epic and makes it cheaper |
| Market the number-hiding setting and one-tap cancellation | Medium | ~zero | Do now — both already exist, neither is promoted |
| Calm alcohol logging — every tracker turns a beer into a budget breach; nobody offers this | Medium | Low | Candidate — fits the non-negotiables exactly, needs no new mechanics |
| Low-capacity "rough day" logging mode | Medium | Medium | Candidate — non-negotiable #6 applied to the input surface |
| AI recipe generator | High | High | Plus, post-D7. The one place generative AI clearly earns its tokens; contributions are the ideal input; and a generated recipe is the only path to a logged meal with high confidence and no estimation error. Requires hard allergen and exclusion constraint injection — a recipe is an instruction, not an estimate. |
| Garmin / Fitbit / Withings OAuth | Low-medium | Medium | Not now |