From 17b81f567dfa600dccf9eae3240093da4462a270 Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Sat, 17 Oct 2026 18:06:53 -0400 Subject: [PATCH] docs(architecture): add Phase 3C nutritional search engine and food cache design Specifies the L1-L4 ingestion resolution ladder (encrypted local cache, Open Food Facts branded/barcode lookups, USDA FoodData Central fallback for whole/unbranded foods, manual entry), the FoodProduct domain model and FoodProductsTable schema, the OFF/USDA nutrient mapping table, and network resiliency rules (mandatory custom User-Agent, 4-second circuit timeout with automatic fallback, write-through SQLCipher caching). --- docs/architecture.md | 131 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/docs/architecture.md b/docs/architecture.md index d07a935..f003646 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -217,6 +217,137 @@ abstract class IHealthGateway { 2. **Read-Only Minimal Principle:** Scopes requested from HealthKit and Health Connect are strictly restricted to read permissions for weight, blood glucose, and active burn to maximize user trust and prevent third-party telemetry leakage. 3. **Local Encryption at Rest:** Any synced telemetry data cached locally is stored inside the SQLCipher-encrypted SQLite database (`glp1_encrypted.db`), keyed by the hardware-derived master key managed by `CryptoManager`. +### 4.8 Phase 3C: Nutritional Search Engine, Barcode Scanning & Dual-Source Food Cache + +#### 4.8.1 Subsystem Overview +The Food Ingestion subsystem bridges cloud product databases with encrypted local storage (`FoodProductsTable` in Drift SQLCipher). To defend against rate-limiting, missing products, and non-profit server downtime (e.g., Open Food Facts 503 limits), the ingestion engine implements a deterministic multi-tier resolution ladder: +1. **L1 Local Cache:** Encrypted SQLite query by exact barcode or tokenized substring. +2. **L2 Packaged Foods Primary (Open Food Facts):** REST API v3 queries identifying branded packaged items with a strict custom `User-Agent` and a 4-second timeout. +3. **L3 Whole & Unbranded Foods Fallback (USDA FoodData Central):** Automated fallback querying Foundation, SR Legacy, and Survey databases for generic items, unbranded raw foods, or failed L2 lookups. +4. **L4 Manual Ingestion:** Client-side fallback modal allowing user-defined product creation and instant SQLCipher persistence. + +```text + ┌────────────────────────┐ ┌────────────────────────┐ + │ Text Query Search │ │ Mobile Barcode Scan │ + │ ("string cheesestick") │ │ (EAN-13, UPC-A) │ + └───────────┬────────────┘ └───────────┬────────────┘ + │ │ + ▼ ▼ + ┌───────────────────────────────────────────────────────────┐ + │ IFoodSearchRepository │ + └─────────────────────────────┬─────────────────────────────┘ + │ + ┌─────────────────────────┼─────────────────────────┐ + ▼ (1. Check Cache) ▼ (2. Primary Query) ▼ (3. Auto Fallback) +┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ +│ Local Drift Cache │ │ Open Food Facts API │ │ USDA FoodData API │ +│ (SQLCipher AES-256) │ │ (Branded / Barcode) │ │ (Whole/Raw/Fallback)│ +└──────────┬──────────┘ └──────────┬──────────┘ └──────────┬──────────┘ + │ (Hit) │ (Success) │ (Success) + │ └────────────┬─────────────┘ + │ │ (Write Through Cache) + ▼ ▼ +┌───────────────────────────────────────────────────────────────────────────┐ +│ Normalized FoodProduct │ +│ (UPC, Name, Brand, Serving, Calories, Protein, Carbs, Fat, Fiber) │ +└─────────────────────────────────────┬─────────────────────────────────────┘ + │ + ┌──────────────────────────┴──────────────────────────┐ + ▼ ▼ +┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐ +│ Log to Daily Nutrition │ │ Add to Pantry Inventory │ +│ (Inserts into FoodLogsTable) │ │ (Inserts into PantryItemsTable) │ +└──────────────────────────────────────┘ └──────────────────────────────────────┘ +``` + +#### 4.8.2 Domain Model & Contracts + +```dart +// lib/features/nutrition/domain/entities/food_product.dart + +class FoodProduct { + final String id; + final String? barcode; + final String productName; + final String? brand; + final double servingSizeGrams; + final String servingSizeUnit; + final double calories; + final double proteinGrams; + final double carbsGrams; + final double fatGrams; + final double? fiberGrams; + final double? sodiumMg; + final String? imageUrl; + final DateTime lastUpdated; + + const FoodProduct({ + required this.id, + this.barcode, + required this.productName, + this.brand, + required this.servingSizeGrams, + this.servingSizeUnit = 'g', + required this.calories, + required this.proteinGrams, + required this.carbsGrams, + required this.fatGrams, + this.fiberGrams, + this.sodiumMg, + this.imageUrl, + required this.lastUpdated, + }); +} +``` + +#### 4.8.3 Persistence Schema (`FoodProductsTable`) + +```dart +// lib/core/database/tables/food_products_table.dart + +import 'package:drift/drift.dart'; + +class FoodProductsTable extends Table { + TextColumn get id => text()(); // UPC barcode or prefixed UUID ('usda_12345') + TextColumn get barcode => text().nullable()(); + TextColumn get productName => text()(); + TextColumn get brand => text().nullable()(); + RealColumn get servingSizeGrams => real().withDefault(const Constant(100.0))(); + TextColumn get servingSizeUnit => text().withDefault(const Constant('g'))(); + RealColumn get calories => real()(); + RealColumn get proteinGrams => real()(); + RealColumn get carbsGrams => real()(); + RealColumn get fatGrams => real()(); + RealColumn get fiberGrams => real().nullable()(); + RealColumn get sodiumMg => real().nullable()(); + TextColumn get imageUrl => text().nullable()(); + DateTimeColumn get lastUpdated => dateTime()(); + + @override + Set get primaryKey => {id}; +} +``` + +#### 4.8.4 Data Source Specifications & Nutrient Mapping + +| Nutrient Metric | Open Food Facts Nutriments Key | USDA FoodData Central Nutrient ID | Normalization Rule | +|---|---|---|---| +| **Energy (Calories)** | `energy-kcal_serving` / `energy-kcal_100g` | `1008` (Energy) | Expressed as `kcal` (double). | +| **Protein** | `proteins_serving` / `proteins_100g` | `1003` (Protein) | Grams per serving (double). | +| **Carbohydrates** | `carbohydrates_serving` / `carbohydrates_100g` | `1005` (Carbohydrate, by difference) | Grams per serving (double). | +| **Total Fat** | `fat_serving` / `fat_100g` | `1004` (Total lipid) | Grams per serving (double). | +| **Dietary Fiber** | `fiber_serving` / `fiber_100g` | `1079` (Fiber, total dietary) | Grams per serving; nullable. | +| **Sodium** | `sodium_serving` / `sodium_100g` | `1093` (Sodium, Na) | Converted to `mg` (OFF returns grams; multiply by 1000). | + +#### 4.8.5 Network & Resiliency Rules +1. **Mandatory User-Agent:** All HTTP requests to Open Food Facts must include: + ```text + User-Agent: GLP1Companion-App/1.0 (contact: mtkell@kellengineering.com; platform: Flutter) + ``` + Anonymous or default CLI user-agents will trigger upstream proxy 503 throttling. +2. **Circuit Timeout:** Remote calls to Open Food Facts enforce a 4-second timeout. Upon timeout or HTTP 5xx error, the client transparently routes the query to `UsdaFdcClient`. +3. **Write-Through Local Caching:** Every successfully resolved remote payload from OFF or USDA FDC is written into `FoodProductsTable` with `insertOnConflictUpdate`. Subsequent lookups for identical barcodes or query tokens resolve locally with zero network I/O. + ## 4. Multiplatform UX/UI Adaptation Engine The UI layer implements an **Adaptive Design Strategy** rather than a lowest common denominator approach: