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:
| Interfaz | Implementaciones | Dónde |
|---|---|---|
ProveedorOCR.leer() | manual, Tesseract | apps/pwa/js/ocr.js |
| Fuente de producto | Open Food Facts | packages/fuentes/ |
| Búsqueda | PostgreSQL FTS | db/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:
| Falla | Qué pasa |
|---|---|
| Open Food Facts | Caché local, marcada con su fecha. Si no hay, se ofrece fotografiar |
| Red entera | Se puede fotografiar y analizar: taxonomía y motor están cacheados |
| CDN del OCR | Se abre la entrada manual de texto con una explicación |
| BarcodeDetector | Decodificador propio sobre canvas |
| Cámara denegada | Escribir código, subir foto, buscar. Nunca callejón sin salida |
| Motor comercial | No sale anuncio. El resultado ya estaba pintado |
| Analítica | Nada. 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.