# Guía de integración para el equipo de desarrollo

Este repositorio es un **motor de normalización de líneas de factura**: recibe el texto libre
que escribe el proveedor y devuelve un insumo canónico, sus atributos de formato y la cantidad
normalizada. No es un servicio: son datos + una función pura.

## Primeros 5 minutos

```bash
python3 engine/matcher.py --test                     # 40/40, sin instalar nada
python3 engine/matcher.py "CONGRIO FILETE CONG 1KG"  # ver la salida real
open tester.html                                     # probador visual, sin servidor
```

Requisitos: Python 3.10+. No hay dependencias obligatorias. `rapidfuzz` es opcional y solo
acelera. `node` solo hace falta si quieres verificar la paridad del motor JS.

## Qué se entrega

| Ruta | Qué es | ¿Se edita a mano? |
|---|---|---|
| `data/facets/*.json` | 7 vocabularios controlados con sinónimos y regex | Sí |
| `data/families.json` | Reglas de compatibilidad por familia (20) | Sí |
| `data/regional_aliases.json` | Nombres por país (71) | Sí |
| `data/ingredients/*.json` | Catálogo canónico (539 insumos, 3.545 alias) | No, se genera |
| `tools/catalog_data.py` | Fuente del catálogo, una línea por insumo | **Sí, aquí se amplía** |
| `engine/matcher.py` | Motor de referencia (Python, sin dependencias) | Sí |
| `engine/matcher_core.js` | Port a JS, verificado con paridad exacta | Sí, en paralelo |
| `tester.html` | Probador visual autocontenido | No, se genera |
| `tests/*.json` | 65 líneas con resultado esperado | Sí, al añadir casos |

## Contrato de la función

```python
from matcher import KB, match_line
kb = KB.load()                    # cargar UNA vez al arrancar, no por línea
resultado = match_line(kb, "MERLUZA FILETE S/PIEL CONG. CAJA 6x1KG")
```

```jsonc
{
  "ingrediente":  { "id": "PB.MERLUZA", "nombre": "Merluza", "familia": "pescado_blanco" },
  "parte_de":     null,              // si es un corte: { id, nombre } del animal
  "regionalismo": null,              // si el término es local: { termino, paises[], colision }
  "facetas": {
    "conservacion": ["congelado"],   // cardinalidad única
    "preparacion":  ["sin_piel"],    // cardinalidad múltiple
    "forma":        ["filete"],
    "envase":       ["caja"]
  },
  "pack": { "cantidad": 1, "unidad": "kg", "piezas_por_pack": 6,
            "cantidad_base": 6, "unidad_base": "kg" },
  "sku_sugerido": { "label_es": "Filete de merluza limpio", "coincidencias": 3 },
  "confianza": 1.0,
  "necesita_revision": false,
  "evidencia": ["conservacion=congelado <- 'congelad'", "..."],
  "avisos": [],                      // facetas descartadas por incompatibilidad
  "residual_no_interpretado": ""
}
```

El motor es **puro y determinista**: misma entrada, misma salida. No hace red ni I/O más allá
de cargar el KB. Se puede paralelizar sin cuidados.

## Cómo integrarlo

**Opción A — servicio Python.** Envolver `match_line` en un endpoint. `KB.load()` tarda
~390 ms y consume poco; hacerlo al arrancar el proceso y compartir el objeto.

**Opción B — en el navegador.** `engine/matcher_core.js` no tiene dependencias.
`buildKB(raw)` una vez, `matchLine(kb, linea)` por línea. Es lo que hace `tester.html`.

**Opción C — batch.** Recorrer el CSV/XML de facturas y volcar el JSON. Para lotes grandes,
instalar `rapidfuzz` (opcional): el motor lo detecta y acelera el fuzzy.

## Reglas de negocio que hay que respetar al integrar

1. **`necesita_revision` es la señal, no `confianza`.** El umbral (0,75) está en un único sitio
   en `match_line`. Calíbralo con datos reales antes de automatizar asientos contables.
