Saltar al contenido

Metodología

Arquitectura

La decisión que ordena todo lo demás

Hay dos motores y no se hablan:


                    ┌─────────────────────────────────────┐
   escaneo / foto → │          MOTOR DE SEGURIDAD          │ → resultado + evidencias
                    │  taxonomía · reglas · perfil         │
                    └──────────────────┬──────────────────┘
                                       │ (solo categoría, mercado, idioma)
                                       ▼
                    ┌─────────────────────────────────────┐
                    │        CONSTRUCTOR DE CONTEXTO       │
                    │  lanza excepción si detecta un       │
                    │  perfil de salud                     │
                    └──────────────────┬──────────────────┘
                                       ▼
                    ┌─────────────────────────────────────┐
                    │         MOTOR COMERCIAL              │ → anuncio o nada
                    └─────────────────────────────────────┘

La flecha va en un solo sentido. No existe un camino del motor comercial al resultado de seguridad. Está garantizado por construcción: apps/pwa/js/anuncios.js no importa packages/motor, y no puede, porque el grafo de dependencias lo impide.

Verificable en un comando:


grep -r "motor" apps/pwa/js/anuncios.js   # no devuelve nada

Topología actual (MVP)

Todo cliente. La PWA llama a Open Food Facts, ejecuta el motor en el navegador y guarda el perfil en el dispositivo.


navegador
├── PWA (ESM, sin bundler)
│   ├── escáner: BarcodeDetector nativo → decodificador propio
│   ├── OCR: proveedor manual → Tesseract bajo demanda
│   ├── motor determinista (mismo código que en Node)
│   └── perfil + historial + caché: localStorage
└── red
    └── Open Food Facts (v2, con caché local)

Por qué sin servidor todavía: el camino crítico —escanear, analizar, responder— no necesita ninguno. Un backend en el MVP añadiría latencia, una dependencia que se puede caer y una copia de datos de salud que ahora mismo no existe en ningún sitio. El backend entra cuando entren cuentas, alertas y portal de marcas, no antes.

Topología objetivo (fase 2+)


móvil ─── CDN/edge ─── API (versionada)
                        ├── servicio de resolución de producto
                        ├── servicio de análisis (motor + reglas versionadas)
                        ├── servicio OCR (cola + workers)
                        ├── servicio de búsqueda
                        └── servicio de publicidad ◄── BASE DE DATOS DISTINTA
                        │
                        ├── PostgreSQL (núcleo)
                        ├── Redis (caché, límites de uso)
                        ├── Object storage (imágenes, con caducidad)
                        └── Cola de trabajos (OCR, reanálisis, ingesta)

El servicio de publicidad va contra otra base de datos (002_publicidad.sql está escrito para poder ejecutarse aislado). Es el cortafuegos hecho infraestructura: aunque alguien escribiese la consulta equivocada, no hay JOIN posible contra un perfil.

Capas del motor de seguridad


TEXTO CRUDO
   │  normalizar.js — minúsculas, sin acentos, ß→ss, apóstrofos→espacio,
   │                   palabras partidas por guion, Y UN MAPA al texto original
   ▼
TEXTO NORMALIZADO + mapa + cortes de frase
   │  idioma.js — marcadores de etiquetado, no vocabulario general
   ▼
IDIOMA (con confianza)
   │  extraer.js — términos de la taxonomía, más largo primero,
   │                descarte por solape con falsos positivos
   ▼
COINCIDENCIAS con su tramo del texto ORIGINAL
   │  reglas.js — regiones PAL, excepciones Anexo II, declaraciones
   ▼
MATRIZ por alérgeno: contiene | puede_contener | declarado_sin |
                      declarado_muy_bajo | exceptuado | contradictoria | sin_datos
   │  perfil.js — cruce con los NO, el rigor y las condiciones
   ▼
RESULTADO por NO + evidencias + fuentes + acción sugerida

Cada paso es una función pura. Ninguna hace red. Eso es lo que permite tener un golden dataset que se ejecuta en milisegundos y bloquea despliegues.

Por qué normalizar devuelve un mapa

Es la decisión menos vistosa y la más importante de la arquitectura.

Si se normaliza el texto y se tira el original, la evidencia se convierte en «contiene trigo». Con el mapa, se convierte en:

«farine de BLÉ, sucre» BLÉ = TRIGO

Esa diferencia es el producto entero. Un icono rojo lo tiene cualquiera; la cita literal con el acento y las mayúsculas del envase es lo que hace que la persona pueda comprobarlo mirando el paquete que tiene en la mano.

Abstracciones de proveedor

Tres piezas están detrás de una interfaz para no quedarse atrapados:

InterfazImplementacionesDónde
ProveedorOCR.leer()manual, Tesseractapps/pwa/js/ocr.js
Fuente de productoOpen Food Factspackages/fuentes/
BúsquedaPostgreSQL FTSdb/migraciones/003

El proveedor manual no es un respaldo de segunda: es el único que funciona sin conexión, sin CDN y con precisión del 100 %. Siempre está.

Degradación

Cada dependencia externa tiene salida (prompt 185), y la regla general es que la seguridad no depende de nada que se pueda caer:

FallaQué pasa
Open Food FactsCaché local, marcada con su fecha. Si no hay, se ofrece fotografiar
Red enteraSe puede fotografiar y analizar: taxonomía y motor están cacheados
CDN del OCRSe abre la entrada manual de texto con una explicación
BarcodeDetectorDecodificador propio sobre canvas
Cámara denegadaEscribir código, subir foto, buscar. Nunca callejón sin salida
Motor comercialNo sale anuncio. El resultado ya estaba pintado
AnalíticaNada. Los eventos se quedan en memoria y se descartan

Rendimiento

El orden de carga está elegido, no heredado:

1. HTML + tokens + CSS de la aplicación. 2. Módulo de la aplicación y motor. 3. Después del load: service worker. 4. Solo bajo demanda: Tesseract (≈2 MB) — nunca en el arranque. 5. Solo tras pintar el resultado: el módulo comercial, en un requestAnimationFrame.

Un anuncio no puede retrasar nunca un resultado, y por eso se pinta en un frame posterior y dentro de un try.