emealia docs · internal 2026-08-06

Feature state, verified

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.

Verified against emealia-app backend + frontend and the 278-story implementation plan · 2026-08-06

Shipped in production   Partial built but not fully delivered   Gap planned, not built   By design deliberately absent

The headline: nutrient contributions

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.

€0.00
marginal cost — deterministic arithmetic, no AI call
8 + rest
top contributors, then a bounded "rest" bucket
midpoints
shares computed on midpoints only, never on ranges
0
competitors offering retrospective non-numeric attribution

Why it matters more than it looks

  1. It closes the band concept's one logical hole. A band tells the user where the day sits. It has never told them why. An explanation-free signal is, functionally, a verdict — the exact thing the product exists to avoid. Contribution turns the band from a judgment into an observation with evidence. That is what "glass box" was always supposed to mean at the day level, not only at the meal level.
  2. It buys perceived intelligence while lowering COGS. The list is arithmetic over data already stored. No tokens, no hallucination surface, no UsageMeter reservation. Features that make a product feel smarter and cost less are rare.
  3. It is the missing input for suggestions. The contribution ranking is the reasoning a coaching model would otherwise have to derive from raw meal history. Feeding a model the top contributor plus the shortfall nutrient means fewer tokens, less personal data in the prompt, and a more specific tip. The pipeline →
  4. It only reads well in emealia's frame. "Lunch was 43% of your protein" inside MyFitnessPal is an invitation to arithmetic and a per-meal grade. Inside a range-based, no-numbers product it is just a fact about the day. The feature is more natural here than at a competitor — which is the definition of a fit-dependent, hard-to-copy feature.

How the shipped implementation handles the four known risks

RiskWhat the code doesStatus
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
Non-negotiable #2 is respected, elegantly

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.

Two open items on this surface

What is actually shipped

LayerArtefact
Core (pure)core/nutrient_contributions.py — ranking, share math, rest bucket, missing-value counting
Use casefeatures/build_nutrient_contributions.py — clock → window → one store read → pure ranking
BoundaryDailyViewStore.meal_contributions() + the SQLAlchemy integration
APIGET /nutrients/{nutrient}/contributions — period day or week
FrontenduseNutrientContributions.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
BonusThe 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

The other headline: multi-meal logging

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.

Why it is underrated

  1. It rescues the exact moment trackers lose people. The evening catch-up — "I haven't logged since breakfast, so why bother" — is the canonical abandonment point, and it is a failure state by construction. Batch entry converts it into one sentence. That is non-negotiable #6 applied to the logging flow itself, and it plausibly moves D7 more than anything else built this quarter.
  2. It is cheaper per meal. One prompt producing three meals costs roughly one call's overhead instead of three.
  3. It is only possible because of the free-text architecture. A barcode or database product structurally cannot offer it. It is a defensible consequence of the core bet, not a feature that can be copied across.

The decisions that were locked

DecisionResolution
Credit accountingOne 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 batchSplitting 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 cap8. 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 changeA single-occasion text renders pixel-identically to the previous single-meal preview — no multi-meal chrome, no "Save all" wording change. Snapshot-asserted.
KPI hygiene

"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 state, surface by surface

Logging & estimation

FeatureStatusDetail
Free-text AI estimationShippedClaude Haiku 4.5, structured tool output, versioned prompt snippets per locale with snapshot fixtures
Multi-meal segmentationShippedUp to 8 occasions from one text, one credit, N drafts
Structure validation + one retryShippedCosmetic violations drop the field and keep the meal; nutrition failures retry once with the reason fed back
Plausibility validationShippedHard per-nutrient caps plus macro-energy coherence (±50%). Includes the "5000 kcal apple" regression test.
Language enforcementShippedApp locale controls output language even when the meal text is in another language; one retry on mismatch
Soft fallback, never an errorShippedAfter two validation failures: a low-confidence, editable fallback with calm bilingual copy and a needs_user_confirmation status
Prompt-injection guardShippedHTML stripped, entities decoded, whitespace normalised, capped at an admin-configurable 1200 chars, wrapped in <meal_input> so hostile strings stay data
Portion size + product breakdownShippedCanonical unit and portion normalisation (EN + DE spellings), structured amounts, merged ingredient list with dominating markers
BackdatingShippedUp to 7 days, no future dates, applies to a whole batch
Re-estimateShippedOnce per segment, then a defined disabled state
Offline loggingShippedQueue plus auto-commit when connectivity returns
Photo loggingBy designPost-launch Plus only. Non-negotiable #4.
Barcode / food databaseBy designNever. See what we will not build.
AI-estimation cost optimisationGapEpic 21: 1 of 8 stories done. Relevant because AI cost > €0.08/user/month is a kill condition.