2. **Nunca autoaceptar con `avisos` no vacíos.** Significa que el texto pedía una faceta
   imposible para esa familia: o el catálogo está mal o la línea es rara.
3. **Normaliza siempre por `cantidad_base` + `unidad_base`.** `cantidad` y `unidad` son lo
   que decía el proveedor; comparar precios con eso da errores de factor 1000.
4. **`unidad: "caja"` no es comparable.** Requiere el peso por caja del proveedor.
5. **Guarda el país del proveedor y pásalo al motor.** Hay colisiones que solo se resuelven
   así: `panela` es queso en Colombia y azúcar en México; `cecina` es vacuno en España y
   cerdo en Perú. Están marcadas con `_nota` en `regional_aliases.json`.

## El bucle de mejora (lo más importante)

El KB no se mantiene solo pero mejora casi solo si se cablea esto:

```
línea sin match o marcada para revisión
        ↓
un humano elige el insumo correcto en el back-office
        ↓
el texto original se escribe como alias de ese insumo
        ↓
la siguiente factura del mismo proveedor matchea sola
```

Sin este bucle el catálogo se queda donde está. Con él, cada corrección vale para siempre.
Guarda también `proveedor + código de artículo → insumo`: el mismo código de un proveedor
siempre significa lo mismo, y esa tabla resuelve el caso fácil **antes** de llegar al motor.

## Cómo ampliar el catálogo

Se edita `tools/catalog_data.py`, 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
```

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

`build_catalog.py` fusiona conceptos repetidos en vez de duplicarlos, compara solo nombres en
español (cruzar idiomas fusiona cosas sin relación) y **audita alias ambiguos**. Haz caso a esa
auditoría: un alias que apunta a dos insumos es la causa más común de errores silenciosos.

## Los dos motores tienen que ir a la vez

`matcher.py` y `matcher_core.js` implementan la misma lógica. Si tocas uno, toca el otro y
verifica la paridad:

```bash
python engine/build_tester.py
node -e "..."   # ver el bloque de verificación en el README
```

Hoy hay **paridad exacta en 77 líneas y todos los campos**. Dos trampas ya encontradas y
resueltas, por si aparecen otras: el `\w` de JavaScript es ASCII (rompe `1ª` → `primera`) y
`difflib` de Python no es distancia de edición (cambia el umbral de typos).

## Rendimiento y límites conocidos

- **~360 líneas/s en Python, ~530 en JavaScript** (medido, un solo hilo, sin `rapidfuzz`).
  `KB.load()` tarda ~390 ms: hazlo al arrancar, nunca por línea.
- El matching recorre linealmente los 3.545 alias con un prefiltro de subcadena. Con este
  catálogo va sobrado; si crece a decenas de miles de alias hay que indexar por token
  (diccionario token → alias candidatos) en vez de recorrer la lista.
- Ojo si tocas el bucle de `match_exact`: la primera versión hacía una búsqueda lineal del
  ingrediente por cada alias que coincidía y rendía **9 líneas/s**. El índice `by_id` y el
  prefiltro lo multiplicaron por 40.
- `residual_no_interpretado` arrastra fragmentos de palabras cuando una regex de faceta parte
  un término. Es cosmético, solo ensucia la depuración.
- El motor asume **un insumo por línea**. Las líneas con dos productos reales
  ("tabla de quesos y jamones") devolverán solo el dominante.

## Estado de validación

| Batería | Resultado |
|---|---|
| `tests/lineas_factura.json` | 40/40 |
| `tests/stress.json` (typos, inglés, no alimentarios, facetas imposibles) | 25/25 |
| Carta real de un restaurante chileno (114 términos) | 114/114 |
| Auditoría de alias ambiguos | 0 colisiones |
| Paridad Python ↔ JavaScript | 0 diferencias en 77 líneas |
