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:
parent
5b5e7e2f57
commit
17b81f567d
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue