diff --git a/CHANGELOG.md b/CHANGELOG.md index ef7aefb0..1d669281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ Adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added + +- Epic #920 — Country-Aware Scanner & Submission Pipeline: 12 issues (#921–#932), + 10 new migrations, `gs1_country_hint()` GS1 prefix → country utility, + `scan_country` + `suggested_country` columns, region-preferred product matching, + country-aware quality scoring, cross-country analytics views + (`v_cross_country_scan_analytics`, `v_cross_country_ean_candidates`, + `v_submission_country_analytics`), admin mismatch badges, 3 new QA checks + (view_consistency 13→16), frontend country propagation for scan/submit flows + ### Fixed - Populate `nutri_score_source` for all products — pipeline now sets source diff --git a/CURRENT_STATE.md b/CURRENT_STATE.md index 30d1b403..71707893 100644 --- a/CURRENT_STATE.md +++ b/CURRENT_STATE.md @@ -1,6 +1,6 @@ # CURRENT_STATE.md -> **Last updated:** 2026-03-17 by GitHub Copilot (session 51) +> **Last updated:** 2026-03-21 by GitHub Copilot (epic #920 closeout) > **Purpose:** Volatile project status for AI agent context recovery. Read this FIRST at session start. --- @@ -8,10 +8,37 @@ ## Active Branch & PR - **Branch:** `main` -- **Latest SHA (main):** `b4e3c877` (fix(scanner): permission-aware error recovery + extract scan page into modules #918) -- **Open PRs:** 0 +- **Latest SHA (main):** `dbe06364` (feat(scanner): cross-country analytics views (#932) (#945)) +- **Open PRs:** 1 (Dependabot #941 — Next.js 16.1.6→16.1.7 security patch, 5 CVEs) - **Mode:** 🟢 Clean — no active work +## Recently Shipped (Epic #920 — Country-Aware Scanner & Submission Pipeline) + +12/12 issues implemented, merged, and closed. Epic #920 closed. + +| PR | Issue | Summary | +| ---- | ----- | ----------------------------------------------------------------- | +| #933 | #921 | `scan_country` column on `scan_history` | +| #934 | #922 | `scan_country` + `suggested_country` on `product_submissions` | +| #935 | #923 | Pass user region through `api_record_scan` / `api_submit_product` | +| #936 | #924 | Frontend scan/submit country propagation | +| #937 | #925 | Admin submission review UI country context | +| #938 | #926 | Region-preferred product matching in `api_record_scan` | +| #939 | #927 | Cross-country product badge in scan result card | +| #940 | #928 | GS1 prefix → country hint utility function | +| #942 | #929 | Country mismatch detection badges in admin review | +| #943 | #930 | Country-scoped pending submission uniqueness | +| #944 | #931 | Country-aware submission quality scoring | +| #945 | #932 | Cross-country analytics views (3 views) | + +**10 new migrations** (`20260320000100`–`20260321000700`), 3 new views, 1 new function (`gs1_country_hint`), 4 modified RPC functions, 3 new QA checks (view consistency 13→16). + +## Recently Shipped (Session 51 — Scanner Error Recovery + Extraction) + +| PR | Summary | +| ---- | ------------------------------------------------------------------------------------- | +| #918 | fix(scanner): permission-aware error recovery + extract scan page into modules (#889) | + ## Recently Shipped (Session 49 — Next.js 16 Upgrade) | PR | Summary | @@ -31,34 +58,11 @@ **Security CVEs patched:** CVE-2025-59471, CVE-2025-59472, CVE-2026-23864 -## Recently Shipped (Session 51 — Scanner Error Recovery + Extraction) - -| PR | Summary | -| ---- | -------------------------------------------------------------------------------------- | -| #918 | fix(scanner): permission-aware error recovery + extract scan page into modules (#889) | - -**Phase 1 — Permission-aware error recovery:** -- 5-state camera error model (`CameraErrorKind`): permission-prompt, permission-denied, permission-unknown, no-camera, generic -- Per-error-kind user guidance with contextual hints and retry/settings CTAs -- `classifyCameraError()` maps DOMException names to structured error kinds -- Browser detection for permission-recovery instructions (Chrome vs Safari vs Firefox) - -**Phase 2 — Scan page extraction (856→463 lines, 46% reduction):** -- `useBarcodeScanner` hook: ZXing camera lifecycle, device enumeration, torch control, watchdog timeout -- `ScannerErrorState` component: 5-state error card with i18n text and callbacks -- `ScanResultView` component: 4 sub-components (Error, NotFound, LookingUp, Found) - -**Phase 3 — CI fixes + test coverage:** -- Fixed `Typecheck & Lint` CI failure: downgraded `react-hooks/purity` to "warn" (5th React Compiler rule) -- 66 new tests: 24 hook tests + 20 error state tests + 22 result view tests -- i18n: en/pl/de `scan.cameraStarting` key + error state strings -- 12 files changed, +1,992 / -433 lines - ## Recently Shipped (Session 50 — Scanner Black Camera Fix) -| PR | Summary | -| ---- | -------------------------------------------------------------------------------------- | -| #916 | fix(scanner): resolve black camera feed with event-driven stream readiness (#889) | +| PR | Summary | +| ---- | --------------------------------------------------------------------------------- | +| #916 | fix(scanner): resolve black camera feed with event-driven stream readiness (#889) | **6 root causes diagnosed and fixed in scan/page.tsx:** - `decodeFromVideoDevice()` not awaited → now properly awaited @@ -70,12 +74,6 @@ **3 new tests + MediaStream jsdom stub. i18n: en/pl/de `scan.cameraStarting` key.** -## Recently Shipped (Session 49 — Next.js 16 Upgrade) - -| PR | Summary | -| ---- | ----------------------------------------------------------------------------------- | -| #904 | chore(deps): bump next 15.5.12 → 16.1.6 + eslint-config-next 16.1.6 (MAJOR upgrade) | - ## Recently Shipped (Session 48 — CI Baseline Restoration) | PR | Summary | @@ -122,8 +120,8 @@ | ------------ | ------ | --------------------------------------------------------------------- | | pr-gate | ✅ | Typecheck, lint, unit tests, build, Playwright smoke | | main-gate | ✅ | All passing | -| qa.yml | ✅ | 756/756 checks passing (48 suites) | -| deploy.yml | ✅ | All 209 migrations on production (deployed 2026-03-16T08:24:26Z) | +| qa.yml | ✅ | 759/759 checks passing (48 suites) | +| deploy.yml | ⚠️ | 209/227 migrations on production — 18 pending deploy (epic #920) | | dep-audit | ✅ | 0 high/critical vulnerabilities | | python-lint | ✅ | 0 ruff errors | | quality-gate | ✅ | Passing — ReDoS hotspot resolved (PR #914) | @@ -137,6 +135,7 @@ | #212 | Deferred | Infrastructure Cost Attribution Framework | Issue #889 fully resolved and closed with PR #918 merge. +Epic #920 fully resolved and closed — all 12/12 issues shipped. ## Milestones Completed @@ -193,7 +192,7 @@ These are documented follow-ups, not active work items. Address opportunisticall - **Products (local DB):** 2,602 active (1,380 PL + 1,222 DE across 21 active + 1 deactivated category) - **Deprecated products:** 58 -- **QA checks:** 756 total (48 suites) — 47 pass, 1 pre-existing failure (Suite 11 NutriRange), 1 warning (local DB) +- **QA checks:** 759 total (48 suites) — view_consistency +3 checks for cross-country analytics views - **Negative tests:** 23/23 caught - **EAN coverage:** 2,261/2,264 with EAN (99.9%) — local DB - **Ingredient refs:** 3,100 (local, after orphan cleanup from 6,279) @@ -205,9 +204,9 @@ These are documented follow-ups, not active work items. Address opportunisticall - **Nutrition coverage (production):** 2,438/2,438 (100%) - **Frontend test coverage:** ~92% lines (SonarCloud Quality Gate passing) - **ESLint warnings:** 23 (React Compiler rules — 5 rules downgraded to warn, see follow-ups) -- **Open issues:** 1 | **Open PRs:** 0 -- **Vitest:** 5,733 tests passing across 348 test files -- **DB migrations:** 209 append-only (all 209 applied to production) +- **Open issues:** 1 | **Open PRs:** 1 (Dependabot #941) +- **Vitest:** 5,755 tests passing across 348 test files +- **DB migrations:** 227 append-only (209 applied to production, 18 pending from epic #920) - **pgTAP test files:** 17 - **Ruff lint:** 0 errors - **GitHub Ruleset:** strict_required_status_checks_policy = true diff --git a/README.md b/README.md index 91da6695..14bc11a7 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@
-
+
-
+
@@ -137,7 +137,7 @@ supabase db reset # Full rebuild (migrations + seed)
.\RUN_SEED.ps1 # Seed reference data only
# ── Testing ──
-.\RUN_QA.ps1 # 747 QA checks across 48 suites
+.\RUN_QA.ps1 # 759 QA checks across 48 suites
.\RUN_NEGATIVE_TESTS.ps1 # 23 constraint violation tests
.\RUN_SANITY.ps1 -Env local # Row-count + schema assertions
python validate_eans.py # EAN checksum validation
@@ -168,7 +168,7 @@ echo "SELECT * FROM v_master LIMIT 5;" | docker exec -i supabase_db_tryvit psql
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────────┐
│ Open Food Facts │────▶│ Python Pipeline │────▶│ PostgreSQL (Supabase) │
-│ API v2 │ │ sql_generator │ │ 199 migrations │
+│ API v2 │ │ sql_generator │ │ 227 migrations │
│ (category tags, │ │ validator │ │ 43 pipeline folders │
│ countries=PL,DE│ │ off_client │ │ products + nutrition │
└─────────────────┘ └──────────────────┘ │ + ingredients + scores │
@@ -231,18 +231,18 @@ Every score is fully explainable via `api_score_explanation()` — returns the 9
| 2,438 Active Products |
+ 2,602 Active Products |
43 Categories |
PL + DE Markets |
2,995 Ingredients |
99.9% EAN Coverage |
- 199 Migrations |
+ 227 Migrations |
| 747 QA Checks |
+ 759 QA Checks |
48 Test Suites |
23 Negative Tests |
≥88% Line Coverage |
@@ -296,11 +296,11 @@ tryvit/
│ │ ├── chips-pl/ # Reference PL implementation
│ │ ├── chips-de/ # DE Chips (51 products)
│ │ └── ... (21 more PL + 20 DE) # Variable product counts per category
-│ ├── qa/ # 48 test suites (747 checks)
+│ ├── qa/ # 48 test suites (759 checks)
│ └── views/ # Reference view definitions
│
├── supabase/
-│ ├── migrations/ # 199 append-only schema migrations
+│ ├── migrations/ # 227 append-only schema migrations
│ ├── seed/ # Reference data seeds
│ ├── tests/ # pgTAP integration tests
│ └── functions/ # Edge Functions (API gateway, push notifications, CAPTCHA)
@@ -327,7 +327,7 @@ tryvit/
├── monitoring/ # Alert definitions
│
├── RUN_LOCAL.ps1 # Pipeline runner (idempotent)
-├── RUN_QA.ps1 # QA test runner (747 checks)
+├── RUN_QA.ps1 # QA test runner (759 checks)
├── RUN_NEGATIVE_TESTS.ps1 # Negative test runner (23 tests)
├── RUN_SANITY.ps1 # Sanity checks
├── CHANGELOG.md # Structured changelog
@@ -400,7 +400,7 @@ Every change is validated against **747 automated checks** across 48 QA suites p
1. **PR Gate** — Typecheck → Lint → Build → Unit tests → Playwright smoke E2E
2. **Main Gate** — Above + Coverage → SonarCloud Quality Gate
-3. **QA Gate** — Schema → Pipelines → 747 QA checks → Sanity → Confidence threshold
+3. **QA Gate** — Schema → Pipelines → 759 QA checks → Sanity → Confidence threshold
4. **Nightly** — Full Playwright (all projects) + Data Integrity Audit
---
diff --git a/copilot-instructions.md b/copilot-instructions.md
index ecf44756..e432cd2b 100644
--- a/copilot-instructions.md
+++ b/copilot-instructions.md
@@ -1,15 +1,15 @@
# Copilot Instructions — TryVit
> **Last updated:** 2026-03-05
-> **Scope:** Poland (`PL`) primary + Germany (`DE`) full parity (1,168 products across 21 categories)
-> **Products:** ~2,366 active (22 PL categories + 21 DE categories), 273 deprecated
+> **Scope:** Poland (`PL`) primary + Germany (`DE`) full parity (1,222 products across 21 categories)
+> **Products:** ~2,602 active (22 PL categories + 21 DE categories), 58 deprecated
> **Categories:** 29 defined (22 PL + 21 DE active + 7 new cross-country, not yet populated)
> **EAN coverage:** 2,261/2,264 (99.9%)
> **Scoring:** v3.3 — 9-factor weighted penalty + nutrient density bonus via `compute_unhealthiness_v33()` (protein & fibre credit)
> **Servings:** removed as separate table — all nutrition data is per-100g on nutrition_facts
> **Ingredient analytics:** 5,340 unique ingredients (all clean ASCII English), 2,691 allergen declarations, 2,702 trace declarations
> **Ingredient concerns:** EFSA-based 4-tier additive classification (0=none, 1=low, 2=moderate, 3=high)
-> **QA:** 756 checks across 48 suites + 23 negative validation tests — all passing
+> **QA:** 759 checks across 48 suites + 23 negative validation tests — all passing
---
@@ -115,7 +115,7 @@ tryvit/
│ │ ├── QA__data_quality.sql # 25 data quality checks
│ │ ├── QA__data_consistency.sql # 24 data consistency checks
│ │ ├── QA__referential_integrity.sql # 18 referential integrity checks
-│ │ ├── QA__view_consistency.sql # 13 view consistency checks
+│ │ ├── QA__view_consistency.sql # 16 view consistency checks
│ │ ├── QA__naming_conventions.sql # 12 naming convention checks
│ │ ├── QA__nutrition_ranges.sql # 16 nutrition range checks
│ │ ├── QA__allergen_integrity.sql # 15 allergen integrity checks
@@ -169,7 +169,7 @@ tryvit/
│ │ ├── api-gateway/ # Write-path gateway (rate limiting, validation) (#478)
│ │ └── send-push-notification/ # Push notification handler
│ ├── dr-drill/ # Disaster recovery drill artifacts
-│ └── migrations/ # 203 append-only schema migrations
+│ └── migrations/ # 227 append-only schema migrations
│ ├── 20260207000100_create_schema.sql
│ ├── 20260207000200_baseline.sql
│ ├── 20260207000300_add_chip_metadata.sql
@@ -283,7 +283,7 @@ tryvit/
│ ├── 006-append-only-migrations.md
│ └── 007-english-canonical-ingredients.md
├── RUN_LOCAL.ps1 # Pipeline runner (idempotent)
-├── RUN_QA.ps1 # QA test runner (756 checks across 48 suites)
+├── RUN_QA.ps1 # QA test runner (759 checks across 48 suites)
├── RUN_NEGATIVE_TESTS.ps1 # Negative test runner (23 injection tests)
├── RUN_SANITY.ps1 # Sanity checks (16) — row counts, schema assertions
├── RUN_REMOTE.ps1 # Remote deployment (requires confirmation)
@@ -357,7 +357,7 @@ tryvit/
│ ├── pr-gate.yml # Lint → Typecheck → Build → Playwright E2E
│ ├── pr-title-lint.yml # PR title conventional-commit validation (all PRs)
│ ├── main-gate.yml # Build → Unit tests + coverage → SonarCloud
-│ ├── qa.yml # Schema → Pipelines → QA (756) → Sanity
+│ ├── qa.yml # Schema → Pipelines → QA (759) → Sanity
│ ├── nightly.yml # Full Playwright (all projects) + Data Integrity Audit
│ ├── deploy.yml # Manual trigger → Schema diff → Approval → db push
│ ├── sync-cloud-db.yml # Remote DB sync
@@ -456,50 +456,51 @@ tryvit/
### Key Functions
-| Function | Purpose |
-| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `compute_unhealthiness_v33()` | Scores 1–100 from 9 penalty factors + nutrient density bonus: sat fat, sugars, salt, calories, trans fat, additives, prep, controversies, ingredient concern − protein/fibre bonus |
-| `explain_score_v33()` | Returns JSONB breakdown of score: final_score + 10 factors (9 penalties + 1 bonus) with name, weight, raw (0–100), weighted, input, ceiling |
-| `find_similar_products()` | Top-N products by Jaccard ingredient similarity (returns product details + similarity coefficient) |
-| `find_better_alternatives()` | Healthier substitutes in same/any category, ranked by score improvement and ingredient overlap |
-| `resolve_ingredient_name()` | Returns localized ingredient name. Fallback: requested lang → en translation → name_en → NULL |
-| `assign_confidence()` | Returns `'verified'`/`'estimated'`/`'low'` from data completeness |
-| `score_category()` | Consolidated scoring procedure: Steps 0/1/4/5 (concern defaults, unhealthiness, flags + dynamic `data_completeness_pct`, confidence) for a given category |
-| `compute_data_confidence()` | Composite confidence score (0-100) with 6 components; band, completeness profile |
-| `compute_data_completeness()` | Dynamic 15-checkpoint field-coverage function for `data_completeness_pct` (EAN, 9 nutrition, Nutri-Score, NOVA, ingredients, allergens, source) |
-| `api_data_confidence()` | API wrapper for compute_data_confidence(); returns structured JSONB |
-| `api_product_detail()` | Single product as structured JSONB (identity, scores, flags, nutrition, ingredients, allergens, trust) |
-| `api_category_listing()` | Paged category listing with sort (score\|calories\|protein\|name\|nutri_score) + pagination |
-| `api_score_explanation()` | Score breakdown + human-readable headline + warnings + category context (rank, avg, relative position) |
-| `api_better_alternatives()` | Healthier substitutes wrapper with source product context and structured JSON |
-| `api_search_products()` | Full-text + trigram search across product_name and brand; uses pg_trgm GIN indexes |
-| `api_get_cross_country_links()` | Returns linked products for a given product_id; bidirectional query across product_links; returns JSONB array |
-| `api_get_recipes()` | Browse published recipes with filters (country, category, tag, difficulty, max_time); paginated JSONB with total_count + recipes array |
-| `api_get_recipe_detail()` | Full recipe detail by slug: recipe metadata + ingredients (with linked products) + steps; returns structured JSONB |
-| `api_get_recipe_nutrition()` | Aggregate nutrition summary from linked products; picks primary product per ingredient; returns per-100g averages + coverage_pct |
-| `browse_recipes()` | Browse published recipes with filters (category, country, tag, difficulty, max_time); returns TABLE rows |
-| `get_recipe_detail()` | Recipe detail by slug: returns JSONB with metadata, steps, ingredients (with linked_products array) |
-| `find_products_for_recipe_ingredient()` | Finds products for a recipe ingredient: admin-curated links first, then auto-suggested via ingredient_ref matching |
-| `refresh_all_materialized_views()` | Refreshes all MVs concurrently; returns timing report JSONB |
-| `mv_staleness_check()` | Checks if MVs are stale by comparing row counts to source tables |
-| `check_formula_drift()` | Compares stored SHA-256 fingerprints against recomputed hashes for active scoring/search formulas |
-| `check_function_source_drift()` | Compares registered pg_proc source hashes against actual function bodies for critical functions |
-| `governance_drift_check()` | Master drift detection runner — 8 checks across scoring, search, naming conventions, and feature flags |
-| `log_drift_check()` | Executes governance_drift_check() and persists results into drift_check_results; returns run_id UUID |
-| `validate_log_entry()` | Validates a structured log JSON entry against LOG_SCHEMA.md spec; returns `{valid: true}` or `{valid: false, errors: [...]}` |
-| `execute_retention_cleanup()` | Deletes audit rows older than retention_policies window; SECURITY DEFINER, dry-run by default, batch deletion via ctid; returns JSONB summary |
-| `mv_last_refresh()` | Returns the most recent refresh per MV; columns: mv_name, refreshed_at, duration_ms, row_count, triggered_by, age_minutes |
-| `check_flag_readiness()` | Returns activation readiness status for all feature flags — dependency resolution, expiry tracking, status (ready/blocked/expired/enabled) |
-| `api_export_user_data()` | GDPR Art.15/20 — exports all user data as structured JSONB (preferences, health profiles, lists, comparisons, searches, scans). SECURITY DEFINER |
-| `api_delete_user_data()` | GDPR Art.17 — cascading delete across 8 tables in FK-safe order; writes anonymized audit to `deletion_audit_log`. SECURITY DEFINER |
-| `is_valid_ean()` | GS1 checksum validation for EAN-8/EAN-13 barcodes. IMMUTABLE STRICT — returns NULL for NULL, false for invalid |
-| `check_submission_rate_limit()` | Returns rate limit status for product submissions: 10 per 24h rolling window per user. SECURITY DEFINER |
-| `check_scan_rate_limit()` | Returns rate limit status for barcode scans: 100 per 24h rolling window per user. SECURITY DEFINER |
-| `check_api_rate_limit()` | Generic per-endpoint rate limiter: checks `api_rate_limits` config, logs to `api_rate_limit_log`. Returns `{allowed, remaining}` or blocked JSONB. SECURITY DEFINER |
-| `check_share_limit()` | Per-user share count limiter: max 50 shared items per type (comparisons/lists). Returns `{allowed, type, limit}` JSONB. SECURITY DEFINER |
-| `score_submission_quality()` | Scores a submission's quality (0-100) from 7 signals: account age, velocity, EAN match, photo, brand/name quality, user trust score. SECURITY DEFINER |
-| `api_admin_batch_reject_user()` | Rejects all pending/manual_review/flag_for_review submissions from a user, flags trust score (cap at 10). SECURITY DEFINER |
-| `api_admin_submission_velocity()` | Returns submission velocity stats: last_24h, last_7d, pending_count, auto_rejected_24h, status_breakdown, top_submitters with trust. SECURITY DEFINER |
+| Function | Purpose |
+| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `compute_unhealthiness_v33()` | Scores 1–100 from 9 penalty factors + nutrient density bonus: sat fat, sugars, salt, calories, trans fat, additives, prep, controversies, ingredient concern − protein/fibre bonus |
+| `explain_score_v33()` | Returns JSONB breakdown of score: final_score + 10 factors (9 penalties + 1 bonus) with name, weight, raw (0–100), weighted, input, ceiling |
+| `find_similar_products()` | Top-N products by Jaccard ingredient similarity (returns product details + similarity coefficient) |
+| `find_better_alternatives()` | Healthier substitutes in same/any category, ranked by score improvement and ingredient overlap |
+| `resolve_ingredient_name()` | Returns localized ingredient name. Fallback: requested lang → en translation → name_en → NULL |
+| `assign_confidence()` | Returns `'verified'`/`'estimated'`/`'low'` from data completeness |
+| `score_category()` | Consolidated scoring procedure: Steps 0/1/4/5 (concern defaults, unhealthiness, flags + dynamic `data_completeness_pct`, confidence) for a given category |
+| `compute_data_confidence()` | Composite confidence score (0-100) with 6 components; band, completeness profile |
+| `compute_data_completeness()` | Dynamic 15-checkpoint field-coverage function for `data_completeness_pct` (EAN, 9 nutrition, Nutri-Score, NOVA, ingredients, allergens, source) |
+| `api_data_confidence()` | API wrapper for compute_data_confidence(); returns structured JSONB |
+| `api_product_detail()` | Single product as structured JSONB (identity, scores, flags, nutrition, ingredients, allergens, trust) |
+| `api_category_listing()` | Paged category listing with sort (score\|calories\|protein\|name\|nutri_score) + pagination |
+| `api_score_explanation()` | Score breakdown + human-readable headline + warnings + category context (rank, avg, relative position) |
+| `api_better_alternatives()` | Healthier substitutes wrapper with source product context and structured JSON |
+| `api_search_products()` | Full-text + trigram search across product_name and brand; uses pg_trgm GIN indexes |
+| `api_get_cross_country_links()` | Returns linked products for a given product_id; bidirectional query across product_links; returns JSONB array |
+| `api_get_recipes()` | Browse published recipes with filters (country, category, tag, difficulty, max_time); paginated JSONB with total_count + recipes array |
+| `api_get_recipe_detail()` | Full recipe detail by slug: recipe metadata + ingredients (with linked products) + steps; returns structured JSONB |
+| `api_get_recipe_nutrition()` | Aggregate nutrition summary from linked products; picks primary product per ingredient; returns per-100g averages + coverage_pct |
+| `browse_recipes()` | Browse published recipes with filters (category, country, tag, difficulty, max_time); returns TABLE rows |
+| `get_recipe_detail()` | Recipe detail by slug: returns JSONB with metadata, steps, ingredients (with linked_products array) |
+| `find_products_for_recipe_ingredient()` | Finds products for a recipe ingredient: admin-curated links first, then auto-suggested via ingredient_ref matching |
+| `refresh_all_materialized_views()` | Refreshes all MVs concurrently; returns timing report JSONB |
+| `mv_staleness_check()` | Checks if MVs are stale by comparing row counts to source tables |
+| `check_formula_drift()` | Compares stored SHA-256 fingerprints against recomputed hashes for active scoring/search formulas |
+| `check_function_source_drift()` | Compares registered pg_proc source hashes against actual function bodies for critical functions |
+| `governance_drift_check()` | Master drift detection runner — 8 checks across scoring, search, naming conventions, and feature flags |
+| `log_drift_check()` | Executes governance_drift_check() and persists results into drift_check_results; returns run_id UUID |
+| `validate_log_entry()` | Validates a structured log JSON entry against LOG_SCHEMA.md spec; returns `{valid: true}` or `{valid: false, errors: [...]}` |
+| `execute_retention_cleanup()` | Deletes audit rows older than retention_policies window; SECURITY DEFINER, dry-run by default, batch deletion via ctid; returns JSONB summary |
+| `mv_last_refresh()` | Returns the most recent refresh per MV; columns: mv_name, refreshed_at, duration_ms, row_count, triggered_by, age_minutes |
+| `check_flag_readiness()` | Returns activation readiness status for all feature flags — dependency resolution, expiry tracking, status (ready/blocked/expired/enabled) |
+| `api_export_user_data()` | GDPR Art.15/20 — exports all user data as structured JSONB (preferences, health profiles, lists, comparisons, searches, scans). SECURITY DEFINER |
+| `api_delete_user_data()` | GDPR Art.17 — cascading delete across 8 tables in FK-safe order; writes anonymized audit to `deletion_audit_log`. SECURITY DEFINER |
+| `is_valid_ean()` | GS1 checksum validation for EAN-8/EAN-13 barcodes. IMMUTABLE STRICT — returns NULL for NULL, false for invalid |
+| `gs1_country_hint()` | Extracts GS1 country-of-registration hint from EAN-13 prefix. IMMUTABLE STRICT — returns JSONB `{code, name, confidence}` where confidence ∈ {high, low, none}. Not definitive for imported products |
+| `check_submission_rate_limit()` | Returns rate limit status for product submissions: 10 per 24h rolling window per user. SECURITY DEFINER |
+| `check_scan_rate_limit()` | Returns rate limit status for barcode scans: 100 per 24h rolling window per user. SECURITY DEFINER |
+| `check_api_rate_limit()` | Generic per-endpoint rate limiter: checks `api_rate_limits` config, logs to `api_rate_limit_log`. Returns `{allowed, remaining}` or blocked JSONB. SECURITY DEFINER |
+| `check_share_limit()` | Per-user share count limiter: max 50 shared items per type (comparisons/lists). Returns `{allowed, type, limit}` JSONB. SECURITY DEFINER |
+| `score_submission_quality()` | Scores a submission's quality (0-100) from 7 signals: account age, velocity, EAN match, photo, brand/name quality, user trust score. SECURITY DEFINER |
+| `api_admin_batch_reject_user()` | Rejects all pending/manual_review/flag_for_review submissions from a user, flags trust score (cap at 10). SECURITY DEFINER |
+| `api_admin_submission_velocity()` | Returns submission velocity stats: last_24h, last_7d, pending_count, auto_rejected_24h, status_breakdown, top_submitters with trust. SECURITY DEFINER |
### Views
@@ -517,6 +518,12 @@ tryvit/
**`mv_scoring_distribution`** — Materialized view of scoring band distribution per country and category. Columns: country, category, band (Green/Yellow/Orange/Red/Dark Red), product_count, pct_of_category, avg_score, min_score, max_score, stddev_score. Unique index on (country, category, band). Refreshed by `refresh_all_materialized_views()`.
+**`v_cross_country_scan_analytics`** — Per-country scan metrics. Columns: scan_country, total_scans, found_scans, missed_scans, miss_rate_pct, unique_eans_scanned, unique_eans_missed. Used for cross-country scanner performance monitoring.
+
+**`v_cross_country_ean_candidates`** — EANs scanned in more than one country — candidates for product_links. Columns: ean, scanned_in_countries (text[]), country_count, first_scanned, last_scanned, total_scans.
+
+**`v_submission_country_analytics`** — Per-country submission metrics. Columns: suggested_country, total_submissions, pending, approved, rejected, merged, acceptance_rate_pct. Used for cross-country submission monitoring.
+
### Edge Functions
> **Location:** `supabase/functions/`
@@ -671,7 +678,7 @@ a mix of `'baked'`, `'fried'`, and `'none'`.
## 7. Migrations
-**Location:** `supabase/migrations/` — managed by Supabase CLI. Currently **203 migrations**.
+**Location:** `supabase/migrations/` — managed by Supabase CLI. Currently **227 migrations**.
**Rules:**
@@ -743,7 +750,7 @@ A change is **not done** unless relevant tests were added/updated, every suite i
| Component tests | **Testing Library React** + Vitest | `frontend/src/components/**/*.test.tsx` | same as above |
| E2E smoke | **Playwright 1.58** (Chromium) | `frontend/e2e/smoke.spec.ts` | `cd frontend && npx playwright test` |
| E2E auth | Playwright (requires `SUPABASE_SERVICE_ROLE_KEY`) | `frontend/e2e/authenticated.spec.ts` | same (CI auto-detects key) |
-| DB QA (756 checks) | Raw SQL (zero rows = pass) | `db/qa/QA__*.sql` (48 suites) | `.\RUN_QA.ps1` |
+| DB QA (759 checks) | Raw SQL (zero rows = pass) | `db/qa/QA__*.sql` (48 suites) | `.\RUN_QA.ps1` |
| Negative validation | SQL injection/constraint tests | `db/qa/TEST__negative_checks.sql` | `.\RUN_NEGATIVE_TESTS.ps1` |
| DB sanity | Row-count + schema assertions | via `RUN_SANITY.ps1` | `.\RUN_SANITY.ps1 -Env local` |
| Pipeline structure | Python validator | `check_pipeline_structure.py` | `python check_pipeline_structure.py` |
@@ -884,7 +891,7 @@ E2E tests are the **only** exception — they run against a live dev server but
- **`pr-title-lint.yml`**: PR title conventional-commit validation (all PRs)
- **`main-gate.yml`**: Typecheck → Lint → Build → Unit tests with coverage → Playwright smoke E2E → SonarCloud scan + BLOCKING Quality Gate → Sentry sourcemap upload
- **`nightly.yml`**: Full Playwright (all projects incl. visual regression) + Data Integrity Audit (parallel)
- - **`qa.yml`**: Pipeline structure guard → Schema migrations → Schema drift detection → Pipelines → QA (756 checks) → Sanity (17 checks) → Confidence threshold
+ - **`qa.yml`**: Pipeline structure guard → Schema migrations → Schema drift detection → Pipelines → QA (759 checks) → Sanity (17 checks) → Confidence threshold
- **`deploy.yml`**: Manual trigger → Schema diff → Approval gate (production) → Pre-deploy backup → `supabase db push` → Post-deploy sanity
- **`sync-cloud-db.yml`**: Auto-sync migrations to production on merge to `main`
- **Required (merge-blocking) checks:** `Unit Tests`, `Playwright Smoke`, `Typecheck & Lint`, `Build`. These four must pass before a PR can merge.
@@ -921,7 +928,7 @@ If adding/changing DB schema or SQL functions:
- For rollback procedures, see `DEPLOYMENT.md` → **Rollback Procedures** (5 scenarios + emergency checklist).
- Add a QA check that verifies the migration outcome (row counts, constraint behavior).
- Ensure idempotency (`IF NOT EXISTS`, `ON CONFLICT`, `DO UPDATE SET`).
-- Run `.\RUN_QA.ps1` to verify all 756 checks pass + `.\RUN_NEGATIVE_TESTS.ps1` for 23 injection tests.
+- Run `.\RUN_QA.ps1` to verify all 759 checks pass + `.\RUN_NEGATIVE_TESTS.ps1` for 23 injection tests.
### 8.14 Snapshots Are Not Enough
@@ -975,7 +982,7 @@ At the end of every PR-like change, include a **Verification** section:
| Confidence Reporting | `QA__confidence_reporting.sql` | 7 | Yes |
| Data Quality | `QA__data_quality.sql` | 29 | Yes |
| Ref. Integrity | `QA__referential_integrity.sql` | 18 | Yes |
-| View Consistency | `QA__view_consistency.sql` | 13 | Yes |
+| View Consistency | `QA__view_consistency.sql` | 16 | Yes |
| Naming Conventions | `QA__naming_conventions.sql` | 12 | Yes |
| Nutrition Ranges | `QA__nutrition_ranges.sql` | 20 | Yes |
| Data Consistency | `QA__data_consistency.sql` | 26 | Yes |
@@ -1016,7 +1023,7 @@ At the end of every PR-like change, include a **Verification** section:
| Scoring Band Distribution | `QA__scoring_distribution.sql` | 12 | No |
| **Negative Validation** | `TEST__negative_checks.sql` | 23 | Yes |
-**Run:** `.\RUN_QA.ps1` — expects **768/768 checks passing** (+ EAN validation).
+**Run:** `.\RUN_QA.ps1` — expects **771/771 checks passing** (+ EAN validation).
**Run:** `.\RUN_NEGATIVE_TESTS.ps1` — expects **23/23 caught**.
### 8.19 Key Regression Tests (Scoring Suite)
@@ -1174,7 +1181,7 @@ security(rls): lock down product_submissions to authenticated users
**Pre-commit checklist:**
-1. `.\RUN_QA.ps1` — 756/756 pass
+1. `.\RUN_QA.ps1` — 759/759 pass
2. No credentials in committed files
3. No modifications to existing `supabase/migrations/`
4. Docs updated if schema or methodology changed
@@ -1548,7 +1555,7 @@ Before a feature is considered complete, verify against all CI gates:
| Gate | Command | Expected |
| ------------------- | ------------------------------------- | ------------------------------- |
| Pipeline structure | `python check_pipeline_structure.py` | 0 errors |
-| DB QA | `.\RUN_QA.ps1` | All checks pass (currently 756) |
+| DB QA | `.\RUN_QA.ps1` | All checks pass (currently 759) |
| Negative tests | `.\RUN_NEGATIVE_TESTS.ps1` | All caught (currently 23) |
| pgTAP tests | `supabase test db` | All pass |
| TypeScript | `cd frontend && npx tsc --noEmit` | 0 errors |
@@ -1693,10 +1700,10 @@ Produce **exactly this structure** — fill every section with real data:
| Metric | Current Value | Target / Baseline | Status |
| ------------------------- | --------------- | ----------------------- | -------- |
-| Active products (PL+DE) | ~X,XXX | ≥2,366 | ✅/⚠️/❌ |
-| QA checks passing | XXX/756 | 756/756 | ✅/⚠️/❌ |
+| Active products (PL+DE) | ~X,XXX | ≥2,602 | ✅/⚠️/❌ |
+| QA checks passing | XXX/759 | 759/759 | ✅/⚠️/❌ |
| Negative tests passing | 23/23 | 23/23 | ✅/⚠️/❌ |
-| Migrations committed | XXX | ≥199 | ✅/⚠️/❌ |
+| Migrations committed | XXX | ≥227 | ✅/⚠️/❌ |
| Vitest coverage (lines) | XX% | ≥88% | ✅/⚠️/❌ |
| SonarCloud quality gate | PASS/FAIL | PASS | ✅/⚠️/❌ |
| EAN coverage | XXXX/XXXX (XX%) | ≥99.8% | ✅/⚠️/❌ |
@@ -2015,7 +2022,7 @@ Else → fallback Z (always safe, always returns a value)
## Verification Checklist (Definition of Done)
- [ ] `python check_pipeline_structure.py` — 0 errors
-- [ ] `.\RUN_QA.ps1` — all 756 checks pass
+- [ ] `.\RUN_QA.ps1` — all 759 checks pass
- [ ] `.\RUN_NEGATIVE_TESTS.ps1` — 23/23 caught
- [ ] `supabase test db` — all pgTAP pass
- [ ] `cd frontend && npx tsc --noEmit` — 0 errors
@@ -2137,15 +2144,15 @@ Then execute §19 (Canonical Execution Discipline Protocol v2) in full.
### 18.1 Architecture & System Design
-| Document | Purpose | Load When |
-| ------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------- |
-| `docs/ARCHITECTURE.md` | Full system architecture — data flow, schema topology, scoring pipeline, API layer, security perimeter | Any schema, API, or infra work |
-| `docs/DOMAIN_BOUNDARIES.md` | Domain ownership map — who owns what, cross-domain coupling rules | Adding new domains, touching multiple services |
-| `docs/ENVIRONMENT_STRATEGY.md` | Local / staging / production environment strategy, secret management | Env config, deployment, secrets changes |
-| `docs/STAGING_SETUP.md` | Staging environment setup guide | Setting up staging, CI config |
-| `docs/DEPLOYMENT.md` | Deployment procedures, rollback playbook, emergency checklist | Any production deployment |
-| `docs/DISASTER_DRILL_REPORT.md` | DR drill findings and follow-up actions | Incident prep, DR automation |
-| `docs/PRODUCTION_DATA.md` | Production data management rules — no PII, retention policies | Any migration touching prod |
+| Document | Purpose | Load When |
+| ------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
+| `docs/ARCHITECTURE.md` | Full system architecture — data flow, schema topology, scoring pipeline, API layer, security perimeter | Any schema, API, or infra work |
+| `docs/DOMAIN_BOUNDARIES.md` | Domain ownership map — who owns what, cross-domain coupling rules | Adding new domains, touching multiple services |
+| `docs/ENVIRONMENT_STRATEGY.md` | Local / staging / production environment strategy, secret management | Env config, deployment, secrets changes |
+| `docs/STAGING_SETUP.md` | Staging environment setup guide | Setting up staging, CI config |
+| `docs/DEPLOYMENT.md` | Deployment procedures, rollback playbook, emergency checklist | Any production deployment |
+| `docs/DISASTER_DRILL_REPORT.md` | DR drill findings and follow-up actions | Incident prep, DR automation |
+| `docs/PRODUCTION_DATA.md` | Production data management rules — no PII, retention policies | Any migration touching prod |
| `docs/HEALTH_GOAL_PERSONALIZATION.md` | Health-goal personalization design — goal taxonomy, personalization model, MVP scope, privacy, copy safety | Health profile, personalization, onboarding goals |
### 18.2 API & Frontend
@@ -2229,17 +2236,17 @@ Then execute §19 (Canonical Execution Discipline Protocol v2) in full.
### 18.9 Architecture Decision Records
-| Decision | Status | Summary |
-| ----------------------------------------------------- | -------- | ------------------------------------------------------------------------------ |
-| `docs/decisions/001-postgresql-only-stack.md` | Accepted | No ORM, no Redis cluster, PostgreSQL as sole data store |
-| `docs/decisions/002-weighted-scoring-formula.md` | Accepted | 9-factor weighted model, science-backed, EFSA-aligned |
-| `docs/decisions/003-country-scoped-isolation.md` | Accepted | All queries country-filtered, no cross-contamination |
-| `docs/decisions/004-pipeline-generates-sql.md` | Accepted | Python pipeline generates idempotent SQL, no runtime inserts |
-| `docs/decisions/005-api-function-name-versioning.md` | Accepted | Additive versioning via suffix (`_v2`), never rename |
-| `docs/decisions/006-append-only-migrations.md` | Accepted | Never modify committed migrations; forward-only schema evolution |
-| `docs/decisions/007-english-canonical-ingredients.md` | Accepted | `name_en` is canonical; translations stored in `ingredient_translations` |
-| `docs/decisions/008-nutrient-density-bonus.md` | Accepted | Nutrient density bonus (protein + fibre) as subtracted 10th factor in v3.3 |
-| `docs/decisions/009-scoring-band-calibration.md` | Accepted | Scoring band calibration — catalog limited, formula correct, no changes needed |
+| Decision | Status | Summary |
+| ----------------------------------------------------- | -------- | -------------------------------------------------------------------------------- |
+| `docs/decisions/001-postgresql-only-stack.md` | Accepted | No ORM, no Redis cluster, PostgreSQL as sole data store |
+| `docs/decisions/002-weighted-scoring-formula.md` | Accepted | 9-factor weighted model, science-backed, EFSA-aligned |
+| `docs/decisions/003-country-scoped-isolation.md` | Accepted | All queries country-filtered, no cross-contamination |
+| `docs/decisions/004-pipeline-generates-sql.md` | Accepted | Python pipeline generates idempotent SQL, no runtime inserts |
+| `docs/decisions/005-api-function-name-versioning.md` | Accepted | Additive versioning via suffix (`_v2`), never rename |
+| `docs/decisions/006-append-only-migrations.md` | Accepted | Never modify committed migrations; forward-only schema evolution |
+| `docs/decisions/007-english-canonical-ingredients.md` | Accepted | `name_en` is canonical; translations stored in `ingredient_translations` |
+| `docs/decisions/008-nutrient-density-bonus.md` | Accepted | Nutrient density bonus (protein + fibre) as subtracted 10th factor in v3.3 |
+| `docs/decisions/009-scoring-band-calibration.md` | Accepted | Scoring band calibration — catalog limited, formula correct, no changes needed |
| `docs/decisions/010-ingredient-language-model.md` | Accepted | English canonical + `ingredient_translations` fallback; no schema changes needed |
---
@@ -2459,7 +2466,7 @@ Execute every command. Record output. Do not skip.
```powershell
# ── Database layer ───────────────────────────────────────────────────
supabase test db # All pgTAP tests pass
-.\RUN_QA.ps1 # All 756+ QA checks pass
+.\RUN_QA.ps1 # All 759+ QA checks pass
.\RUN_NEGATIVE_TESTS.ps1 # All 23 negative tests caught
python check_pipeline_structure.py # 0 errors
python validate_eans.py # 0 EAN failures
@@ -2509,7 +2516,7 @@ After implementation, update ALL of these that apply (per §18.1):
```powershell
supabase test db → XX/XX pgTAP tests pass
-.\RUN_QA.ps1 → 756/756 checks pass (0 failures)
+.\RUN_QA.ps1 → 759/759 checks pass (0 failures)
.\RUN_NEGATIVE_TESTS.ps1 → 23/23 caught
npx tsc --noEmit → 0 errors
npx vitest run → XXX/XXX tests pass