The daily view

FeatureStatusDetail
Band system, five calm statesShippedA 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-lockedShippedStory 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 setShippedStar any of the 32 tracked nutrients onto Today, up to 10; starred pin first
Nutrient contributionsShippedSee above
Today ↔ This-week tabsShippedA rolling 7-local-day window (distinct from the calendar week Statistics uses)
All-details, 32 nutrientsShippedTop 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 levelPartialThe 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 slotGapThe component and the demo copy exist; nothing is delivered into it. See below.

Context, history and statistics

FeatureStatusDetail
Day context tags (mood, hunger, energy)ShippedEight controlled tags: energetic, calm, focused, stressed, tired, hungry, craving_sweet, bloated. Optional, per day, replace-semantics.
Activity contextShippedManual 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 trackingShippedConsent-gated schema, profile entry, a weight-protocol page, and full inclusion in GDPR export and erasure. No goal line, no countdown.
HistoryShippedCalendar with day dots, search, favourites, meal memory for confirmed repeat dishes
StatisticsShippedDaily, weekly and monthly spans from daily_summaries and period_summaries; gaps render as gaps, never as zeros
Mood ↔ nutrient correlationBy designNever. Log it, show it as context, never assert causality.
Compliance note on mood

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.

Suggestions — the real gap

5 of 10 stories done, and the missing half is the half the user sees

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 →

Account, trial and compliance

FeatureStatusDetail
Anonymous session + pre-signup demoShippedHardcoded data, zero AI calls, zero cost
OnboardingShippedAge gate, goals (the only required step), body params, coaching params, dietary preferences, guided demo
Credit trial + StripeShippedAll 6 stories. 30 AI credits, no card up front, granted at the first real estimation; Stripe Tax for EU VAT and US sales tax.
AuthPartial7 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 + erasurePartial4 of 5 done; the remainder is blocked on a human dependency
Security hardeningPartial10 of 13 done
Medical / general-wellness disclaimerBlockerResearch and draft EN + DE copy exist; legal counsel sign-off (H10) does not. Launch blocker.
EU AI Act Art. 50 disclosureGapEnforceable 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.
ObservabilityPartial2 of 4. Needed to detect the cost kill condition.
PWA install, update, offlineShipped5 of 6
Localisation EN + DEShippedFull parity; DE is adaptation, not translation

What is deliberately absent

Absence here is a position, not a backlog. Stating it plainly is part of the product.

AbsentReason
Barcode scanner, food databaseNon-negotiable #4 — a race emealia would lose while abandoning its own axis
Streaks, badges, leaderboardsNon-negotiable #6 and the ED evidence base
Red, anywhere in nutrition feedbackNon-negotiable #1
Goal-weight countdownA number as a verdict with a failure state built in
Earned calories from exerciseFood-as-currency; the worst mechanic in the category
Apple Health / Health Connect syncNative-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 toolingMedical claim surface, wrong market, capital wins
Household / multi-eater accountsLow differentiation, high cost

Remaining opportunities

Ranked by differentiation against cost. The top four are already true and simply unclaimed.

OpportunityDifferentiationCostVerdict
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 databaseHigh~zeroDo now — already true, never said. In Germany the cuisine point reaches a large population no competitor addresses.
Publish the guilt-design comparison rubricHighLowDo now — creates the axis instead of losing on theirs
Ship the deterministic tip libraryMedium-highLowDo now — lands before the AI coaching epic and makes it cheaper
Market the number-hiding setting and one-tap cancellationMedium~zeroDo now — both already exist, neither is promoted
Calm alcohol logging — every tracker turns a beer into a budget breach; nobody offers thisMediumLowCandidate — fits the non-negotiables exactly, needs no new mechanics
Low-capacity "rough day" logging modeMediumMediumCandidate — non-negotiable #6 applied to the input surface
AI recipe generatorHighHighPlus, 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 OAuthLow-mediumMediumNot now