Module map
The feature and
css/nutrition.css load together only when needed. nutrition-context.js is the cold-path facade used by the shell, dashboard, context assembly, sync-applied listener, and import/export code.
Meal record boundary
A canonical reviewed meal contains a stable ID,eatenAt, local-time metadata, occasion, name, note, components, whole-meal nutrients, source/provenance, review state, optional response check-in, and up to four sanitized image thumbnails.
Rules that must remain true:
- Unknown nutrient values are absent or null, never silently converted to zero.
- Component nutrient edits and gram changes use deterministic arithmetic after AI returns.
source.kinddistinguishes manual entries, volume-only drinks, label scans, and photo estimates.- Photo-estimate provenance records the provider/model and the keys the model estimated from food identity and portions.
- An identity confidence value is a model self-rating; UI exposes only coarse language and never presents it as calibrated correctness.
- A saved response check-in can contain the bounded hunger/energy choices and time, but it is excluded from compact AI nutrition context.
Image and AI request lifecycle
prepareMealPhotos() accepts at most four supported image files. It creates a small local preview/thumbnail and retains the original bytes only in the in-memory prepared request. mealAnalysisImageBlocks() sends the original image variant to the selected provider; resized previews are a legacy in-flight fallback, not the normal analysis source.
analyzeMealPhoto():
- Resolves the dedicated meal route or the main AI route and requires a vision-capable model.
- Builds the meal-photo or nutrition-label task as multimodal user content.
- Calls the provider router with structured JSON mode, temperature 0, no automatic request retry, the import timeout, a bounded output budget, and cloud consent kind
meal-photo. - Rejects truncated or non-actionable output.
- Normalizes aliases, non-negative numbers, components, assumptions, warnings, and label data.
- Returns an editable draft plus provider/model/usage provenance and thumbnail-only image records.
AbortController owns the active meal request. Closing the modal can convert it into a background session without aborting; explicit cancel aborts the signal and leaves the editor reusable. Keep the tab open because unsaved work is in memory.
Each benchmark model owns a separate controller. Canceling one run must not cancel siblings. The comparison source is copied into an independent workspace, and selections are restored when routing between benchmark and editor. Profile changes invalidate or discard results rather than handing health data across profiles.
See Nutrition prompts for the maintained instruction and output contracts.
Deterministic benchmark scoring
scoreMealAnalysis() compares a result with user-entered known values or a selected model baseline. It scores amount/core nutrient agreement and ingredient-name overlap, then ranks usable results. This is a developer and user evaluation aid, not an accuracy certificate.
Do not:
- treat a model baseline as ground truth;
- include unknown reference fields in the denominator;
- combine canceled, failed, or partial output with completed results;
- persist API credentials or full request images in a comparison snapshot; or
- make provider completion order determine rank or reference identity.
Storage, sync, and reconciliation
The per-profile IndexedDB database isgetbased-nutrition-{normalizedProfileId}. It contains meals and meta object stores. Every cached meal payload is AES-GCM encrypted under a non-extractable device key stored in IndexedDB metadata.
sanitizeNutritionMeal() is the mandatory storage and sync boundary. It:
- allowlists JPG, PNG, WebP, or GIF thumbnail data URLs;
- caps each thumbnail at 160 KiB and each meal at four thumbnails;
- retains only bounded media metadata and quality warnings;
- strips
dataUrl,photoDataUrl,fullSizePhoto, the legacy singularimage, transient food candidates, and pre-enrichment visual snapshots; and - normalizes the optional response check-in.
importedData.nutritionMeals, which is the canonical cross-device surface. Saves merge against the latest committed profile value, clear the meal’s tombstone, persist the profile, then mirror the timestamp-winning row back to IndexedDB. Deletes record a nutritionMeals tombstone before removing the local row.
On startup and after labcharts-sync-applied, reconciliation merges the profile surface into the encrypted local cache. The initialization marker prevents an already-authoritative empty profile from being mistaken for a missing cache. Treat profile persistence and the cache update as one logical operation; preserve rollback behavior when the canonical profile write fails.
Summaries, timing, and targets
computeNutritionSummary() creates seven-, 30-, and 90-day windows plus previous-period baselines. computeNutritionHistory() supports 30 days, 3 months, 6 months, 1 year, and all recorded history. Aggregates include:
- food meals and volume-only drink counts;
- logged days and per-nutrient complete-day coverage;
- recorded daily averages;
- reviewed-entry ratio and occasion counts;
- first/last logged meal and observed eating windows;
- consecutive-day observed fasting windows;
- carb/fat composition and bounded personal response associations; and
- sleep-relative timing when local wearable sleep intervals are available.
importedData.nutritionTargets. Protein can be fixed or resolved from the latest wearable/manual weight with a bounded factor. Target grade colors are presentation over recorded averages; they do not change nutrient values or compensate for incomplete logging.
Fuel Mix is descriptive deterministic math from logged carbohydrate and fat. It has no preferred center and must not be labeled as measured Randle-cycle activity, substrate oxidation, insulin sensitivity, or metabolic health.
AI context contract
Meals & Nutrition has its own context-source toggle and a profile-selected 7-, 30-, or 90-day context range.buildNutritionSummaryContext() emits at most 2,600 characters and includes aggregate reviewed estimates, coverage caveats, core averages, available detailed nutrient averages, timing, Fuel Mix language, and sufficiently supported trends.
It never includes individual meal names, ingredients, notes, photos, or response check-ins. Detailed nutrition replaces the Diet & Digestion Typical meals narrative for the task; it must not be appended in a way that double-counts both sources.
The explicit History Ask AI action creates an editable user message for the selected range. buildNutritionHistoryReceiptContext() tells chat assembly to omit the automatic nutrition summary for that message, preventing the same period from appearing twice.
Agent Access uses the existing external-sharing context path and can receive the compact aggregate when profile context is assembled with external-sharing overrides. It still never receives individual meals or images through nutrition context.
Export, restore, and cleanup
buildNutritionArchive() serializes up to 10,000 sanitized meals. Restore validates version, record count, IDs, dates, names, components, image type, dimensions, and size before seeding the canonical profile surface. A legacy archive full image may be validated for recovery, but the normal storage sanitizer strips it before writing.
Profile removal must delete the profile’s nutrition IndexedDB database. Full-size images are not an export surface. Keep nutrition storage cleanup aligned with profile deletion, database-bundle restore, Sync hydration, and demo archive loading.
Verification checklist
- Run
tests/nutrition-analysis.test.jsfor prompt-output normalization and incomplete output. - Run
tests/nutrition-ai-settings.test.jswhen vision routing or model catalogs change. - Run
tests/nutrition-store.test.js,tests/nutrition-store-save-race.test.js, andtests/nutrition-sync.test.jsfor encryption, thumbnails, reconciliation, races, and tombstones. - Run
tests/nutrition-summary.test.js,tests/nutrition-context.test.js,tests/nutrition-targets.test.js, andtests/nutrition-fuel-mix.test.jsfor aggregate semantics. - Run
tests/nutrition-comparison.test.jswhen scoring or model-run lifecycle changes. - Run the directly related cases in
tests/playwright/nutrition-module.spec.jsfor editor/background cancellation, benchmark independence, history, targets, mobile accessibility, and saved-meal reuse. - Prove no full-size image or provider credential enters IndexedDB, profile JSON, sync, backup, comparison snapshots, logs, or fixtures.
- Prove missing meals/nutrients remain unknown in UI, targets, trends, and AI prompts.