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).
This commit is contained in:
Mike Kell 2026-10-17 18:06:53 -04:00
parent 5b5e7e2f57
commit 17b81f567d
1 changed files with 131 additions and 0 deletions

View File

@ -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. 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`. 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<Column> 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 ## 4. Multiplatform UX/UI Adaptation Engine
The UI layer implements an **Adaptive Design Strategy** rather than a lowest common denominator approach: The UI layer implements an **Adaptive Design Strategy** rather than a lowest common denominator approach: