Saltar al contenido

Cómo sabemos lo que sabemos

Motor de alérgenos

El fichero que decide es packages/motor/src/reglas.js. Ninguna IA entra ahí.

Los siete estados de la matriz

No hay booleanos. false y «no lo sabemos» no son lo mismo, y confundirlos es exactamente cómo se produce un falso negativo.

EstadoSignificaEjemplo
contieneIngrediente detectado en la listaharina de TRIGO
puede_contenerDetectado dentro de una declaración precautoriapeut contenir des arachides
declarado_sinEl envase o el fabricante lo declaranSIN GLUTEN
declarado_muy_bajoMención «muy bajo en gluten»≤100 mg/kg, no equivale a sin gluten
exceptuadoDerivado que el Anexo II exceptúa de declararwheat based glucose syrup
contradictoriaDos afirmaciones incompatiblesSIN GLUTEN + harina de trigo
sin_datosNo hay informaciónlista ausente o ilegible

Cómo se decide el estado

Dentro de una misma fuente, por severidad creciente:


sin_datos  <  exceptuado  <  puede_contener  <  contiene

Un contiene nunca lo baja un puede_contener. Y por encima de todo:


declarado_sin  +  (contiene | puede_contener)  =  contradictoria

Esa regla vale igual dentro de una fuente y entre fuentes distintas. Una declaración de ausencia conviviendo con una detección no se resuelve a favor del verde: se le enseña a la persona.

De la matriz al resultado

perfil.js traduce estado + cobertura + rigor a lo que ve la persona.

El caso interesante es sin_datos, porque es donde se cuela el optimismo:


sin_datos
  ├── grupo con umbral legal de declaración (sulfitos)
  │     → INFORMACIÓN INSUFICIENTE, siempre.
  │       Por debajo de 10 mg/kg no hay obligación de declararlos: su ausencia
  │       en la etiqueta no es evidencia de ausencia en el producto.
  │
  ├── el texto NO parece una lista de ingredientes
  │     → INFORMACIÓN INSUFICIENTE
  │
  └── el texto sí parece una lista de ingredientes
        → NO HEMOS DETECTADO <X>, con la coletilla de que no nos consta
          que la lista sea completa si no lo sabemos.

«Parece una lista de ingredientes» no se decide contando caracteres. Se exige un encabezado reconocido (Ingredientes, Zutaten, Ingrédients…) o estructura de lista: tres o más elementos separados y al menos 25 caracteres. Producto alimenticio. tiene veintiuno y no es una lista.

El mecanismo anti-falso-positivo

Cada ingrediente puede declarar falsos_positivos: sintagmas normalizados que redefinen su núcleo. Si una coincidencia solapa una aparición de uno de ellos, se descarta.

Solape, no contención. La diferencia importa:


texto:      "farine de blé noir"
término:    "farine de ble"   [0, 13)
falso pos.: "ble noir"        [10, 18)

El término empieza antes del falso positivo, así que no está contenido en él. Si se exigiera contención, el alforfón francés pasaría por trigo. Con solape, se descarta. Este caso es g021 en el golden dataset, y es de severidad 0.

Falsos positivos que ya están cubiertos: Buchweizen, blé noir, grano saraceno, trigo sarraceno, maltodextrina, maltitol, leche/lait/latte de coco, cocoa butter, beurre de cacao, peanut butter, noix de coco, nuez moscada, peanuts dentro de nuts, y todas las declaraciones de ausencia (sin gluten contiene la palabra gluten).

Modos de coincidencia

  • palabra — con límite de palabra a ambos lados. Es el modo por defecto.
  • raiz — subcadena en cualquier posición. Solo para lenguas aglutinantes (alemán), y solo con raíces distintivas de cinco caracteres o más: weizen, milch, erdnuss, haselnuss.

El alemán ei (huevo) va como palabra, nunca como raíz: aparece dentro de decenas de palabras corrientes. Los compuestos se listan explícitamente (eigelb, eiweiss, volleipulver).

Declaraciones precautorias (PAL)

En la UE el «puede contener» es voluntario y no está armonizado. De ahí dos reglas:

1. Un PAL nunca se convierte en contiene. Son riesgos distintos. 2. La ausencia de PAL no es evidencia de ausencia de contaminación cruzada. No se convierte en tranquilidad.

El alcance de una región PAL va del patrón hasta el final de la frase (., ;, :, |, salto de línea) o 160 caracteres, lo que llegue antes. Todo ingrediente detectado dentro de ese tramo se clasifica puede_contener.

Avena

La avena es un cereal del Anexo II y por defecto produce contiene gluten. Además genera siempre una nota explícita:

Contiene avena. La avena sin contaminar se tolera en muchos casos de celiaquía, pero no en todos, y la avena común suele llevar trazas de trigo. Sin una declaración de avena sin gluten no la damos por apta.

Solo con una declaración explícita (avena sin gluten, gluten free oats, glutenfreier Hafer…) y siendo la avena el único cereal detectado, el estado pasa a declarado_sin.

Excepciones del Anexo II

Degradan contiene a exceptuado, nunca a declarado_sin, y siempre se muestran con su explicación. Con perfil estricto vuelven a avisar como puede_contener.

Todas están marcadas requiere_revision_regulatoria y no deben considerarse verificadas. Ver REGULATORY.md.

Trampa cubierta: la excepción de la soja exige «totalmente refinado». aceite de soja a secas no entra en la excepción y sigue siendo contiene. Es el caso g052, severidad 0.

Versionado de reglas

Cada evidencia lleva pegada la regla que la produjo:


regla ingredient:wheat_fr_reglas-eu-2026.09.1 · fuente etiqueta · 2026-09-16

Las reglas nunca se actualizan en silencio. Cada análisis guarda su rule_version, lo que permite reanalizar en masa cuando cambia una regla crítica y explicar por qué ayer decía otra cosa. En base de datos, un cambio crítico exige dos personas distintas (CONSTRAINT four_eyes).

Añadir un idioma

1. Añadir términos a packages/taxonomia/datos/ingredientes.json con su lang y su modo. 2. Añadir marcadores a packages/motor/src/idioma.js. 3. Añadir patrones PAL y de declaración a sus ficheros. 4. Añadir casos al golden dataset, incluidos los falsos positivos del idioma. Un idioma sin casos trampa no está validado. 5. Medir precisión, recall y falsos negativos. Sin eso, el idioma no sale de experimental en la tabla languages.

El paso 4 no es opcional. Cada idioma trae sus propias trampas: el francés trae blé noir, el alemán trae Buchweizen, el italiano trae grano saraceno.