# Knowledge base de insumos y formatos para matching de facturas

## La decisión de diseño que lo cambia todo

La tentación es hacer una lista de "todos los insumos y todos sus formatos". No funciona:
si tienes 800 insumos × 25 formas × 20 procesados × 7 conservaciones × 14 preparaciones,
son **~39 millones de filas**, la mayoría imposibles ("merluza en pasta tostada").

En su lugar, este KB separa el problema en **ejes ortogonales**:

```
línea de factura = INGREDIENTE + N facetas independientes + pack
```

`MERLUZA FILETE S/PIEL CONG. CAJA 6x1KG`

| Componente | Valor |
|---|---|
| ingrediente | `PB.MERLUZA` (familia `pescado_blanco`) |
| forma | `filete` |
| preparación | `sin_piel` |
| conservación | `congelado` |
| envase | `caja` |
| pack | 6 × 1 kg = 6 kg |

Mantienes **~800 ingredientes + ~120 valores de faceta** (unos mil registros), y el producto
cartesiano lo genera el motor, no un humano.

## Las 6 facetas

| Faceta | Qué captura | Ejemplos | Cardinalidad |
|---|---|---|---|
| `forma` | geometría física | entero, filete, lomo, rodaja, medallón, taco, tira, rallado, molido, pasta/crema, laminado, granillo, anilla | única |
| `procesado` | tratamiento | crudo, cocido, tostado, frito, ahumado, curado, salado, escabeche, encurtido, deshidratado, rebozado, garrapiñado | múltiple |
| `conservacion` | cadena de frío | fresco, congelado, ambiente, conserva, semiconserva, vacío, ATM | única |
| `preparacion` | trabajo ya hecho | eviscerado, sin piel, sin espinas, sin hueso, sin cáscara, pelado, lavado/IV gama | múltiple |
| `calidad` | grado, origen, certificación | ecológico, salvaje, acuicultura, DOP, IGP, extra, virgen extra, ibérico, MSC | múltiple |
| `envase` + `unidad` | packaging y medida | caja, bandeja, bolsa, saco, lata, brik, barril / kg, g, l, ud, docena | única |

Tu ejemplo del cacahuete son tres facetas combinándose, no tres productos:
`pasta` = forma · `molido` = forma · `crudo` = procesado · `con cáscara` = preparación.

Y la merluza: `entera` / `filete` / `medallón` = forma; `sin piel`, `sin espinas`,
`eviscerada` = preparación; `fresca` vs `congelada` = conservación.

## Las reglas de compatibilidad son la pieza clave

`data/families.json` declara, por familia, qué valores de faceta son **plausibles**.
Eso hace tres cosas:

1. **Mata falsos positivos.** "Merluza tostada en pasta" → el motor descarta ambas facetas
   y baja la confianza, en vez de inventar un SKU inexistente.
2. **Desambigua.** "Natural" significa *crudo* en frutos secos y *en su jugo* en conserva.
3. **Valida altas manuales.** Si alguien da de alta un SKU imposible, salta el aviso.

## Estructura

```
kb-insumos/
├── data/
│   ├── facets/                6 vocabularios controlados (ES+EN, sinónimos, regex)
│   ├── families.json          reglas de compatibilidad (20 familias)
│   ├── regional_aliases.json  capa de nombres por país (71 regionalismos)
│   └── ingredients/           catálogo canónico (539 insumos, Chile primero)
├── tools/                     generador del catálogo (listas compactas -> JSON)
├── engine/matcher.py          motor de matching, sin dependencias obligatorias
├── tests/                     65 líneas de factura con resultado esperado
└── README.md
```

Cada ingrediente lleva: id, nombre canónico ES/EN, nombre científico, alias, abreviaturas de
proveedor, atributos propios (calibre, variedad, origen FAO, W de fuerza…) y una lista corta de
`common_skus` — las combinaciones que de verdad circulan, útiles para subir la confianza
y para poblar un desplegable.

En **carnes**, el corte NO es faceta: es un insumo propio con precio propio (ver más abajo).

## Cómo funciona el motor

```
normalizar → extraer pack → identificar el INSUMO sobre el texto íntegro
           → enmascarar su nombre → extraer facetas de lo que queda
           → validar contra la familia → puntuar
```

La normalización expande abreviaturas de proveedor (`s/piel`→`sin piel`, `cong.`→`congelado`,
`H&G`→`eviscerado descabezado`, `IQF`→`congelado`), quita acentos y tolera typos vía fuzzy.

```bash
python engine/matcher.py "MERLUZA FILETE S/PIEL CONG. CAJA 6x1KG"
python engine/matcher.py --test     # 40/40 + 25/25 en las baterías actuales
```

## Probador visual — `tester.html`

Abre `tester.html` con doble clic. Es un único fichero autocontenido: lleva el KB y el motor
dentro, funciona sin servidor ni conexión, y nada de lo que pegues sale de tu equipo.

- **Probar** — pega líneas de factura (una por fila) y ves el insumo, las facetas con su
  etiqueta legible, el pack normalizado, el SKU más parecido, la confianza y la evidencia de
  qué texto disparó cada faceta.
- **Catálogo** — busca cualquier insumo y consulta sus formatos admitidos, alias, nombres por
  país y SKUs catalogados.
- **Formatos** — referencia completa de las 7 facetas con todos sus valores y sinónimos.

El motor JS (`engine/matcher_core.js`) es un port fiel del de Python, verificado campo a campo:
**77 líneas, 0 diferencias**. Si tocas `data/`, regenera con:

```bash
python engine/build_tester.py
```

El orden del pipeline importa. Si las facetas se extraen primero, `lomo` se consume como forma
y `lomo vetado` deja de existir como nombre de insumo. Por eso el insumo se identifica antes,
se enmascara su texto, y solo entonces se buscan facetas en el resto.

Dos reglas más, aprendidas de líneas reales:

- **Posición.** En una factura el producto encabeza la línea; lo que va detrás es detalle.
- **Acompañamiento.** Lo que sigue a *en / con / al* es el medio, no el producto: en
  `ATÚN EN ACEITE DE OLIVA` se factura atún, no aceite.

## Capa regional (LATAM + España)

Un insumo tiene **un solo id canónico** y N nombres según el país. `palta` y `aguacate` no son
dos productos: son `FR.AGUACATE` visto desde Santiago o desde Madrid.

`data/regional_aliases.json` mapea alias → ingrediente → países. El motor devuelve además
`regionalismo`, que te dice de qué mercado viene la línea:

```
PALTA HASS 18/20 CAJA 4KG   ->  Aguacate  [palta -> CL/PE/AR/BO]
Poroto negro seco saco 25kg ->  Alubia    [poroto -> CL/AR/UY/PE]
CALLAMPA SECA BOLSA 100GR   ->  Champiñón [callampa -> CL]
```

Sirve para enrutar a la lista de precios correcta y para detectar el mercado de un proveedor
nuevo sin configurarlo.

**Ojo con las colisiones.** Están marcadas en el JSON con `_nota`:

- `panela` = queso fresco en Colombia, azúcar de caña en México y Ecuador.
- `cecina` = vacuno curado en España, cerdo frito conservado en Perú.

Estas solo se resuelven sabiendo el país del proveedor. Es un argumento fuerte para guardar
el país en la ficha del proveedor y pasárselo al motor.

## Ampliar el catálogo

El catálogo grande no se edita en JSON: se edita en `tools/catalog_data.py`, en listas de
una línea por insumo.

```
PES.CONGRIO_DORADO | Congrio dorado | Golden kingklip | congrio | golden kingklip | Genypterus blacodes
        id         |   nombre ES    |    nombre EN    | alias   |    alias EN     |      científico
```

Añades líneas y regeneras:

```bash
python tools/build_catalog.py     # genera data/ingredients/*_ext.json y audita colisiones
python engine/build_tester.py     # refresca tester.html
python engine/matcher.py --test   # comprueba que no rompiste nada
```

`build_catalog.py` hace tres cosas que conviene conocer:

- **Fusiona conceptos repetidos.** Si das de alta algo que ya existe, sus alias se añaden al
  insumo existente en vez de crear un duplicado.
- **Solo compara nombres en español.** Cruzar idiomas fusiona cosas sin relación: `uñi` (baya
  chilena) normaliza igual que `uni` (erizo en japonés).
- **Audita colisiones.** Avisa cuando un mismo alias apunta a dos insumos distintos, que es la
  causa más común de matches silenciosamente erróneos.

## Modelo de carnes: el corte es el insumo

En Chile el filete, el lomo vetado y el asiento son productos distintos con precios distintos.
Cada corte es un insumo propio (`COR.VAC.LOMO_VETADO`) con un puntero `parent` al animal
(`CAR.TERNERA`). El motor devuelve ese vínculo en `parte_de`.

Hay 57 cortes catalogados de vacuno, cerdo, pollo, pavo y cordero, con sus equivalencias
españolas y argentinas (`lomo vetado` = `entrecot` = `bife ancho` = `ribeye`).

## Cómo escalarlo a cobertura real

1. **Las facetas ya están casi completas.** Añadirás valores puntuales, no familias enteras.
2. **Los ingredientes crecen por extracción, no a mano.** Coge 6-12 meses de líneas de factura
   reales, quita las que ya matchean, agrupa el residual por frecuencia y da de alta el top.
   El 80% del volumen suele caber en 300-500 insumos.
3. **Los alias se aprenden del feedback.** Cada corrección humana ("esto era merluza") se
   escribe de vuelta como alias. Es el mecanismo que hace que el KB mejore solo.
4. **Códigos externos.** Cuando lo tengas estable, mapea cada ingrediente a CPV / GS1 GPC /
   CN8 para conciliar con proveedores y aduanas.

## Dos cosas que conviene decidir pronto

- **Umbral de auto-aceptación.** Ahora `necesita_revision` salta bajo 0.75. Calíbralo con
  tus datos: mide qué % de errores pasa el filtro antes de automatizar el asiento contable.
- **Precio como señal.** El precio/kg es un discriminador potentísimo: si "merluza filete"
  entra a 4 €/kg es congelada, a 18 €/kg es fresca de pincho. Merece la pena añadir rangos
  de precio esperados por SKU como validación cruzada.

## Limitaciones conocidas

- El catálogo son 63 insumos **semilla**, elegidos para cubrir todas las familias y probar el
  motor. No es cobertura de producción.
- El `residual_no_interpretado` arrastra fragmentos de palabras cuando una regex de faceta
  parte un término. Es cosmético: no afecta al match, pero ensucia la depuración.
- No hay desambiguación por proveedor. En la práctica el mismo código de proveedor siempre
  significa lo mismo, así que una tabla `proveedor + código → SKU` resolvería el caso fácil
  antes de llegar a este motor.
